超级面板
文章目录
最新文章
最近更新
文章分类
标签列表
文章归档

Javascript 原生创建和修改 DOM 节点的方法

本文总结了常用的 js 进行 DOM 修改的操作,主要总结节点的创建、修改和元素属性的管理的一些相关的操作。

节点创建

常用的和节点创建有关的接口主要有:document.createElementdocument.createTextNodedocument.createDocumentFragmentdocument.createAttributedocument.adoptNodedocument.importNodenode.cloneNode

document.createElement

document.createElement(tagName) 这是我们最常用的一个方法了,创建由标签名称(tagName)指定的 HTML 元素,如果标签名称不是一个有效的 HTML 元素,不会报错,会创建一个 HTMLUnknownElement对象

HTML 对大小写不敏感,所以使用大写或者小写的标签名称都可以,但是注意,标签中不能包含尖括号 <> ,否则会报错。

var newDiv = document.createElement('div');
// or
var newDiv = document.createElement('DIV');

document.createElement('<div>');
// Uncaught DOMException: Failed to execute 'createElement' on 'Document': The tag name provided ('<div>') is not a valid name.

注意,通过 createElement 创建的元素,并未添加到 HTML 文档中,需要调用 appendChild 等节点修改方法将其添加到 HTML 文档树中。后几节创建的其他类型的节点也是如此,后文不再对此进行说明。

document.createTextNode

document.createTextNode(text) 创建一个文本节点,参数(text)为文本节点的内容。

这个方法返回的节点,被浏览器当作文本渲染,而不是当作 HTML 代码渲染,因此会对 HTML 代码进行转义,可以用来展示用户的输入,避免 XSS 攻击:

function escapeUserInput(str) {
var div = document.createElement('div');
div.appendChild(document.createTextNode(str));
return div.innerHTML;
}

var userInput = '<p>危险内容</p>';
var template = '<div>' + escapeUserInput(userInput) + '</div>'

// 此时被转义,危险内容不再危险
<div>&lt;p&gt;危险内容&lt;/p&gt;</div>

但是,该方法不对单引号和双引号转义,因此用来为属性赋值的时候,仍然会被 XSS 攻击:


var userInput = '" onmouseover="console.log(\'危险操作\')" "';
var template = '<div color="' + escapeUserInput(userInput) + '">user set color</div>'

// 被注入一个 onmouseover 操作
<div color="" onmouseover="console.log('危险操作')" "">user set color</div>

document.createDocumentFragment

document.createDocumentFragment() 方法创建一个新的 DocumentFragment 对象。

DocumentFragments 存在于内存中,并不在 DOM 树存在,将元素插入到文档片段也不会引起回流。因此创建文档片段,并将复杂的 DOM 结构附加到文档片段中,然后将文档片段附加到 DOM 树,可以优化性能。

var ul = document.getElementById('list');
var domfrag = document.createDocumentFragment();

for (var i = 0; i < 100; i ++) {
var li = document.createElement('li');
li.textContent = i;
domfrag.appendChild(li);
}

ul.appendChild(domfrag);

document.createAttribute

document.createAttribute(attrName) 方法创建并返回一个新的属性节点。这个方法不是很常用用,因为添加属性通常用 node.setAttribute 即可:

var node = document.getElementById('div');

var attr = document.createAttribute('attr');
attr.nodeValue = 'value';
node.setAttributeNode(attr);
// 等价于
node.setAttribute('attr', 'value');

document.adoptNode

document.adoptNode(externalNode) 从其他的 document 中获取一个节点(externalNode),并将该节点以及它的所有子节点从原文档删除, 并且它的 ownerDocument 属性会变成当前的 document。之后你可以把这个节点插入到当前文档中,不常用,了解即可。

// 该函数用来从本文档的第一个 iframe 中获取第一个 element 元素,
// 并插入到当前文档树中
function getEle(){
var iframe = document.getElementsByTagName("iframe")[0],
ele = iframe.contentWindow.document.body.firstElementChild;
if(ele){
document.body.appendChild(document.adoptNode(ele))
}else{
alert("没有更多元素了")
}
}
document.getElementById("move").onclick = getEle

