JavaScript笔记——DOM操作

前言

本篇文章以介绍常见的DOM节点知识、DOM元素操作方法为目的,其中也对一些比较容易忽略的问题进行简要说明。才疏学浅,有说错的地方或优化的建议也欢迎留下评论。

Node节点

首先,简单看看Node节点。有三个属性个人认为比较需要注意,nodeType、nodeName、nodeValue。

  1. nodeType——节点类型
    返回的是一个整数,表面节点的类型。包括元素节点(1)、文本节点(3)、注释节点(8)等。详见Node.nodeType
  2. nodeName——节点名称
    返回节点名称。
    元素节点的 nodeName 与标签名相同
    文本节点的 nodeName 始终是 #text
    文档节点的 nodeName 始终是 #document
    详见Node.nodeName

  3. nodeValue——节点值
    元素节点的 nodeValue 是 null
    文本节点的 nodeValue 是文本本身
    详见Node.nodeValue

Node节点间的关系

Node节点关系
这个图是来自《Javascript高级程序设计》一书中的Node节点间的关系图谱,比较清晰地介绍了节点之间的关系。

  • parentNode: 父节点
  • childNodes: 所有子节点
  • firstNode: 第一个子节点
  • lastNode: 最后一个子节点
  • previousSibling: 前一个兄弟节点
  • nextSibling: 下一个兄弟节点

特别注意上述属性获取的并不只是元素节点,也会包含文本节点等。所以进行操作时需要进行元素类型判断过滤。
此外,还有一些方式可以获得相关的元素节点。

  • Node.parentElement
  • ParentNode.children(IE6-8返回的元素可能会包含注释节点)
  • ParentNode.childElemnetCount
  • ParentNode.firstElementChild
  • ParentNode.lastElementChild
  • NonDocumentTypeChildNode.previousElementSibling
  • NonDocumentTypeChildNode.nextElementSibling

DOM元素获取方法

方法 简述 兼容性
getElementById(‘id’) 通过id获取 -
getElementsByTagName(‘p’) 通过标签名获取 -
getElementsByClassName(‘class’) 通过class获取 IE>= 9
getElementsByName(‘name’) 通过name属性获取 -
querySelector() 返回匹配选择器的第一个元素 IE >= 8
querySelectorAll() 返回匹配选择器的所有元素 IE >=8

特别注意:querySelectorAll()与其他方法获取的DOM元素是不同的,它返回的是静态的
NodeList 对象,其他返回的是动态的 HTMLCollection 对象。静态意味着不会随着DOM结构的变换而改变。举例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// html
<ul id="list">
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
<li>6</li>
</ul>
//js
let list = document.getElementById('list'),
child1 = document.getElementsByTagName('li'),
child2 = document.querySelectorAll('li')
console.log(child1.length) // 6
console.log(child2.length) // 6
let ele = document.createElement('li')
ele.innerHTML = 7
list.appendChild(ele)
console.log(child1.length) // 7
console.log(child2.length) // 6

所以,在使用getElementsByTagName、getElementsByClassName、getElementsByName方法时要特别注意循环处理DOM节点的情况。

创建DOM节点

createElement() 创建一个元素节点
createTextNode() 创建一个文本节点
createAttribute() 创建一个属性节点(用setAttribute方法更加方便)
createDocumentFragment() 创建一个文档片段(适合在批量操作DOM元素时使用,详见后面章节的例子)

DOM元素内容属性获取

  1. 元素内容的获取
    这里有几个容易混淆的属性,innerHTML、outerHTML、innerText、outerText、textContent,都是可以获取元素内容。区别如下:
