文章简介
DOM 是 JavaScript 操作网页的接口,全称为文档对象模型(Document Object Model)。DOM 操作是 JavaScript 最常见的任务,离开了 DOM,JavaScript 就无法操作网页。
本篇文章为【JavaScript 漫游】专栏的第 020 篇文章,对 DOM 规范的常用知识点进行了总结。
由于 DOM 规范的知识内容比较庞大,有不少知识点在日常开发工作中用得非常少,哪怕是在找工作的笔试题和面试题中都很少出现,所以这篇博客就只记录常用的知识点。
文章的内容顺序如下:
- DOM 概述,介绍 DOM、节点和节点树三者的基本概念,从而对 DOM 规范有初步的了解
- 接口总结,DOM 规范中定义了 Node、NodeList、HTMLCollection、ParentNode、ChildNode 这五大接口,这些接口提供的属性和方法有助于 JavaScript 操作 DOM 树,这里介绍每个接口的基本概念、常用属性和常用方法
- 节点总结,DOM 规范中还定义了 Document、Element、Text 和 DocumentFragment 这四种节点,它们提供的属性和方法同样有利于 JavaScript 操作 DOM 树,这里介绍 Document 和 Element 节点的基本概念、常用属性和常用方法(Text 和 DocumentFragment 节点不常用)。
- 属性操作总结,介绍 JavaScript 如何操作元素节点的属性。
- CSS操作简述,简单介绍 JavaScript 如何操作 CSS 的基本方法。
- Mutation Observer API 基本介绍,该 API 用于监视 DOM 的变动
DOM 概述
DOM 介绍
DOM 是 JavaScript 操作网页的接口,全称为文档对象模型(Document Object Model)。它的作用是将网页转为一个 JavaScript 对象,从而可以用脚本进行各种操作(比如增删查改)。
DOM 是一种接口规范,基于规范定义的 DOM 模型,能将结构化文档(比如 HTML)解析成一系列的节点,再由节点组成节点树(称为 DOMTree)。所有的节点和最终的树状结构,都有规范的对外接口。任何一种编程语言,只要它实现了这种接口规范,它就具备能够对结构化文档的内容进行增删查改等操作的能力。JavaScript 正是实现了 DOM 规范,从而具备了在浏览器上操作 HTML 文档内容的能力。
节点和节点树
DOM 的最小组成单位叫做节点(node)。各种不同类型的节点,可以组成节点树。
节点的类型有七种。
Document
:整个文档树的顶层节点DocumentType
:doctype
标签Element
:网页的各种 HTML 标签(比如<body>
)Attr
:网页元素的属性(比如class="right"
)Text
:标签之间或标签包含的文本Comment
:注释DocumentFragment
:文档的片段
这些节点都具备公共或独有的属性和方法,以供 JavaScript 使用。
DOM 树的存在,主要是提供一种上下文,JavaScript 可以先拿到某一个节点,再去找它的祖父节点、兄弟节点和子孙节点等,这样就可以对整个文档的所有节点都进行操作。
DOM 接口总结
Node 接口
所有 DOM 节点对象都继承了 Node 接口,拥有一些共同的属性和方法。这是 DOM 操作的基础。
它的实例属性中常用的有:
textContent
baseURI
previousSibling
parentNode
parentElement
firstChild
和lastChild
childNodes
常用的实例方法有:
appendChild()
insertBefore()
removeChild()
replaceChild()
textContent
需要获取当前节点和它的所有后代节点的文本内容时,就用这个属性。它会自动忽略当前节点内部的 HTML 标签,返回所有文本内容。
// HTML 代码为
// <div id="divA">This is <span>some</span> text</div>
document.getElementById('divA').textContent;
// This is some text
这个属性是可读写的,通过设置属性的值,新的文本节点就会替换原来的所有子节点。它还有一个用处,就是用于对 HTML 标签转义。
document.getElementById('foo').textContent = '<p>GoodBye!</p>';
上面代码在插入文本时,会将 <p>
标签解释为文本,而不会当作标签去处理。
baseURI
要获取当前网页的绝对路径时,就用这个属性。记住它是只读的。
// 当前网页的网址为
// http://www.example.com/index.html
document.baseURI
// "http://www.example.com/index.html"
如果读不到,就返回 null
。
属性值一般是当前网址的 URL 决定,但是可以使用 HTML 的 <base>
标签来改变它的值。
// <base href="http://www.example.com/page.html">
document.baseURI
// "http://www.example.com/page.html"
nextSibling
和 previousSibling
通过前者可以获取当前节点毗邻的下一个兄弟节点,后者则可以获取到当前节点毗邻的前一个兄弟节点。
要注意的是,属性值包括了文本节点和注释节点,如果当前节点前后有空格,那么它们的属性值就是空格对应的文本节点。
// HTML 代码如下
// <div id="d1">hello</div><div id="d2">world</div>
var d1 = document.getElementById('d1');
var d2 = document.getElementById('d2');
d1.nextSibling === d2 // true
d2.previousSibling === d1 // true
parentNode
和 parentElement
前者拿到父节点,后者拿到父元素,区别就在于拿到的父节点可以为三种节点类型:元素节点(element
)、文档节点(document
)和文档片段节点(documentfragment
)。后者相当于把文档节点和文档片段节点都排除掉了。
if (node.parentNode) {
node.parentNode.removeChild(node);
}
if (node.parentElement) {
node.parentElement.style.color = 'red';
}
firstChild
和 lastChild
前者获取第一个子节点,后者获取最后一个子节点。
// HTML 代码如下
// <p id="p1"><span>First span</span></p>
var p1 = document.getElementById('p1');
p1.firstChild.nodeName // "SPAN"
childNodes
要获取当前节点的所有子节点时,就用这个属性。它会返回一个类数组对象,NodeList
集合,成员包含当前节点的所有子节点。
var div = document.getElementById('div1');
var children = div.childNodes;
for (var i = 0; i < children.length; i++) {
// ...
}
注意 NodeList
对象是一个动态集合,一旦子节点发生变化,立刻会反映在返回结果之中。
appendChild()
给当前节点增加一个子节点,位置在末尾。参数是一个节点对象,返回值也是它。
var p = document.createElement('p');
document.body.appendChild(p);
如果参数节点是 DOM 中已经存在的节点,方法会将其从原来的位置,移动到新位置。
var div = document.getElementById('myDiv');
document.body.appendChild(div);
还有,如果参数是 DocumentFragment
节点,那么插入的是 DocumentFragment
的所有子节点,而不是 DocumentFragment
节点本身。返回值是一个空的 DocumentFragment
节点。
insertBefore()
需要将某个节点插入到父节点内部的指定位置时,就需要用到这个方法。语法如下:
var insertedNode = parentNode.insertBefore(newNode, referenceNode);
newNode
:要插入的节点referenceNode
:父节点内部的一个子节点
插入的节点就被插入到该内部子节点的前面。
如果要插入的节点是当前 DOM 现有的节点,则该节点将从原有的位置移除,插入新的位置。
接口规范中不存在
insertAfter()
,要实现类似的功能,可以将insertBefore()
和nextSibling
结合使用
removeChild()
需要移除子节点时,就用这个方法。
var divA = document.getElementById('A');
divA.parentNode.removeChild(divA);
replaceChild()
需要用新节点,替换某个子节点时,就用这个方法。语法如下:
var replacedNode = parentNode.replaceChild(newChild, oldChild);
newChild
:新的子节点oldChild
:要替换的子节点
var divA = document.getElementById('divA');
var newSpan = document.createElement('span');
newSpan.textContent = 'Hello World!';
divA.parentNode.replaceChild(newSpan, divA);
NodeList 接口和 HTMLCollection 接口
节点都是单个对象,有时需要一种数据结构,能够容纳多个节点。DOM 提供两种节点集合,用于容纳多个节点:NodeList
和 HTMLCollection
。它们的主要区别是,前者包含了元素节点、文本节点和注释节点,而后者只包含元素节点。
许多 DOM 属性和方法,返回的结果是 NodeList
实例或 HTMLCollection
实例。通过以下方法可以获得 NodeList
实例。
Node.childNodes
document.querySelectorAll()
等节点搜索方法
而返回 HTMLCollection
实例的,主要是一些 Document
对象的集合属性。比如 document.links
、document.forms
、document.images
等。
NodeList 接口概述
NodeList
实例很像数组,可以使用 length
属性和 forEach
方法,但它不是数组,不能使用 pop
或 push
等数组特有的方法。
var children = document.body.childNodes;
Array.isArray(children); // false
children.length; // 34
children.forEach(console.log);
如果 NodeList
实例要使用数组方法,可以将其转为真正的数组。
var children = document.body.childNodes;
var nodeArr = Array.prototype.slice.call(children);
除了使用 forEach
方法遍历 NodeList 实例,还可以使用 for
循环。。
var children = document.body.childNodes;
for (var i = 0; i < children.length; i++) {
var item = children[i];
}
要注意的是,NodeList 实例可能是动态集合,也可能是静态集合。所谓动态集合就是一个活的集合,DOM 删除或新增一个相关节点,都会立刻反映在 NodeList 实例。目前,只有 Node.childNodes
返回的是一个动态集合,其他的 NodeList 都是静态集合。
var children = document.body.childNodes;
children.length // 18
document.body.appendChild(document.createElement('p'));
children.length // 19
NodeList.prototype.forEach()
用法与数组实例的 forEach()
完全一致。
var children = document.body.childNodes;
children.forEach(function f(item, i, list) {
// ...
}, this);
item
,当前成员i
,位置list
,当前 NodeList 实例
NodeList.prototype.keys()
、NodeList.prototype.values()
和 NodeList.prototype.entries()
实例方法
这三个方法都返回一个 ES6 的遍历器对象,可以通过 for...of
循环遍历获取每一个成员的信息。区别在于,keys()
返回键名的遍历器,values()
返回键值的遍历器,entries()
返回的遍历器同时包含键名和键值的信息。
var children = document.body.childNodes;
for (var key of children.keys()) {
console.log(key);
}
// 0
// 1
// 2
// ...
for (var value of children.values()) {
console.log(value);
}
// #text
// <script>
// ...
for (var entry of children.entries()) {
console.log(entry);
}
// Array [ 0, #text ]
// Array [ 1, <script> ]
// ...
HTMLCollection
接口概述
HTMLCollection
是一个节点对象的集合,只能包含元素节点。它的返回值是一个类似数组的对象,但是与 NodeList
接口不同,HTMLCollection
没有 forEach()
,只能使用 for
循环遍历。并且此接口的实例都是动态集合,节点的变化会实时反映在集合中。
// HTML 代码如下
// <img id="pic" src="http://example.com/foo.jpg">
var pic = document.getElementById('pic');
document.images.pic === pic // true
HTMLCollection.prototype.length
返回 HTMLCollection
实例包含的成员数量。
HTMLCollection 实例方法都不常用,所以这里就不记录
ParentNode 接口和 ChildNode 接口
节点对象除了继承 Node 接口以外,还拥有其他接口。ParentNode
接口表示当前节点是一个父节点,提供一些处理子节点的方法。ChildNode
接口表示当前节点是一个子节点,提供一些相关方法。
ParentNoode
接口简述
如果当前节点是父节点,就会混入了(mixin)ParentNode
接口。由于只有元素节点(element)、文档节点(document)和文档片段节点(documentFragment)拥有子节点,所以也只有这三类接口会拥有 ParentNode
接口。
ParentNode.children
返回 HTMLCollection
实例。
ParentNode.firstElementChild
和 ParentNode.lastElementChild
分别返回当前节点的第一个元素子节点和最后一个元素子节点。
ParentNode.append()
和 ParentNode.prepend()
两者都为当前节点追加一个或多个子节点,区别在于前者追加在末尾,后者追加在前面。
var parent = document.body;
// 添加元素子节点
var p = document.createElement('p');
parent.append(p);
// 添加文本子节点
parent.append('Hello');
// 添加多个元素子节点
var p1 = document.createElement('p');
var p2 = document.createElement('p');
parent.append(p1, p2);
// 添加元素子节点和文本子节点
var p = document.createElement('p');
parent.append('Hello', p);
ChildNode.remove()
用于从父节点移除当前节点。
ChildNode.before()
和 ChildNode.after()
分别用于在当前节点的前面和后面插入一个或多个同级节点。
var p = document.createElement('p');
var p1 = document.createElement('p');
// 插入元素节点
el.before(p);
// 插入文本节点
el.before('Hello');
// 插入多个元素节点
el.before(p, p1);
// 插入元素节点和文本节点
el.before(p, 'Hello');
ChildNode.replaceWith()
使用参数节点替换当前节点。
var span = document.createElement('span');
el.replaceWith(span);
DOM 节点总结
Document 节点
document
节点对象代表整个文档,每张网页都有自己的 document
对象。只要浏览器开始载入 HTML 文档,该对象就存在了,可以直接使用。
document
对象有不同的方法可以获取。
- 正常的网页,直接使用
document
或window.document
iframe
框架里面的网页,使用iframe
节点的contentDocument
属性- Ajax 操作返回的文档,使用
XMLHttpRequest
对象的responseXML
属性 - 内部节点的
ownerDocument
属性
document
对象继承了 EventTarget
接口和 Node
接口,并且混入(mixin)了 ParentNode
接口。这意味着,这些接口的方法都可以在 document
对象上调用。除此之外,document
对象还有很多自己的属性和方法。
这里要介绍的常用属性有如下:
document.documentURI
和document.URL
document.domain
document.location
document.lastModified
document.cookie
常用方法有如下:
document.querySelector()
和document.querySelectorAll()
document.getElementsByTagName()
document.getElementsByClassName()
document.getElementsByName()
document.getElementById()
document.createElement()
document.createAttribute()
document.createEvent()
document.addEventListener()
、document.removeEventListener()
和document.dispatchEvent()
documentURI
和 URL
它们都返回一个字符串,表示当前文档的网址。不同之处是它们继承自不同的接口,documentURI
继承自 Document
接口,可用于所有文档;URL
继承自 HTMLDocument
接口,只能用于 HTML 文档。
document.URL
// http://www.example.com/about
document.documentURI === document.URL
// true
domain
获取当前文档的域名,不包含协议和端口。
location
获取浏览器提供的原生对象 Location,提供 URL 相关的信息和操作方法。
lastModified
获取当前文档的最后修改时间。
cookie
用来操作浏览器 Cookie。
querySelector()
和 querySelectorAll()
它们都接受一个 CSS 选择器作为参数,前者返回匹配该选择器的元素节点,后者返回包含所有匹配该选择器的节点的 NodeList
对象。
var el1 = document.querySelector('.myclass');
var el2 = document.querySelector('#myParent > [ng-click]');
var matches = document.querySelectorAll('div.note, div.alert');
注意,它们不支持 CSS 伪元素选择器和伪类选择器。
getElementsByTagName()
通过标签名来获取相应的元素集合,返回值是 HTMLCollection
实例。
var paras = document.getElementsByTagName('p');
paras instanceof HTMLCollection // true
getElementsByClassName()
通过 class 名来获取相应的元素集合,返回值是 HTMLCollection
实例。
var elements = document.getElementsByClassName('foo bar');
getElementsByName()
获取拥有 name
属性的 HTML 元素,比如 <form>
、<radio>
、<img>
等,返回值是 NodeList
实例。
// 表单为 <form name="x"></form>
var forms = document.getElementsByName('x');
forms[0].tagName // "FORM"
getElementById()
获取匹配指定 id
的元素节点。
var elem = document.getElementById('para1');
createElement()
生成元素节点,并返回该节点。
var newDiv = document.createElement('div');
createAttribute()
生成属性节点,并返回该节点。
var node = document.getElementById('div1');
var a = document.createAttribute('my_attrib');
a.value = 'newVal';
node.setAttributeNode(a);
// 或者
node.setAttribute('my_attrib', 'newVal');
document.createEvent()
生成一个可以被 dispatchEvent()
调用并触发的事件对象。
var event = document.createEvent(type);
参数 type
是事件类型,比如 UIEvents
、MouseEvents
、MutationEvents
、HTMLEvents
。
var event = document.createEvent('Event');
event.initEvent('build', true, true);
document.addEventListener('build', function (e) {
console.log(e.type); // "build"
}, false);
document.dispatchEvent(event);
addEventListener()
、removeEventListener()
和 dispatchEvent()
这三个方法用于处理 document
节点的事件。它们都继承自 EventTarget
接口。
// 添加事件监听函数
document.addEventListener('click', listener, false);
// 移除事件监听函数
document.removeEventListener('click', listener, false);
// 触发事件
var event = new Event('click');
document.dispatchEvent(event);
Element 节点
Element
节点对象对应网页的 HTML 元素。每一个 HTML 元素,在 DOM 树上都会转化成一个 Element
节点对象。
Element
对象继承了 Node
接口,因此 Node
接口的属性和方法在 Element
对象上都存在。此外,它还有自己的属性和方法。
下面要介绍的常用属性有:
Element.draggable
Element.innerHTML
Element.scrollLeft
和Element.scrollTop
Element.children
Element.firstElementChild
和Element.lastElementChild
Element.nextElementSibling
和Element.previousElementSibling
常用方法有:
Element.querySelector()
和Element.querySelectorAll()
Element.getElementsByClassName()
Element.getElementsByTagName()
Element.addEventListener()
、Element.removeEventListener()
、Element.dispatchEvent()
Element.remove()
Element.focus()
和Element.blur()
Element.click()
其中与 Document
对象上同名的方法,它们的用法和含义都是一致的,所以后面就不作赘述。
draggable
返回布尔值,表示当前元素是否可拖拽,可读写。
innerHTML
获取元素包含的所有 HTML 代码,可读写。
scrollLeft
和 scrollTop
前者表示当前元素的水平滚动条向右侧滚动的像素数量,后者表示当前元素的垂直滚动条向下滚动的像素数量。对于那些没有滚动条的网页元素,这两个属性总是等于0。
如果要查看整张网页的水平的和垂直的滚动距离,要从 document.documentElement
元素上读取。
document.documentElement.scrollLeft;
document.documentElement.scrollTop;
这两个属性都可读写,设置该属性的值,会导致浏览器将当前元素自动滚动到相应的位置。
children
获取包含元素节点的所有子元素节点的 HTMLCollection
实例。
if (para.children.length) {
var children = para.children;
for (var i = 0; i < children.length; i++) {
// ...
}
}
firstElementChild
和 lastElementChild
分别获取第一个子元素和最后一个子元素。
nextElementSibling
和 previousElementSibling
分别获取后面一个毗邻的同级元素节点和前一个毗邻的同级元素节点。
remove()
将当前节点从它的父节点移除。
var el = document.getElementById('mydiv');
el.remove();
focus()
和 blur()
前者用于将当前页面的焦点,转移到指定元素上。
document.getElementById('my-span').focus();
后者用于将焦点从当前元素移除。
click()
用于在当前元素上模拟一次鼠标点击,相当于触发了 click
事件。
属性操作总结
属性本身是一个 Attr
对象,但是实际上,这个对象极少使用。一般都是通过元素节点对象(HTMLElement
对象)来操作属性。
这里介绍如何操作这些属性。
Element.attributes
元素对象有一个 attributes
属性,返回一个类似数组的动态对象,成员是该元素标签的所有属性节点对象,属性的实时变化都会反映在这个节点对象上。
单个属性可以通过序号引用,也可以通过属性名引用。
// HTML 代码如下
// <body bgcolor="yellow" οnlοad="">
document.body.attributes[0]
document.body.attributes.bgcolor
document.body.attributes['ONLOAD']
属性节点对象有 name
和 value
属性,对应属性名和属性值。
// HTML代码为
// <div id="mydiv">
var n = document.getElementById('mydiv');
n.attributes[0].name // "id"
n.attributes[0].nodeName // "id"
n.attributes[0].value // "mydiv"
n.attributes[0].nodeValue // "mydiv"
Element.getAttribute()
返回当前元素节点的指定属性。如果指定属性不存在,则返回 null
。
// HTML 代码为
// <div id="div1" align="left">
var div = document.getElementById('div1');
div.getAttribute('align') // "left"
Element.getAttributeNames()
返回包含当前元素的所有属性的名字的数组。它与 Element.attributes
的区别在于,后者返回的是类数组对象。
var mydiv = document.getElementById('mydiv');
mydiv.getAttributeNames().forEach(function (key) {
var value = mydiv.getAttribute(key);
console.log(key, value);
})
Element.setAttribute()
用于为当前节点新增属性。如果同名属性已存在,则相当于编辑已存在的属性。方法没有返回值。
// HTML 代码为
// <button>Hello World</button>
var b = document.querySelector('button');
b.setAttribute('name', 'myButton');
b.setAttribute('disabled', true);
Element.hasAttribute()
当前元素节点是否包含指定属性。
var d = document.getElementById('div1');
if (d.hasAttribute('align')) {
d.setAttribute('align', 'center');
}
Element.hasAttributes()
当前元素是否有属性,如果没有任何属性,就返回 false
,否则返回 true
。
var foo = document.getElementById('foo');
foo.hasAttributes() // true
Element.removeAttribute()
移除指定属性。方法没有返回值。
// HTML 代码为
// <div id="div1" align="left" width="200px">
document.getElementById('div1').removeAttribute('align');
// 现在的HTML代码为
// <div id="div1" width="200px">
CSS 操作简述
CSS 与 JavaScript 是两个有着明确分工的领域,前者负责页面的视觉效果,后者负责与用户的行为互动。但是,它们毕竟同属网页开发的前端,因此不可避免有着交叉和互相配合。
操作 CSS 样式最简单的方式,就是使用网页元素节点的 getAttribute()
、setAttribute()
和 removeAttribute()
,直接读写或删除网页元素的 style
属性。
div.setAttribute(
'style',
'background-color:red;' + 'border:1px solid black;'
);
style
不仅可以使用字符串读写,它本身还是一个对象,部署了 CSSStyleDeclaration 接口,可以直接读写个别属性。
e.style.fontSize = '18px';
e.style.color = 'black';
Mutation Observer API 基本介绍
Mutation Observer API 用来监视 DOM 变动。DOM 的任何变动,比如节点的增减、属性的变动、文本内容的变动,这个 API 都可以得到通知。
概念上,它很接近事件,可以理解为 DOM 发生变动就会触发 Mutation Observer 事件。但是,它与事件有一个本质不同:事件是同步触发,也就是说,DOM 的变动立刻会触发相应的事件;Mutation Observer 则是异步触发,DOM 的变动并不会马上触发,而是要等到当前所有 DOM 操作都结束才触发。
这样设计是为了应付 DOM 变动频繁的特点。举例来说,如果文档中连续插入1000个 <p>
元素,就会连续触发1000个插入事件,执行每个事件的回调函数,这很可能造成浏览器的卡顿;而 Mutation Observer 完全不同,只在1000个段落都插入结束后才会触发,而且只触发一次。
Mutation Observer 有以下特点。
- 它等待所有脚本任务完成后,才会运行(即异步触发方式)。
- 它把 DOM 变动记录封装成一个数组进行处理,而不是一条条个别处理 DOM 变动。
- 它既可以观察 DOM 的所有类型变动,也可以指定只观察某一类变动。