注意,该方法在同一 document 下的不同两个元素中也可以使用,可以实现从左边栏列表中选取某些元素加载到右边栏的功能。

注意,如果节点资源来自不同的源的时候,调用 adoptNode 可能会失败。

有些情况下,将外部文档的节点插入当前文档之前,你需要使用 document.importNode() 从外部文档导入源节点,了解更多细节

document.importNode

document.importNode(externalNode, deep) 这个接口也不常用,作用是拷贝外部文档的一个节点(externalNode)。deep 表明是否要导入节点的后代节点,默认为 false 不导入后代节点。

var iframe = document.getElementsByTagName("iframe")[0];
var oldNode = iframe.contentDocument.getElementById("myNode");
var newNode = document.importNode(oldNode, true);
document.getElementById("container").appendChild(newNode);

注意,这个方法仅拷贝节点,此时,节点存在于内存中,还需要插入当前文档中才能显示。

node.cloneNode

node.cloneNode(deep) 方法返回该节点的一个副本,deep 可选,表明是否采用深度克隆,如果为 true,则该节点的所有后代节点也都会被克隆,否则,只克隆该节点本身。

注意,克隆节点会克隆节点的所有属性,但是绑定的事件呢?我们来看一下:

首先看一下使用内联方式直接写在 HTML 标签上的 onevent 事件:

<div id="old">
<p id="clone" onclick="console.log('click clone')">clone me.</p>
</div>
<div id="new"></div>

// cloneNode
var p = document.getElementById('clone');
var newDiv = document.getElementById('new');
newDiv.appendChild(p.cloneNode(true));

// test
// 点击 #old > #clone
'click clone'
// 点击 #new > #clone
'click clone'
// on-event 属性绑定的事件保留

结果显示,使用内联方式写在 HTML 上的事件是可以的,接下来看一下绑定在节点对象上的 on-event 事件:

<div id="old">
<p id="clone">clone me.</p>
</div>
<div id="new"></div>

// cloneNode
var p = document.getElementById('clone');
var newDiv = document.getElementById('new');
p.onclick = function() {
console.log('click clone')
};
newDiv.appendChild(p.cloneNode(true));

// test
// 点击 #old > #clone
'click clone'
// 点击 #new > #clone
// do nothing
// 绑定在对象上的 on-event 事件没有克隆

从测试结果来看,使用 js 绑定在节点对象上的 on-event 事件在克隆的副本并不包含事件处理程序。

接下来再看一下使用 addEventListener 方法添加在这个节点上的事件监听函数:

<div id="old">
<p id="clone" class="clone">clone me.</p>
</div>
<div id="new"></div>

// cloneNode
var p = document.getElementById('move');
p.addEventListener('click', function() {
console.log('click clone');
})
var newDiv = document.getElementById('new');
newDiv.appendChild(p.cloneNode(true));

// test
// 点击 #old > #clone
'click clone'
// 点击 #new > #clone
// do nothing
// addEventListener 绑定在对象上的事件没有克隆

也就是说,副本节点只能绑定使用内联方式绑定的事件处理函数。

注意,这个拷贝的节点并不在文档中,需要自行添加到文档中。同时拷贝的节点有可能会导致节点的的 id 属性重复,最好修改新节点的 id,而 name 属性也可能重复,自行决定是否需要修改。

节点修改

和节点内容修改有关的接口主要有 node.appendChildnode.insertBeforenode.removeChildnode.replaceChild 这四个接口。

node.appendChild

parentNode.appendChild(child) 方法将一个节点(child)添加到指定父节点(parentNode)的子节点列表的末尾。本方法返回值为要插入的这个节点。

// 创建一个新的段落p元素,然后添加到body的最尾部
var p = document.createElement('p');
document.body.appendChild(p);

// p 节点为 body 元素的最后一个子节点

注意,如果被插入的节点已经存在文档树中,则节点会被从原先的位置移除,并插入到新的位置,当然,被移动元素被绑定的事件也会被同步过去:

// html
<div id="old"><p id="move"></p></div>
<div id="new"></div>

var p = document.getElementById('move');
var newDiv = document.getElementById('new');
newDiv.appendChild(p);

// new html
<div id="old"></div>
<div id="new"><p id="move">move me.</p></div>

