本文总结了常用的 js 进行 DOM 修改的操作,主要总结节点的创建、修改和元素属性的管理的一些相关的操作。
节点创建
常用的和节点创建有关的接口主要有:document.createElement
、document.createTextNode
、document.createDocumentFragment
、 document.createAttribute
、document.adoptNode
、document.importNode
、node.cloneNode
。
document.createElement
document.createElement(tagName)
这是我们最常用的一个方法了,创建由标签名称(tagName
)指定的 HTML 元素,如果标签名称不是一个有效的 HTML 元素,不会报错,会创建一个 HTMLUnknownElement对象。
HTML 对大小写不敏感,所以使用大写或者小写的标签名称都可以,但是注意,标签中不能包含尖括号 <>
,否则会报错。
var newDiv = document.createElement('div'); |
注意,通过 createElement
创建的元素,并未添加到 HTML 文档中,需要调用 appendChild
等节点修改方法将其添加到 HTML 文档树中。后几节创建的其他类型的节点也是如此,后文不再对此进行说明。
document.createTextNode
document.createTextNode(text)
创建一个文本节点,参数(text
)为文本节点的内容。
这个方法返回的节点,被浏览器当作文本渲染,而不是当作 HTML 代码渲染,因此会对 HTML 代码进行转义,可以用来展示用户的输入,避免 XSS
攻击:
function escapeUserInput(str) { |
但是,该方法不对单引号和双引号转义,因此用来为属性赋值的时候,仍然会被 XSS
攻击:
|
document.createDocumentFragment
document.createDocumentFragment()
方法创建一个新的 DocumentFragment
对象。
DocumentFragments
存在于内存中,并不在 DOM 树存在,将元素插入到文档片段也不会引起回流。因此创建文档片段,并将复杂的 DOM 结构附加到文档片段中,然后将文档片段附加到 DOM 树,可以优化性能。
var ul = document.getElementById('list'); |
document.createAttribute
document.createAttribute(attrName)
方法创建并返回一个新的属性节点。这个方法不是很常用用,因为添加属性通常用 node.setAttribute
即可:
var node = document.getElementById('div'); |
document.adoptNode
document.adoptNode(externalNode)
从其他的 document 中获取一个节点(externalNode
),并将该节点以及它的所有子节点从原文档删除, 并且它的 ownerDocument
属性会变成当前的 document。之后你可以把这个节点插入到当前文档中,不常用,了解即可。
// 该函数用来从本文档的第一个 iframe 中获取第一个 element 元素, |
注意,该方法在同一 document 下的不同两个元素中也可以使用,可以实现从左边栏列表中选取某些元素加载到右边栏的功能。
注意,如果节点资源来自不同的源的时候,调用 adoptNode 可能会失败。
有些情况下,将外部文档的节点插入当前文档之前,你需要使用 document.importNode() 从外部文档导入源节点,了解更多细节。
document.importNode
document.importNode(externalNode, deep)
这个接口也不常用,作用是拷贝外部文档的一个节点(externalNode
)。deep
表明是否要导入节点的后代节点,默认为 false 不导入后代节点。
var iframe = document.getElementsByTagName("iframe")[0]; |
注意,这个方法仅拷贝节点,此时,节点存在于内存中,还需要插入当前文档中才能显示。
node.cloneNode
node.cloneNode(deep)
方法返回该节点的一个副本,deep
可选,表明是否采用深度克隆,如果为 true,则该节点的所有后代节点也都会被克隆,否则,只克隆该节点本身。
注意,克隆节点会克隆节点的所有属性,但是绑定的事件呢?我们来看一下:
首先看一下使用内联方式直接写在 HTML 标签上的 onevent
事件:
<div id="old"> |
结果显示,使用内联方式写在 HTML 上的事件是可以的,接下来看一下绑定在节点对象上的 on-event
事件:
<div id="old"> |
从测试结果来看,使用 js 绑定在节点对象上的 on-event
事件在克隆的副本并不包含事件处理程序。
接下来再看一下使用 addEventListener
方法添加在这个节点上的事件监听函数:
<div id="old"> |
也就是说,副本节点只能绑定使用内联方式绑定的事件处理函数。
注意,这个拷贝的节点并不在文档中,需要自行添加到文档中。同时拷贝的节点有可能会导致节点的的 id 属性重复,最好修改新节点的 id,而 name 属性也可能重复,自行决定是否需要修改。
节点修改
和节点内容修改有关的接口主要有 node.appendChild
、 node.insertBefore
、 node.removeChild
、 node.replaceChild
这四个接口。
node.appendChild
parentNode.appendChild(child)
方法将一个节点(child
)添加到指定父节点(parentNode
)的子节点列表的末尾。本方法返回值为要插入的这个节点。
// 创建一个新的段落p元素,然后添加到body的最尾部 |
注意,如果被插入的节点已经存在文档树中,则节点会被从原先的位置移除,并插入到新的位置,当然,被移动元素被绑定的事件也会被同步过去:
// html |
如果要保留原来的这个子节点的位置,则可以用 Node.cloneNode
方法复制出一个节点的副本,然后再插入到新位置:
// html |
这个方法只能将某个子节点插入到同一个文档的其他位置,如果你想跨文档插入,需要先调用 document.importNode
方法。
还有,如果 appendChild
方法的参数是 DocumentFragment
节点,那么插入的是 DocumentFragment
的所有子节点,而不是 DocumentFragment
节点本身。此时,返回值是一个空的 DocumentFragment
节点。
node.insertBefore
parentNode.insertBefore(child, referenceNode)
方法将一个节点(child
)插入作为父节点(parentNode
)的一个子节点,并且位置在参考节点(referenceNode
)之前。
如果第二个参数 referenceNode
为 null,则插入位置为父节点的末尾:
parentNode.insertBefore(node, null); |
注意,第二个参数为 null 时不能省略,否则会抛出异常:
Uncaught TypeError: Failed to execute 'insertBefore' on 'Node': 2 arguments required, but only 1 present. |
使用这个方法可以模拟 prependChild
,产生类似于 appendChild
,但是将节点插入作为指定父节点的第一个子节点:
Node.prototype.prependChild = function(node) { |
使用这个方法还可以模拟 insertAfter
,将节点要插在父节点的某个子节点后面:
Node.prototype.insertAfter = function(node, referenceNode) { |
和 appendChild
类似,如果插入的节点是文档中已经存在的节点,则会移动该节点到指定位置,并且保留其绑定的事件。
node.removeChild
parentNode.removeChild(child)
删除指定父节点(parentNode
)的一个子节点(child
),并返回被删除的节点。
注意,这个方法是要在被删除的节点的父节点上调用的,而不是在被删除节点上调用的,如果参数节点不是当前节点的子节点,removeChild
方法将报错:
// 通过 parentNode 属性直接删除自身 |
使用这个方法也可以很简单的模拟 removeAllChild
:
Node.prototype.removeAllChild = function() { |
被移除的这个子节点仍然存在于内存中,只是不在当前文档的 DOM 中,仍然还可以被添加回文档中。但是如果不使用一个变量保存这个节点的引用,被删除的节点将不可达,会在某次垃圾回收被清除。
node.replaceChild
parentNode.replaceChild(newChild, oldChild)
方法用指定的节点(newChild
)替换当前节点(parentNode
)的一个子节点(oldChild
),并返回被替换的节点(oldChild
)。
<div> |
元素修改
元素是节点的一种,除了拥有上一节节点修改的一些方法外,元素还继承了 ParentNode 和 ChildNode 两个接口,因此拥有一些额外的修改方法:继承自 ParentNode
的 ParentNode.append
、 ParentNode.prepend
和继承自 ChildNode
的 ChildNode.before
、 ChildNode.after
、 ChildNode.remove
、 ChildNode.replaceWith
这几个方法。
但是要注意的是,ParentNode
和 ChildNode
这两个都是原始接口,只能在实现了它的对象上使用。如果当前节点是父节点,就会继承 ParentNode
接口。由于只有元素节点(Element
)、文档节点(Document
)和文档片段节点(DocumentFragment
)拥有子节点,因此只有这三类节点会继承 ParentNode
接口。如果一个节点有父节点,那么该节点就继承了 ChildNode
接口。
还要注意的是,这些方法目前都是实验性方法,浏览器的支持度还不太够,未来也可能发生变动,请谨慎使用。
append 和 ParentNode.prepend
parentNode.append((Node/String) ...nodes)
方法为指定节点的最后一个子节点之后插入一组节点。
和 node.appendChild
方法类似,不同点在于:
append
方法允许添加 string 做为参数,并将其包装为文本节点,而appendChild
方法只能接受 Node 对象作为参数。append
方法允许添加多个参数,而appendChild
方法只能接受一个节点。append
方法没有返回值,而appendChild
方法返回被添加的节点。
parentNode.prepend(...[Node/String])
方法为指定节点的第一个子节点之后插入一组节点,其他内容和 append
方法类似,不再说明。
// old html |
使用 appendChild
和 insertBefore
可以很容易实现如下的 Polyfill:
function getDocFrag() { |
ChildNode.before 和 ChildNode.after
childNode.before(...[Node/String])
方法在指定节点的前面插入一组节点,childNode.after(...[Node/String])
方法在指定节点的后面插入一组节点,其他内容和 parentNode.append
方法类似,不再说明。
childNode.replaceWith((Node/String) ...nodes)
用指定节点(nodes
),替换当前节点(childNode
)。
使用 insertBefore
可以很容易实现如下的 Polyfill:
(function (arr) { |
Element.insertAdjacentHTML、 Element.insertAdjacentText
如果习惯了使用 jQuery
,就一定会对 jQuery 中的可以直接在参数中使用Html
的方式很是喜欢(比如 $(ele).append('<div>content</div>')
),而原生的 DOM 也有支持直参数为 Html
的方法。
Element.insertAdjacentHTML(position, text)
Element.insertAdjacentText(position, text)
- position: 这个参数指定了元素的插入位置,取值为以下4种:
- ‘beforebegin’: 元素自身的前面,效果类似于
childNode.before(ele)
。 - ‘afterbegin’: 插入元素内部的第一个 子节点 之前,效果类似于
ParentNode.prepend(ele)
。 - ‘beforeend’: 插入元素内部的最后一个 子节点 之后,效果类似于
ParentNode.append(ele)
。 - ‘afterend’: 元素自身的后面,效果类似于
childNode.after(ele)
。
- ‘beforebegin’: 元素自身的前面,效果类似于
- text: 要插入的文本或者html字符串。
// origin html |
两个方法的区别在于 insertAdjacentHTML
方法会将字符串解析为 html,而 insertAdjacentText
不会做解析,因此在不需要解析的情况下(如添加纯文本)使用 insertAdjacentText
性能会更好。
ChildNode.remove
childNode.remove()
方法把它从它所属的 DOM 树中删除,没有返回值,所以在删除前,需要先保留引用:
// old html |
使用 removeChild
可以很容易实现如下的 Polyfill:
(function (arr) { |
ChildNode.replaceWith
childNode.replaceWith((Node/String) ...nodes)
用指定节点(nodes
),替换当前节点(childNode
)。
使用 replaceChild
可以很容易实现如下的 Polyfill:
(function (arr) { |
元素属性操作
这些操作仅针对节点类型为 Element
的节点,即元素。
读取属性
读取属性可以使用 element.getAttribute(attrName)
。如果指定的属性不存在,则返回 null 。(旧版本的浏览器可能会返回空字符串""
)
修改属性
修改属性可以使用 element.setAttribute(attrName, attrValue)
。 如果此属性已经存在,则更新该值, 否则,将添加一个新的属性用指定的名称和值。
删除属性
删除属性可以使用 element.removeAttribute(attrName)
。尝试删除不存在的属性并不会引发异常。
注意,当不需要某个属性的时候,优先使用 removeAttribute
,而非使用 setAttribute
将属性值设置为 null。
检查属性是否存在
检查属性可以使用 element.hasAttribute(attrName)
。
修改元素节点对象的属性值
当然,可能你已经见过这种操作元素属性的方法:
// 修改属性 |
这种方法通过修改元素节点对象的属性值来修改元素的属性,但是有一定的局限性:
1.有些 HTML 的属性名是 Javascript 的保留字,比如 for
、class
,所以使用的时候必须更改:
ele.htmlFor = "someValue"; |
2.getAttribute
获取的 HTML 属性值都是字符串,而这种方法有时会对某些属性的值进行类型转换,比如,将字符串的 true/false
转为布尔类型,将 onClick
的值转为函数类型。
3.这种方法无法删除属性,即使用 delete
运算符不生效(自定义属性 data-[attr-name]*
是可以删除的)。
4.这种方法对非 HTML 支持的标准属性不生效,尽管这种非标准属性是不建议使用的:
// html |
标准的自定义属性 data-[attr-name]
则使用 ele.dataset.attrName
。
5.元素的属性名大小写不敏感,而 JavaScript 对象的属性名大小写敏感的,并且当属性名包括多个单词,会对属性名进行驼峰拼写法转换:
<button onclick="doSthing"></button> |
优先使用上面提到的四个属性操作的方法进行属性的修改。
获取元素的全部属性
Element.attributes
属性返回该元素所有属性节点的一个实时集合。
实时 指的是元素属性的任意变化都会反映在这个属性上:
// html |
集合是一个类数组结构,大概是这个样子的:
// html |
集合的常用操作:
// 获取id属性的属性名: |
Gitalking ...