DOM基础
DOM 介绍
DOM 全程 Document Object Model
文档对象模型,在浏览器平台下,渲染进程解析 HTML 后产出的结果就是 DOM 树对象,在 JS 中我们可以通过 window.document
来访问这个对象。
从上图可以知道,在浏览器中,DOM、BOM 和 JS 中原生构造函数都是绑定在 window
对象上的,它有两个角色:
- 首先,它是 JS 里的全局对象,在 JS 全局执行上下文 中,通过
var
声明变量、function
定义的函数都会作为window
属性 - 其次,它代表 浏览器窗口,在
window
对象上绑定了许多操作当前浏览器窗口的方法和获取当前窗口信息的属性
important
DOM 不仅仅用于浏览器,DOM 规范解释了文档的结构,并提供了操作文档的对象。有的非浏览器设备也使用 DOM。例如,下载 HTML 文件并对其进行处理的服务器端脚本也可以使用 DOM。但它们可能仅支持部分规范中的内容。
DOM 树结构
上文提到在 JS 中可以通过 window.document
来获取到 DOM 树对象。
你需要知道,HTML 中所有的内容在被解析后都会保存到 DOM 树对象中,比如 <!DOCTYPE HTML>
还有注释都会以节点的形式保存在 window.document
中。
很多时候为了保证 HTML 结构的可读性,我们会自己对标签进行缩进、换行、加空格,但是需要注意的是这些内容会以 文本节点 的形式保存记录在 window.document
中。
比如 HTML 中有下面两个内容,但是它们的结构实际上是不一样的。
<div>1</div>
<div>2</div>>
<div>1</div><div>2</div>>
第一种结构相较于后一个结构来说,两个 div 之间多了一个文本节点(换行符)。
important
很多人认为 window.document
仅仅表示的是 <html>
标签中的内容,这是不正确的。
<!DOCTYPE HTML>
<html>
<head></head>
<body>
The truth about elk.
<ol>
<li>An elk is a smart</li>
<!-- comment -->
<li>...and cunning animal!</li>
</ol>
</body>
</html>
对于上面这个 HTML 结构,它的内容被解析后就会呈现下面这个 DOM 树结构。
图中的每一个节点的 key 指的是该节点对象的 nodeName
属性返回值。
你可以看见 ol
标签的第一个子节点不是 li
,而是一个文本节点。
important
两个 HTML 标签之间的空格、换行符会被识别为文本节点,不过有两个例外:
- 由于历史原因,
<head>
之前的空格和换行符均被忽略 - 在
</body>
标签之后放置的内容,会被自动移动到body
标签最后面
DOM 节点分类
在 DOM 树中,所有的节点类型都间接或者直接继承Node
类型,因此所有类型都共享相同的基本属性和方法。
important
上面这个图不是很正确,对于 Text
和 Comment
,它们是直接继承至 CharacterData
,而 CharacterData
才继承至 Node
。
Node
类型也是一个 "抽象"类,充当 DOM 节点的基础。它提供了树的核心功能:parentNode
,nextSibling
,childNodes
等(它们都是 getter)。Node
类的对象从未被创建。直接继承 Node
类型的节点一共有 12 种。
Node.ELEMENT_NODE = 1 // Element 元素类型节点,HTML 中的所有标签都是元素节,包括 <html> 标签
Node.ATTRIBUTE_NODE = 2 // Attribute 属性节点
Node.TEXT_NODE = 3 // Text 文本节点
Node.CDATA_SECTION_NODE = 4
Node.ENTITY_REFERENCE_NODE = 5
Node.ENTITY_NODE = 6
Node.PROCESSING_INSTRUCTION_NODE = 7
Node.COMMENT_NODE = 8 // Comment 注释节点
Node.DOCUMENT_NODE = 9 // 表示整个 HTML 文档对象这个节点,就是 document 的节点类型
Node.DOCUMENT_TYPE_NODE = 10 // 表示 <!DOCTYPE...> 这个节点
Node.DOCUMENT_FRAGMENT_NODE = 11
Node.NOTATION_NODE = 12
节点对象上的 nodeType
属性返回的就是上面的常量,它可以来确定某个节点对象的类型。
document.nodeType; // 9
document.docType.nodeType; // 10
DOM 节点常用属性
节点属性 | 说明 |
---|---|
Node.nodeType | 节点的类型,返回上面的节点类型常量 |
Node.nodeName | 适用于所有节点类型,返回该节点的标签名或者节点类型字符串 |
Element.tagName | 仅适用于 Element 节点,返回该节点的标签名 |
Element.innerHTML | 可以读写节点内部的 HTML 文本 |
Element.outerHTML | 可以读取当前节点整个 HTML 文本,并且可以替换当前节点 |
HTMLElement.innerText | 忽略该节点内所有非文本的节点,按顺序返回文本,也可以将该节点内部的 HTML 替换为指定文本 |
Node.textContent | 和 innerText 相似,可以读写节点内部文本内容 |
Node.nodeValue | 主要用于读写 Text 节点、 Comment 节点、Attribute 节点的内容 |
CharacterData.data | 主要用于读写 Text 节点或者 Comment 节点的内容 |
HTMLElement.hidden | 布尔类型,当设置元素节点的 hidden 属性为 true 时,等价于 dispaly: none |
Node.nodeType
返回节点类型常量,在上面节点的分类中可以查找对应数字表示的节点类型,不过也有更直观判断一个节点类型的办法:
document.getElementById('content').constructor.name; // HTMLLIElement
该属性为 只读属性。
Node.nodeName 与 Element.tagName
nodeName
适用于任意Node
节点类型tagName
仅适用于Element
节点类型
<body>
The truth about elk.
<ol>
<li>An elk is a smart</li>
<!-- comment -->
<li>...and cunning animal!</li>
</ol>
</body>
<script>
document.body.nodeName; // "BODY"
document.body.tagName; // "BODY"
document.doctype.nodeName; // "html",这里指的是 <!DOCTYPE html>
document.doctype.tagName; // undefined
document.nodeName; // "#document"
document.tagName; // undefined
document.body.firstChild; // "#text"
document.body.tagName; // undefined
</script>
可以做如下总结:
- 对于元素节点,
nodeName
和tagName
等价,均返回其大写形式的标签名字符串 - 对于非元素节点,只能通过
nodeName
获取其节点名,规则是"#" + node.constructor.name.toLocaleLowerCase()
这两个属性也为 只读属性。
Element.innerHTML 与 Element.outerHTML
继续以上面的 body 为例子:
// document.body.innerHTML
'\n \n The truth about elk.\n <ol>\n <li>An elk is a smart</li>\n <\!-- comment -->\n <li>...and cunning animal!</li>\n </ol>\n \n\n'
// document.body.outerHTML
'<body>\n \n The truth about elk.\n <ol>\n <li>An elk is a smart</li>\n <\!-- comment -->\n <li>...and cunning animal!</li>\n </ol>\n \n\n</body>'
innerHTML
返回的是该节点对象 内部 完整的 HTML 文本字符串 (包含换行符和空格)outerHTML
返回的是该节点对象完整的 HTML 文本字符串
使用 innerHTML 修改节点内部的结构
<body>
<ul>
<li>字节跳动</li>
<li>腾讯</li>
</ul>
</body>
<script>
document.body.firstElementChild.innerHTML += "<li>阿里巴巴</li>";
</script>
JS 脚本执行完毕后,就会变成如下 DOM 结构:
<body>
<ul>
<li>字节跳动</li>
<li>腾讯</li>
<li>阿里巴巴</li>
</ul>
</body>
需要注意的是,整个过程并不是在 <li>腾讯</li>
这个节点后加上了新节点,而是删除 ul
节点内部原来的所有节点,然后再替换上新的 innerHTML
。
important
如果 innerHTML
将一个 <script>
标签插入到 document 中,它会成为 HTML 的一部分,但是不会执行。
使用 outerHTML 替换整个节点
<body>
<div id="elem">Hello <b>World</b></div>
</body>
<script>
const div = document.getElementById('elem');
div.outerHTML = '<div>1</div><div>2</div>'
console.log(div.outerHTML); // '<div id="elem">Hello <b>World</b></div>',还是原值
</script>
通过 outerHTML
可以获取节点的整个 HTML 文本,当我们对 div.outerHTML
重写后,DOM 树的该节点会被新节点替换:
<body>
<div>1</div>
<div>2</div>
</body>
修改了 div.outerHTML
后整个过程是:
div
节点在 DOM 树中被删除- 新的 HTML 片段
<div>1</div><div>2</div>
插入到该节点位置
需要注意的是 div
节点对象虽然已经不在 DOM 树中,但是我们依旧引用着它,因为 outerHTML
表示的是该节点对象完整的 HTML 文本,修改节点对象的 outerHTML
仅会改变 DOM 树中的节点,所以 div
节点对象本身的 outerHTML
是不会改变的。
这里很容易出错,对于被修改了 outerHTML
的 div
节点对象,它已经不存在于 DOM 树中了,该节点所在的位置已经替换为了新的 HTML 片段,我们可以通过查询 DOM 来获取对新元素的引用。
总结如下:
- 修改一个节点对象的
outerHTML
属性,节点对象本身的outerHTML
并不会真正的被修改,只会使其在 DOM 树中被删除,然后在原来的位置上插入新的节点
Node.textContent
它的取值与节点类型相关:
document
与document.doctype
都返回 null- 对于
Comment
、Text
、Attribute
类型的节点,textContent
等价于Node.nodeValue
- 对于元素节点,
textContent
和innerText
一样,都会忽略标签名,只会返回文本节点的内容,但是它们之间还是有很大的区别
与 HTMLElement.innerText
的区别
<body>
<p id="source">
<style>
#source { color: red; }
#text { text-transform: uppercase; }
</style>
<span id=text>
Take a look at<br>how this text<br>is interpreted
below.
</span>
<span style="display:none">HIDDEN TEXT</span>
</p>
</body>
document.getElementById('source').textContent;
// '\n \n #source {\n color: red;\n }\n #text {\n text-transform: uppercase;\n }\n \n\n \n Take a look athow this textis interpreted below.\n \n\n HIDDEN TEXT\n '
document.getElementById('source').innerText;
// 'TAKE A LOOK AT\nHOW THIS TEXT\nIS INTERPRETED BELOW.'
textContent
会返回指定节点内部除了标签名外的所有文本,包括<style>
内部的样式文本和hidden
的节点内部文本,并且文本格式和 HTML 文件内的格式一致,换行符、空格都不会被修正innerText
会返回指定节点内部除了标签名外的所有元素节点内的文本,但是会忽略<style>
内部文本和hidden
节点内的文本,并且文本大小写会跟着样式来
与 Element.innerHTML
的区别
当我们修改一个元素的 innerHTML
时,字符串内容会被解析为 HTML,但是对于 textContent
,字符串取值是什么最终显示到页面上的就是什么。
attributes 与 properties
我们把 HTML 标签中的属性称为 attributes
,比如 <div id='nav'>
,这个 div
标签就具有 id
这个属性(attribute),取值为 'nav'
。
我们把 JS 对象上的属性称为 properties
。
标签的 attributes 与 DOM 节点对象的 properties
在浏览器加载页面时,会将 HTML 文件解析为 DOM 树,DOM 树中每一个 DOM 对象作为节点,DOM 对象和 HTML 中的数据一一对应,大部分情况下 HTML 标签上的 attributes
就对应相应的 DOM 对象上的 properties
,比如:
<body>
<div id='wrap'></div>
</body>
<script>
const div = document.getElementById('wrap');
console.log(div.id); // wrap
</script>
important
HTML 标签内的某些 attribute
并不与 DOM 对象上的 property
同名,比如,HTML 标签内的 class
对应 DOM 对象上的 className
。
只有标准的 attributes 才能映射为 properties
在解析 HTML 过程中,将标签的 attributes
映射为 DOM 对象的 properties
之前会先 检查标签上的 attributes
是否标准,如果是 非标准的 attribute,那么不会被映射为 DOM 对象 properties
:
<body>
<div id='wrap' something="non-standard"></div>
</body>
<script>
const div = document.getElementById('wrap');
console.log(div.something); // undefined
</script>
这里所谓的标准,意思就是标签本身是否具有这个 attribute
,如果标签本身没有该 attribute
,比如上面例子中 div 标签肯定没有 something
这个属性,那么它就不会被映射到 DOM 对象的 properties
中。
通过下面的 API,我们可以给一个 DOM 对象添加、删除、修改任何 attributes
,无论标准与否。
方法 | 说明 |
---|---|
elem.hasAttribute(name) | 检查 attribute 是否存在 |
elem.getAttribute(name) | 获取这个 attribute |
elem.setAttribute(name, value) | 设置这个 attribute |
elem.removeAttribute(name) | 移除这个 attribute |
elem.attributes | 获取 属性节点 类数组 |
添加上的 attribute
会直接出现在标签中,我们打开 Chrome 开发者调试工具就可以看见。
需要注意的是,如果使用上面的 API 添加了一个非标准的 attribute
,那么只能通过 elem.getAttribute()
方法来获取该属性,因为它不能映射为 DOM 对象的 property
。
属性节点
HTML 标签内的 attributes 最终解析的时候会生成属性节点对象,它有如下特点:
- 属性节点的 name 对大小写不敏感,value 取值一定是字符串
- 由 HTML 标签中的
attributes
映射到 DOM 对象上的properties
数据类型可能为:String
Boolean
,比如hidden
和 input 标签的 checked 属性Object
,比如style
属性
标准的 attributes 与 properties 同步更新问题
在一般情况下:
- 我们通过
set/removeAttribute
修改一个标签的标准attribute
时,该标签对应的 DOM 对象上的property
也会同步改变 - 同理,修改 DOM 对象上标准的
property
时,HTML 标签内的标准attribute
也会同步改变
有一个例外就是 input 的 value
属性。
input 标签上的 value
属性和 input DOM 对象上的 value
属性并不能相互影响。
你可以把 input 标签上的 value 当作 输入框的初始内容,初始状态下标签上的 value 等于 DOM 对象上的 value。
后续修改输入框内容,内容与 DOM 对象上的 value 是同步的,但是标签上的 value 就不会再改变了。
就算你使用 elem.setAttribute()
方法,来修改标签上的 value,起到的作用仅仅是修改输入框的初始值,并不能实时修改输入框内容。
自定义 attributes
前文中所说的非标准 attributes
就可以理解为自定义 attributes
。
虽然我们可以随意的给任何一个标签添加自定义 attribute
,就像下面这样。
<div show-info="name">name:</div>
<div show-info="age">age:</div>
<div show-info="gender">gender:</div>
我们现在添加的自定义 attribute
或许能够正常工作,但是在之后的 HTML 版本更新中,可能会为某些标签添加新的标准 attribute
,这就很可能和我们自定义的 attribute
产生冲突。
为了避免上述问题,在 HTML 标准中,为所有标签提供了一个 data-*
这样的保留 attribute
,专为程序员服务,在标签中任何以 data-
开头的 attribute
最终会保存在相应 DOM 节点的 dataset
属性中:
<body>
<div data-about='Elephants'></div>
</body>
<script>
console.log(document.body.firstElementChild.dataset.about); // 'Elephants'
</script>
elem.dataset
是可读写的,在标签中的 data-
短横线的命名格式会转换为 elem.dataset
里面驼峰命名的格式。