如果要保留原来的这个子节点的位置,则可以用 Node.cloneNode 方法复制出一个节点的副本,然后再插入到新位置:

// html
<div id="old"><p id="move"></p></div>
<div id="new"></div>

var p = document.getElementById('move');
var newDiv = document.getElementById('new');
newDiv.appendChild(p.cloneNode(true));

// new html
<div id="old"><p id="move">move me.</p></div>
<div id="new"><p id="move">move me.</p></div>

这个方法只能将某个子节点插入到同一个文档的其他位置,如果你想跨文档插入,需要先调用 document.importNode 方法。

还有,如果 appendChild 方法的参数是 DocumentFragment 节点,那么插入的是 DocumentFragment 的所有子节点,而不是 DocumentFragment 节点本身。此时,返回值是一个空的 DocumentFragment 节点。

node.insertBefore

parentNode.insertBefore(child, referenceNode) 方法将一个节点(child)插入作为父节点(parentNode)的一个子节点,并且位置在参考节点(referenceNode)之前。

如果第二个参数 referenceNode 为 null,则插入位置为父节点的末尾:

parentNode.insertBefore(node, null);
// 等价于
parentNode.appendChild(node);

注意,第二个参数为 null 时不能省略,否则会抛出异常:

Uncaught TypeError: Failed to execute 'insertBefore' on 'Node': 2 arguments required, but only 1 present.

使用这个方法可以模拟 prependChild,产生类似于 appendChild ,但是将节点插入作为指定父节点的第一个子节点:

Node.prototype.prependChild = function(node) {
return this.insertBefore(node, this.firstChild);
}

// html
<div id="div"><p>原来的第一个子节点</p></div>

var div = document.getElementById('div');
var p = document.createElement('p');
p.innerText = '第一个子节点';
div.prependChild(p);

// new html
<div id="div"><p>第一个子节点</p><p>原来的第一个子节点</p></div>

使用这个方法还可以模拟 insertAfter,将节点要插在父节点的某个子节点后面:

Node.prototype.insertAfter = function(node, referenceNode) {
return this.insertBefore(node, referenceNode.nextSibling);
}

appendChild 类似,如果插入的节点是文档中已经存在的节点,则会移动该节点到指定位置,并且保留其绑定的事件。

node.removeChild

parentNode.removeChild(child) 删除指定父节点(parentNode)的一个子节点(child),并返回被删除的节点。

注意,这个方法是要在被删除的节点的父节点上调用的,而不是在被删除节点上调用的,如果参数节点不是当前节点的子节点,removeChild 方法将报错:

// 通过 parentNode 属性直接删除自身
var node = document.getElementById('deleteDiv');
if (node.parentNode) {
node.parentNode.removeChild(node);
}

// 也可以封装以下作为一个方法直接使用:
Node.prototype.remove = function(node) {
if (node.parentNode) {
return node.parentNode.removeChild(node);
}
throw new Error('Can not delete.');
}

node.remove();

使用这个方法也可以很简单的模拟 removeAllChild

Node.prototype.removeAllChild = function() {
var deleteNode = []
while (this.firstChild) {
deleteNode.push(this.removeChild(this.firstChild));
}
return deleteNode;
}

被移除的这个子节点仍然存在于内存中,只是不在当前文档的 DOM 中,仍然还可以被添加回文档中。但是如果不使用一个变量保存这个节点的引用,被删除的节点将不可达,会在某次垃圾回收被清除。

node.replaceChild

parentNode.replaceChild(newChild, oldChild) 方法用指定的节点(newChild)替换当前节点(parentNode)的一个子节点(oldChild),并返回被替换的节点(oldChild)。

<div>
<span id="childSpan">foo bar</span>
</div>

// 创建一个空的span元素节点
// 没有id,没有任何属性和内容
var sp1 = document.createElement("span");

// 添加一个id属性,值为'newSpan',并添加文本内容
sp1.setAttribute("id", "newSpan");
sp1.innerText = "新的span元素的内容.";

// 获得被替换节点和其父节点的引用.
var sp2 = document.getElementById("childSpan");
var parentDiv = sp2.parentNode;