属性 描述
innerHTML 返回HTML文本,存在XSS攻击的问题。
outerHTML 返回内容包含元素及其后代的HTML文本。
textContent 返回元素所有文本内容,包括隐藏元素的文本,包括<style><script>不会返回HTML文本,避免直接设置HTML文本。(兼容性:IE9+)
innerText 返回文本内容,受CSS样式影响,会触发DOM重排,不包括隐藏元素的文本,不包括<style><script>,避免直接设置HTML文本。
outerText 非标准属性。获取时返回与innerText相同内容,设置时删除当前节点替换为给定文本。
  1. 元素属性
    Element.attributes(): 引用MDN官网的描述

    返回该元素所有属性节点的一个实时集合。该集合是一个 NamedNodeMap 对象,不是一个数组,所以它没有数组的方法,其包含的属性节点的索引顺序随浏览器不同而不同。更确切地说,attributes 是字符串形式的名(name)/值(value)对,每一对名/值对对应一个属性节点。

    ele.getAttribute(attributeName) 获取属性
    ele.setAttribute(name, value) 设置属性
    HTMLElement.dataset: 获取data-*属性集

  2. 元素样式
    HTMLElement.style 返回元素的内联样式(没错,样式表的属性会被忽略)
    单个样式的设置:ele.style.color='#000'
    多个样式的设置:

    • 依次设置
    • ele.style.cssText='color: blue'
    • ele.setAttribute('style', 'color: blue')

    获取元素样式信息:

    • window.getComputedStyle(ele).color
    • window.getComputedStyle(ele).getPropertyValue('color')。getComputedStyle方法接受第二个参数为伪元素,如'::after'。关于getComputedStyle详细介绍可以看看张鑫旭大神的获取元素CSS值之getComputedStyle方法一文。
  3. 元素类名
    className 获取或设置元素的类名。
    classList 只读,返回元素的类属性的实时 DOMTokenList集合。但可以使用 add() 和 remove() 方法修改。也有类似jQuery的toggle方法,但是兼容性较差。

插入DOM元素

方法 简述
node.appendChild(newNode) 向node节点插入一个新节点newNode
node.insertBefore(newNode, tarNode) 在node节点的tarNode子节点前插入一个新节点newNode
node.replaceChild(newNode, tarNode) 替换node节点的tarNode子节点为新节点newNode
node.removeChild(tarNode) 移除node节点的tarNode子节点
cloneNode(flag) 复制节点,flag: true 深复制;flag: false 浅复制

此处深复制为复制节点及其整个子树,浅复制则仅复制节点本身。

DOM操作的性能问题

频繁进行DOM操作其实会造成多次重排Reflow,影响性能。举个常见的例子,在id为container的元素中添加5个按钮,每个按钮的文案是相应序号,点击打印输出对应序号。解决办法有以下几种:

  1. 依次创建button元素,使用appenChild添加到列表中
    当然,这个方法是最不推荐的,因为多次对DOM进行操作,会造成多次页面重排,性能太差。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    let container = document.getElementById('container')
    for(let i = 1; i < 5; i++) {
    let btn = document.createElement('button'),
    text = document.createTextNode(i)
    btn.appendChild(text)
    btn.addEventListener('click', () => {
    console.log(i)
    })
    container.appendChild(btn)
    }
  2. 利用DocumentFragment
    引用MDN官网关于DocumentFragment的介绍:

    DocumentFragment 接口表示一个没有父级文件的最小文档对象。它被当做一个轻量版本的 Document 使用,用于存储已排好版的或尚未打理好格式的XML片段。最大的区别是因为DocumentFragment不是真实DOM树的其中一部分,它的变化不会引起DOM树的重新渲染的操作(reflow) ,或者导致性能影响的问题出现。

    没错,利用DocumentFragment我们能够避免方法1中多次操作DOM的问题,性能得到提升。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    let container = document.getElementById('container'),
    fragment = document.createDocumentFragment()
    for(let i = 1; i < 5; i++) {
    let btn = document.createElement('button'),
    text = document.createTextNode(i)
    btn.appendChild(text)
    btn.addEventListener('click', () => {
    console.log(i)
    })
    fragment.appendChild(btn)
    }
    container.appendChild(fragment)
  3. 利用字符串拼接
    使用字符串拼接的方法插入DOM元素是效率最高的。并且,这里将事件绑定到了父元素上,一方面可以使用动态添加元素的事件,另一方面当需要在大量元素上绑定事件时,这种方法更加优雅并且节省内存。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    let container = document.getElementById('container'),
    btns = '';
    for(let i = 1; i < 5; i++) {
    let btn = `<button class="btn_num">${i}</button>`
    btns += btn
    }
    container.addEventListener('click', (event) => {
    let target = event.target
    if(target.className == 'btn_num') {
    console.log(target.innerHTML)
    }
    })
    container.innerHTML = btns