// 用新的span元素sp1来替换掉sp2
parentDiv.replaceChild(sp1, sp2);

// 结果:
<div>
<span id="newSpan">新的span元素的内容.</span>
</div>

元素修改

元素是节点的一种,除了拥有上一节节点修改的一些方法外,元素还继承了 ParentNodeChildNode 两个接口,因此拥有一些额外的修改方法:继承自 ParentNodeParentNode.appendParentNode.prepend 和继承自 ChildNodeChildNode.beforeChildNode.afterChildNode.removeChildNode.replaceWith 这几个方法。

但是要注意的是,ParentNodeChildNode 这两个都是原始接口,只能在实现了它的对象上使用。如果当前节点是父节点,就会继承 ParentNode 接口。由于只有元素节点(Element)、文档节点(Document)和文档片段节点(DocumentFragment)拥有子节点,因此只有这三类节点会继承 ParentNode 接口。如果一个节点有父节点,那么该节点就继承了 ChildNode 接口。

还要注意的是,这些方法目前都是实验性方法,浏览器的支持度还不太够,未来也可能发生变动,请谨慎使用。

append 和 ParentNode.prepend

parentNode.append((Node/String) ...nodes) 方法为指定节点的最后一个子节点之后插入一组节点。

node.appendChild 方法类似,不同点在于:

  1. append 方法允许添加 string 做为参数,并将其包装为文本节点,而 appendChild 方法只能接受 Node 对象作为参数。
  2. append 方法允许添加多个参数,而 appendChild 方法只能接受一个节点。
  3. append 方法没有返回值,而 appendChild 方法返回被添加的节点。

parentNode.prepend(...[Node/String]) 方法为指定节点的第一个子节点之后插入一组节点,其他内容和 append 方法类似,不再说明。

// old html
<div id="div"></div>

var div = document.getElementById('div');
var p = document.createElement("p");
var span = document.createElement("span");
div.append(p);
div.append(" End!");
div.prepend("Begin:");
div.prepend(span);

// new html
<div id="div"><span></span>Begin:<p></p> End!</div>

使用 appendChildinsertBefore 可以很容易实现如下的 Polyfill:

function getDocFrag() {
var argArr = Array.prototype.slice.call(arguments),
docFrag = document.createDocumentFragment();

argArr.forEach(function (argItem) {
var isNode = argItem instanceof Node;
docFrag.appendChild(isNode ? argItem : document.createTextNode(String(argItem)));
});
return docFrag
}

(function (arr) {
arr.forEach(function (item) {
item.append = item.append || function () {
this.appendChild(getDocFrag(arguments));
};
item.prepend = item.prepend || function () {
this.insertBefore(docFrag, this.firstChild);
};
});
})([Element.prototype, Document.prototype, DocumentFragment.prototype]);

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) {
arr.forEach(function (item) {
item.before || function() {
this.parentNode.insertBefore(getDocFrag(arguments), this);
}
item.after || function() {
this.parentNode.insertBefore(getDocFrag(arguments), this.nextSibling);
}
});
})([Element.prototype, CharacterData.prototype, DocumentType.prototype]);

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)
  • text: 要插入的文本或者html字符串。
// origin html
<div id="outer">
<div></div>
</div>

const outer = document.getElementById('outer');

outer.insertAdjacentText('beforebegin', 'beforebegin 被插入这里');
outer.insertAdjacentText('afterbegin', 'afterbegin 被插入这里');
outer.insertAdjacentText('beforeend', 'beforeend 被插入这里');
outer.insertAdjacentText('afterend', 'afterend 被插入这里');

// after inserted.
beforebegin 被插入这里
<div id="outer">
beforebegin 被插入这里
<div></div>
beforebegin 被插入这里
</div>
beforebegin 被插入这里

两个方法的区别在于 insertAdjacentHTML 方法会将字符串解析为 html,而 insertAdjacentText 不会做解析,因此在不需要解析的情况下(如添加纯文本)使用 insertAdjacentText 性能会更好。

ChildNode.remove

childNode.remove() 方法把它从它所属的 DOM 树中删除,没有返回值,所以在删除前,需要先保留引用:

// old html
<div><div id="div"></div></div>

var div = document.getElementById('div')
div.remove();

// new html
<div></div>

使用 removeChild 可以很容易实现如下的 Polyfill:

(function (arr) {
arr.forEach(function (item) {
item.remove = item.remove || function() {
this.parentNode.removeChild(this);
}
});
})([Element.prototype, CharacterData.prototype, DocumentType.prototype]);

ChildNode.replaceWith

childNode.replaceWith((Node/String) ...nodes) 用指定节点(nodes),替换当前节点(childNode)。

使用 replaceChild 可以很容易实现如下的 Polyfill:

(function (arr) {
arr.forEach(function (item) {
item.replaceWith = item.replaceWith || function() {
this.parentNode.replaceChild(getDocFrag(arguments), this);
}
});
})([Element.prototype, CharacterData.prototype, DocumentType.prototype]);

元素属性操作

这些操作仅针对节点类型为 Element 的节点,即元素。

读取属性

读取属性可以使用 element.getAttribute(attrName)。如果指定的属性不存在,则返回 null 。(旧版本的浏览器可能会返回空字符串""

修改属性

修改属性可以使用 element.setAttribute(attrName, attrValue)。 如果此属性已经存在,则更新该值, 否则,将添加一个新的属性用指定的名称和值。

删除属性

删除属性可以使用 element.removeAttribute(attrName)。尝试删除不存在的属性并不会引发异常。

注意,当不需要某个属性的时候,优先使用 removeAttribute,而非使用 setAttribute 将属性值设置为 null。

检查属性是否存在

检查属性可以使用 element.hasAttribute(attrName)

修改元素节点对象的属性值

当然,可能你已经见过这种操作元素属性的方法:

// 修改属性
var form = document.getElementById('form');
form.action = '/submit';
form.method = 'POST';

// 读取属性
console.log(p.name)

这种方法通过修改元素节点对象的属性值来修改元素的属性,但是有一定的局限性:

1.有些 HTML 的属性名是 Javascript 的保留字,比如 forclass,所以使用的时候必须更改:

ele.htmlFor = "someValue"; 
ele.className = "someValue";
// 等价于
ele.setAttribute('for', 'someValue')
ele.setAttribute('class', 'someValue')

2.getAttribute 获取的 HTML 属性值都是字符串,而这种方法有时会对某些属性的值进行类型转换,比如,将字符串的 true/false 转为布尔类型,将 onClick 的值转为函数类型。

3.这种方法无法删除属性,即使用 delete 运算符不生效(自定义属性 data-[attr-name]* 是可以删除的)。

4.这种方法对非 HTML 支持的标准属性不生效,尽管这种非标准属性是不建议使用的:

// html
<div id="test" test="true"></div>

// 读取非标准属性
var div = document.getElementById('test');
console.log(div.test); // undefined
console.log(div.getAttribute('test')); // "true"

// 修改非标准属性
div.test = "false";
console.log(div.getAttribute('test')); // "true"

标准的自定义属性 data-[attr-name] 则使用 ele.dataset.attrName

5.元素的属性名大小写不敏感,而 JavaScript 对象的属性名大小写敏感的,并且当属性名包括多个单词,会对属性名进行驼峰拼写法转换:

<button onclick="doSthing"></button>

button.onClick = doSthing;

优先使用上面提到的四个属性操作的方法进行属性的修改。

获取元素的全部属性

Element.attributes 属性返回该元素所有属性节点的一个实时集合

实时 指的是元素属性的任意变化都会反映在这个属性上:

// html
<div id="test" name="test1"></div>

var div = document.getElementById('test');
var attrs = div.attributes;

console.log(attrs[1].value); // test1
div.setAttribute('name', 'test2');
console.log(attrs[1].value); // test2

集合是一个类数组结构,大概是这个样子的:

// html
<div id="test" name="test1"></div>
console.log(attrs);

{
0: Attr(id),
1: Attr(name),
length: 2,
id: Attr(id),
name: Attr(name),
}

集合的常用操作:

// 获取id属性的属性名:
attrs.id.name
attrs.id.nodeName
attrs[0].name
attrs[0].nodeName

// 获取id属性的属性值:
attrs.id.value
attrs[0].value

Gitalking ...