深入理解JavaScript:DOM的全面解析

发表时间: 2024-03-05 16:30

一、节点层级

说明: 任何一个html文档都可以使用DOM将其表示成一个由节点构成的层级结构,当然,节点的类型很多,这也使得构成的html文档可以各种各样

<!-- 最基本的HTML片段 --><!DOCTYPE html><html lang="en">  <head>    <meta charset="UTF-8" />    <meta name="viewport" content="width=device-width, initial-scale=1.0" />    <title>Document</title>  </head>  <body></body></html>

如果以层级结构进行描述,在每个html文档中,其根节点只有一个,那就是document,其次,根节点存在唯一子节点是<html>元素,这个元素成为文档元素,在html页面里面,文档元素就是<html>元素,并且有且只有一个,在DOM 中总共有12种节点类型,这些类型都继承一种基本类型。

1.Node类型

说明: 最开始的DOM描述了Node的接口,这个是每个DOM节点必须实现的,在JavaScript中将其设计成Node类型,每个节点都继承这个类型,节点类型由12个常量表示

Node.ELEMENT_NODE:1 Node.ATTRIBUTE_NODE:2 Node.TEXT_NODE:3 Node.CDATA_SECTION_NODE:4 Node.ENTITY_REFERENCE_NODE:5 Node.ENTITY_NODE:6 Node.PROCESSING_INSTRUCTION_NODE:7 Node.COMMENT_NODE:8 Node.DOCUMENT_NODE:9 Node.DOCUMENT_TYPE_NODE:10 Node.DOCUMENT_FRAGMENT_NODE:11 Node.NOTATION_NODE:12

这样一个节点的类型可通过与这些常量比较来确定,而这个数值可以使用元素节点.nodeType来获取

<!DOCTYPE html><html lang="en">  <head>    <meta charset="UTF-8" />    <meta name="viewport" content="width=device-width, initial-scale=1.0" />    <title>Document</title>  </head>  <body>    <h1>Hello, World!</h1>    <script>      // 获取h1这个节点      let titleElement = document.querySelector("h1");      // 下面这两个值是相等的,这样就可以确定这是一个元素节点了      console.log(titleElement.nodeType);      console.log(titleElement.ELEMENT_NODE);    </script>  </body></html>

nodeName与nodeValue

说明: 这两个属性保存着有关节点的信息,但属性的值完全取决于节点的类型,对元素 而言,nodeName 始终等于元素的标签名,而 nodeValue 则始终为 null

// 以上面的html为例:let titleElement = document.querySelector("h1");console.log(titleElement.nodeName); // h1console.log(titleElement.nodeValue); // null

节点关系

说明: 文档内部的节点都会与其他节点存在关系,一般谁在外层谁是父,同层就是兄,以下面的片段为例

<!DOCTYPE html><html lang="en">  <head>    <meta charset="UTF-8" />    <meta name="viewport" content="width=device-width, initial-scale=1.0" />    <title>Document</title>  </head>    <body>  </body></html>

<body>元素是<html>元素的子元素,而<html>元素则是<body>元素的父元,<head>元素是<body>元素的同胞元素,因为它们有共同的父元素<html>

let titleElement = document.querySelector("h1");console.log(titleElement.childNodes[0]);console.log(titleElement.childNodes.item(0));console.log(titleElement.childNodes);


每个节点都有一个childNodes属性,其中包含一个NodeList的实例,这是一个类数组的对象,用于存储可以按位置存取的有序节点,可以使用[]或者item()来访问,最后,NodeList是实时的活动对象,而不是第一次访问时所获得内容的快照,需要将其转换成数组的时候可以使用Array.from来完成,其次,如果需要查询一个元素是否有子元素,可以使用hasChildNodes()


每一个节点都有一个parentNode属性,表示节点的父元素,那么上面的childNodes中的每个元素都存在相同的父元素,并且它们之间以兄弟相称,可以使用previousSibling(上一个元素)和nextSibling(下一个元素)来回切换,如果切换不了就是null,firstChild和lastChild分别指向childNodes中的第一个和最后一个子节点,如果只有一个子元素,它们相等,如果没有子元素,那么都是null

Element Traversal API新增属性:

childElementCount:返回子元素数量(不包含文本节点和注释); firstElementChild:指向第一个 Element 类型的子元素(旧版为firstChild); lastElementChild:指向最后一个 Element 类型的子元素(旧版为 lastChild); previousElementSibling:指向前一个 Element 类型的同胞元素(旧版为 previousSibling); nextElementSibling:指向后一个 Element 类型的同胞元素(旧版为nextSibling)。

(3)操作节点

appendChild(添加的节点):在 childNodes 列表末尾添加节点,这个方法会返回新添加的节点,如果传递的节点是已经存在的节点,那么这个节点就会从原来的位置转移到新的位置 insertBefore(插入的节点,参照的节点):用于插入节点,将插入的节点会变成参照节点的前一个兄弟节点,并返回,如果参照节点是null,则与第一个方法一致 replaceChild(插入的节点,替换的节点):替换的节点会被返回并从文档 树中完全移除,要插入的节点会取而代之 removeChild(移除的节点):将指定的节点删除,其返回值是这个删除的节点

注意: 并非所有节点类型都有子节点,如果在不支持子节点的节点上调用 这些方法,则会导致抛出错误

(4)其它方法

cloneNode(是否深度复制):用于复制节点,如果传true,进行深度复制,那么其子节点也会被复制,传false则只会复制本身而已 normalize():用于处理文本节点,在遍历其所有子节点的时候,如果发现空文本节点,则将其删除;如果两个同胞节点是相邻的,则将其合并为一个文本节点

2.Document类型

说明: Document类型表示文档节点的类型,文档对象document是 HTMLDocument的实例,它表示HTML页面,document是window对象的属性,因此是一个全局对象

Document 类型的节点的特征:

nodeType:9; nodeName:"#document"; nodeValue:null; parentNode:null; ownerDocument:null; 子节点:DocumentType(最多一个)、Element(最多一个)、ProcessingInstruction 或 Comment 类型。

(1)文档子节点

document.documentElement:返回HTML页面中所有的元素 document.body:返回body中所有的元素 document.doctype:获取文档最开头的东西

<!DOCTYPE html><html lang="en">  <head>    <meta charset="UTF-8" />    <meta name="viewport" content="width=device-width, initial-scale=1.0" />    <title>Document</title>  </head>  <body>    <h1>Hello, World!</h1>    <script>      let titleElement = document.querySelector("h1");      console.log(document.documentElement);      console.log(document.body);      console.log(document.doctype);    </script>  </body></html>




出现在元素外面的注释也是文档的子节点,它们的类型是Comment

(2)文档信息

document.title:包含<title>元素中的文本,通常显示在浏览器窗口或标签页的标题栏,内容可以通过这个属性进行更改,修改也会反映在页面上,但是修改title属性并不会改变元素<title>里面的内容 document.URL:地址栏中的 URL(只读) document.domain:页面的域名,在设置值的时候不能存在URL中不包含的值,最后,新设置的值不能比旧的值长,否则会导致错误 document.referrer:包含链接到当前页面的那个页面的URL,如果没有就是''(只读),

(3)定位元素

说明: 在操作DOM的时候最常见的操作就是获取某个或者某组元素的引用,然后对它们执行某些操作

document.getElementById(元素的ID):查找带有指定ID的元素(ID值需要完全匹配才可以),如果找到就返回这个元素,没找到就返回null,如果查找的ID元素存在多个,只返回第一个
document.getElementsByTagName(元素的标签名):寻找符合标签名的元素,其返回值是一个HTMLCollection对象,它与NodeList相似,所以可以使用相同的[]和item()方法来获取指定的元素,这个对象还存在一个namedItem()的方法,通过标签的name属性获取某一项的引用,对于 name 属性的元素,还可以直接使用中括号来获取,最后就是这个方法如果传入*,表示匹配一切字符
document.getElementsByName(name属性值):寻找满足条件name属性值的元素,返回值也是一个HTMLCollection对象,那么使用起来跟上一个方法相差不大

<!DOCTYPE html><html lang="en">  <head>    <meta charset="UTF-8" />    <meta name="viewport" content="width=device-width, initial-scale=1.0" />    <title>Document</title>  </head>  <body>    <img src="myimage.gif" name="myImage" />    <script>      let imgElement = document.getElementsByTagName("img");      console.log(imgElement);      console.log(imgElement[0]);      console.log(imgElement.item(0));      console.log(imgElement.namedItem("myImage"));      console.log(imgElement["myImage"]);    </script>  </body></html>


对于HTMLCollection对象而言,中括号既可以接收数值索引,也可以接收字符串索引。而在后台, 数值索引会调用item(),字符串索引会调用namedItem()。

(4)特殊集合

document.forms:查找文档中所有<form>元素,返回值是HTMLCollection对象 document.images:查找文档中所有<img>元素,返回值是HTMLCollection对象 document.links:查找文档中所有带href属性的<a>元素,返回值是HTMLCollection对象

(5)文档写入

document.write('字符串'):在页面加载期间向页面中动态添加内容,一般用于动态包含外部资源 document.writeln('字符串'):在页面加载期间向页面中动态添加内容并且在末尾加一个\n,一般用于动态包含外部资源 document.open():打开网页输出流,在node中使用的比较多 document.close():关闭网页输出流,在node中使用的比较多

如果是在页面加载完毕再去动态的去写入,则写入的内容会重写整个页面

3.Element类型

说明: 它暴露出访问元素标签名、子节点和属性的能力

Element类型的节点的特征:

nodeType:1; nodeName:元素的标签名; nodeValue:null; parentNode:Document 或 Element 对象; 子节点的类型: Element、Text、Comment、ProcessingInstruction、CDATASection、EntityReference。

对于标签名的获取,可以使用节点.nodeName或者节点.tagName来获取,不过,在HTML中使用的时候,获取到的结果都是以大写形式的标签名,在XML中,获取的与源代码中标签名的大小写一致,使用的时候需要注意

<!DOCTYPE html><html lang="en">  <head>    <meta charset="UTF-8" />    <meta name="viewport" content="width=device-width, initial-scale=1.0" />    <title>Document</title>  </head>  <body>    <img src="myimage.gif" name="myImage" />    <script>      let imgElement = document.getElementsByName("myImage");      console.log(imgElement[0].nodeName);      console.log(imgElement[0].tagName);    </script>  </body></html>


(1)HTML元素

说明: 所有的HTML元素都可以通过HTMLElement类型表示,包括实例,另外,HTMLElement直接继承了Element并增加了以下属性,这些属性是每个HTML 元素都存在的属性

id:元素在文档中的唯一标识符; title:包含元素的额外信息,通常以提示条形式展示; lang:元素内容的语言代码(很少用); dir:语言的书写方向("ltr"表示从左到右,"rtl"表示从右到左); className:相当于 class 属性,用于指定元素的 CSS 类

可以用对应的属性修改相应的值,不过修改id和lang对用户是不可见的,修改title只有在鼠标移到元素上面才反应出来

(2)获取属性

节点.getAttribute('需要获取的属性名'):返回这个节点上面指定属性名对应的属性值

console.log(imgElement[0].getAttribute("name"));


传递的属性名应该与它们实际的属性名是一样的,如果搜索的属性名不存在,返回值是null,此外,属性名没有大小写之分,最后,当使用DOM对象访问属性的时候,在访问style和事件的时候其返回值与getAttribute的返回值是存在区别的

(3)设置属性

节点.setAttribute('设置的属性名','属性的值'):如果属性存在,则属性值会被替换成新的,如果不存在,则会创建这个属性,此外,这个方法在设置的属性名会规范为小写形式,同时对于自定义属性,并不会将其添加到元素的属性上面去 节点.removeAttribute('需要删除的属性'):将指定的属性从元素上面删除

(4)创建元素

document.createElement('创建元素的标签名'):创建一个新元素,注意HTML不存在大小写,而XML存在,其次,在创建新元素的同时也会将ownerDocument属性设置为 document。 此时,可以再为其添加属性、添加更多子元素,不过,如果这个元素没有被添加到文档中去,添加再多的属性也是依附在元素上的信息,在浏览器上并不会渲染出来

4.Text类型

说明: Text节点由Text类型表示,也就是文本内容,一般包含在标签内部,通常使用childNodes获取,另外,这种节点不包含HTML代码

Text类型的节点的特征:

nodeType:3 nodeName:"#text" nodeValue:节点中包含的文本 parentNode:Element 对象 子节点的类型: 没有

这种节点的内容一般使用nodeValue属性访问,也可以使用data属性访问,不过很少使用,他们包含的值是相同的,这两个属性加上.length就可以得到文本节点包含的字符数了

文本节点操作方法:

appendData(text):向节点末尾添加文本 text; deleteData(offset, count):从位置 offset 开始删除 count 个字符; insertData(offset, text):在位置 offset 插入 text; replaceData(offset, count, text):用 text 替换从位置 offset 到 offset count的文本; normalize:当一个节点存在多个文本节点的时候,可以使用这个方法将其合并成一个字符串 splitText(offset):在位置offset将当前文本节点拆分为两个文本节点; substringData(offset, count):提取从位置 offset 到 offset + count 的文本。 length:获取文本节点包含的字符数

console.log(imgElement.childNodes.item(0).nodeValue);console.log(imgElement.childNodes.item(0).nodeValue.length);console.log(imgElement.childNodes.item(0).data);console.log(imgElement.childNodes.item(0).data.length);


文本内容的每个元素最多只能有一个文本节点,另外在修改文本节点的时候,小于号、大于号或引号会被转义

(1)创建文本节点

document.createTextNode('文本节点的内容'):创建一个文本节点,当然,创建的内容中小于号、大于号或引号会被转义,一般来说一个元素只包含一个文本子节点。不过,也可以让元素包含多个文本子节点

let element = document.createElement("div"); element.className = "message"; let textNode = document.createTextNode("Hello world!"); element.appendChild(textNode); let anotherTextNode = document.createTextNode("Yippee!"); element.appendChild(anotherTextNode); document.body.appendChild(element); 


在将一个文本节点作为另一个文本节点的兄弟元素插入,两个文本节点的文本之间 不包含空格

5.Comment类型

说明: 这是一个注释类型,与Text 类型相似,除了没有splitText这个方法以外操作上是一致的,在创建的时候可以通过document.createComment('注释的内容')来创建,最后,浏览器不承认结束的</html>标签之后的注释。如果要访问注释节点,则必须确定它们是</html>元素的后代

Comment类型的节点的特征:

nodeType:8 nodeName:"#comment" nodeValue:注释的内容 parentNode:Document 或 Element 对象 子节点的类型: 没有

6.CDATASection类型

说明: 它表示XML中特有的CDATA区块,同时继承Text 类型,因此拥有其拥有的方法,在真正的XML文档中,可以使用
document.createCDataSection()并传入节点内容来创建CDATA区块

CDATASection类型的节点的特征:

nodeType:4 nodeName:"#cdata-section" nodeValue:CDATA 区块的内容 parentNode:Document 或 Element 对象 子节点的类型: 没有

7.DocumentType类型

说明: DocumentType对象不支持动态创建,只能在解析文档代码时创建,其次,文档类型可以通过document.doctype来获取,在这个对象中存在三个属性:name、entities和notations,name是文档类型的名称,就是近跟在!DOCTYPE后面的文本,结束符是空格,entities是这个文档类型描述的实体的NamedNodeMap,而 notations是这个文档类型描述的表示法的NamedNodeMap。因为浏览器中的文档通常是HTML或XHTML文档类型,所以entities和notations列表为空,在DOM2的时候扩展了三个属性:publicId、systemId和internalSubset,取值如下示例

DocumentType类型的节点的特征:

nodeType:10 nodeName:文档类型的名称 nodeValue:null parentNode:Document 对象 子节点的类型: 没有

<!DOCTYPE html PUBLIC "-// W3C// DTD XHTML 1.0 Strict// EN" "http://www.w3.org/TR/xhtml1/DTDxhtml1-strict.dtd" [<!ELEMENT name (#PCDATA)>] >
document.doctype.name // htmldocument.doctype.publicId // -// W3C// DTD HTML 4.01// ENdocument.doctype.systemId // "http://www.w3.org/TR/ html4/strict.dtd"document.doctype.internalSubset // "<!ELEMENT name (#PCDATA)>"

8.DocumentFragment类型

说明: 是一种特殊的节点类型,它允许你在内存中创建一个文档片段,然后将其他节点附加到该片段中。文档碎片不是真实DOM树的一部分,因此对其进行操作不会触发页面重绘,这样可以提高性能并减少DOM操作的成本,它可以通过
document.createDocumentFragment()来创建

DocumentFragment类型的节点的特征:

nodeType:11 nodeName:#document-fragment nodeValue:null parentNode:null 子节点的类型:Element、ProcessingInstruction、Comment、Text、CDATASection 或 EntityReference

<ul id="myList"></ul> 
// 给ul添加三个lilet fragment = document.createDocumentFragment(); let ul = document.getElementById("myList"); for (let i = 0; i < 3; ++i) {  let li = document.createElement("li");  li.appendChild(document.createTextNode(`Item ${i + 1}`));  fragment.appendChild(li); } ul.appendChild(fragment); 

二、DOM编程

1.动态脚本

说明: <script>这个标签用于向网页添加JavaScript代码,可以通过src属性引入外部的JavaScript文件,也可以是元素内容的源代码,动态加载脚本就是页面加载时不存在,之后通过DOM引入的JavaScript

// 假设需要向页面加入一个foo.js的脚本let script = document.createElement("script");script.src = "foo.js";document.body.appendChild(script);
// 如果需要插入代码加载脚本<script>   function sayHi() {     alert("hi");   } </script> 
// 通过DOM操作改写上面的HTML片段let script = document.createElement("script");script.appendChild(    document.createTextNode(        "function sayHi(){            alert('hi');        }"    ));document.body.appendChild(script);

2.动态样式

说明: 这个与上面是类似的,只不过加载的内容不同,一种使用<link>引入外部文件,一种是用<style>写样式代码,动态同样也是页面初始加载不存在,后面才加上去的

<!-- 假设加载一个styles.css的文件 --><link rel="stylesheet" type="text/css" href="styles.css"> 
// 通过DOM操作改写let link = document.createElement("link");link.rel = "stylesheet";link.type = "text/css";link.href = "styles.css";let head = document.getElementsByTagName("head")[0];head.appendChild(link);
<!-- 另一种通过style加载css规则 --><style type="text/css">   body {     background-color: red;   } </style> 
// 使用DOM操作改写let style = document.createElement("style");style.type = "text/css";style.appendChild(    document.createTextNode(        "body{            background-color:red        }"    ));let head = document.getElementsByTagName("head")[0];head.appendChild(style);

对于IE的浏览器,操作style节点的时候需要使用这个节点的styleSheet属性的cssText属性,给这个属性设置css样式字符串就可以了

最后,需要注意,NodeList对象和相关的NamedNodeMap、HTMLCollection其保存的值会随着节点的变化而变化,所以使用的时候需要注意

三、MutationObserver接口

说明: MutationObserver是一个用于监视DOM树变化的接口。它可以用来观察DOM节点的插入、删除、属性的变化等变动,并在这些变动发生时执行特定的回调函数,MutationObserver的实例要通过调用MutationObserver构造函数并传入一个回调函数来创建

// 这样就创建了一个观察者observerlet observer = new MutationObserver(    () => console.log('<body> attributes changed'));

1.observe()方法

说明: 上面创建的这个观察者实例并不会关联DOM的任何部分,如果需要,则需要使用observe方法,它有两个参数,第一个是需要观察的DOM节点(必须),第二个是一个配置对象(可选),在使用这个方法之后,被监听的元素上面的属性发生变化的时候都会异步的执行注册的回调函数,后代元素的属性更改并不会触发

配置对象的参数:

subtree:当为 true 时,将会监听以 target 为根节点的整个子树。包括子树中所有节点的属性,而不仅仅是针对 target。默认值为 false。 childList:当为 true 时,监听 target 节点中发生的节点的新增与删除(同时,如果 subtree 为 true,会针对整个子树生效)。默认值为 false。 attributes:当为 true 时观察所有监听的节点属性值的变化。默认值为 true,当声明了 attributeFilter 或 attributeOldValue,默认值则为 false。 attributeFilter:一个用于声明哪些属性名会被监听的数组。如果不声明该属性,所有属性的变化都将触发通知。 attributeOldValue:当为 true 时,记录上一次被监听的节点的属性变化;可查阅监听属性值了解关于观察属性变化和属性值记录的详情。默认值为 false。 characterData:当为 true 时,监听声明的 target 节点上所有字符的变化。默认值为 true,如果声明了 characterDataOldValue,默认值则为 false characterDataOldValue:当为 true 时,记录前一个被监听的节点中发生的文本变化。默认值为 false

observer.observe(document.body, { attributes: true }); document.body.className = 'foo';console.log('Changed body class');


这也证明注册函数的执行是异步的

2.回调参数

说明: MutationObserver实例中注册的回调函数的参数有两个,都是可选的,一个是MutationRecord 实例的数组,它包含的息包括发生了什么变化,以及 DOM 的哪一部分受到了影响,此外,连续的修改会生成多个实例,在最后一次修改后一次性按顺序返回回来,另一个是观察变化的MutationObserver的实例,这个主要观察属性的变化

MutationRecord实例的属性:

target:被修改影响的目标节点 type: 字符串,表示变化的类型:"attributes"、"characterData"或"childList" oldValue: 如果在 MutationObserverInit 对象中启用,"attributes"或"characterData"的变化事件会设置这个属性为被替代的值 "childList"类型的变化始终将这个属性设置为 null attributeName: 对于"attributes"类型的变化,这里保存被修改属性的名字 其他变化事件会将这个属性设置为 null attributeNamespace: 对于使用了命名空间的"attributes"类型的变化,这里保存被修改属性的名字 其他变化事件会将这个属性设置为 null addedNodes: 对于"childList"类型的变化,返回包含变化中添加节点的 NodeList 默认为空 NodeList removedNodes: 对于"childList"类型的变化,返回包含变化中删除节点的 NodeList 默认为空 NodeList previousSibling:对于"childList"类型的变化,返回变化节点的前一个同胞 Node 默认为 null nextSibling:对于"childList"类型的变化,返回变化节点的后一个同胞 Node 默认为 null

let observer = new MutationObserver(   (mutationRecords) => console.log(mutationRecords));observer.observe(document.body, { attributes: true }); document.body.setAttribute('foo', 'bar');


// 连续更改let observer = new MutationObserver(   (mutationRecords) => console.log(mutationRecords));observer.observe(document.body, { attributes: true }); document.body.className = 'foo'; document.body.className = 'bar'; document.body.className = 'baz';


3.终止回调

说明: 一般情况下,只要被监听的元素没有被垃圾回收,那么MutationObserver中注册的回调函数就会在属性变化的时候执行一次,如果需要这个回调函数失效,可以使用disconnect()这个方法,它会取消之前加入队列和之后加入队列的回调函数,也就是停止观察

let observer = new MutationObserver(    () => console.log('<body> attributes changed')); observer.observe(document.body, { attributes: true }); document.body.className = 'foo'; observer.disconnect(); document.body.className = 'bar';


希望断开与观察目标的联系,但又希望处理由于调用disconnect()而被抛弃的记录队列中的MutationRecord实例,可以使用takeRecords()清空记录队列,取出里面的实例

4.一观多用

说明: 多次调用observe(),可以使用一个创建的观察者实例观察多个目标节点,这个过程可以通过MutationRecord参数的target属性观察

let observer = new MutationObserver(   (mutationRecords) =>     console.log(      mutationRecords.map(        (x) => x.target      )    )  ); // 向页面主体添加两个子节点let childA = document.createElement('div'), childB = document.createElement('span'); document.body.appendChild(childA); document.body.appendChild(childB);// 观察两个子节点observer.observe(childA, { attributes: true }); observer.observe(childB, { attributes: true }); // 修改两个子节点的属性childA.setAttribute('foo', 'bar'); childB.setAttribute('foo', 'bar');


5.重启回调

说明: 使用disconnect会停止观察者中的回调函数,但是其生命并未结束,可以重新使用observe将其关联到新的节点将其重启

let observer = new MutationObserver(    () => console.log('<body> attributes changed')); observer.observe(document.body, { attributes: true }); // 停止回调observer.disconnect(); // 这个不会触发document.body.className = 'bar';// 重启observer.observe(document.body, { attributes: true }); // 照常触发document.body.className = 'bar';


6.设计?

说明: 这个接口的设计用于性能优化,其核心是异步回调与记录队列模型,为了在大量变化事件发生时不影响性能,每次变化的信息(由观察者实例决定)会保存在 MutationRecord实例中,然后添加到记录队列。这个队列对每个 MutationObserver实例都是唯一的,每次MutationRecord被添加到 MutationObserver的记录队列时,仅当之前没有已排期的微任务回调时,才会将观察者注册的回调作为微任务调度到任务队列上。这样可以保证记录队列的内容不会被回调处理两次,回调执行后,这些MutationRecord就用不着了, 因此记录队列会被清空,其内容会被丢弃

四、MutationObserverInit

说明: 这个对象用于控制对目标节点的观察范围,看上去很高级,其实就是observe这个函数的第二个参数,参数的具体内容在这个函数这里有写到

在调用observe()时,MutationObserverInit 对象中的 attribute、characterData 和 childList 属性必须至少有一项为 true,否则会抛出错误,因为没有任何变化事件可能触发回调,

1.观察属性

说明: MutationObserver可以观察节点属性的添加、移除和修改。要为属性变化注册回调,需要在MutationObserverInit对象中将attributes属性设置为true

let observer = new MutationObserver(   (mutationRecords) => console.log(mutationRecords)); observer.observe(document.body, { attributes: true }); // 添加属性 document.body.setAttribute('foo', 'bar'); // 修改属性document.body.setAttribute('foo', 'baz'); // 移除属性document.body.removeAttribute('foo');


如果想观察某个或某几个属性,可以使用attributeFilter属性来设置白名单来进行过滤

let observer = new MutationObserver(   (mutationRecords) => console.log(mutationRecords)); observer.observe(document.body, { attributeFilter: ['foo'] }); // 添加白名单属性document.body.setAttribute('foo', 'bar'); // 添加被排除的属性document.body.setAttribute('baz', 'qux');


2.观察字符

说明: MutationObserver可以观察文本节点中字符的添加、删除和修改。要为字符数据注册回调,需要在MutationObserverInit对象中将characterData属性设置为true

let observer = new MutationObserver(   (mutationRecords) => console.log(mutationRecords));// 创建要观察的文本节点document.body.firstChild.textContent = 'foo'; observer.observe(document.body.firstChild, { characterData: true }); // 赋值为相同的字符串document.body.firstChild.textContent = 'foo'; // 赋值为新字符串document.body.firstChild.textContent = 'bar'; // 通过节点设置函数赋值document.body.firstChild.textContent = 'baz';


3.观察子节点

说明: MutationObserver可以观察目标节点子节点的添加和移除。要观察子节点,需要在MutationObserverInit对象中将childList属性设置为true

// 假设需要交换两个子节点的位置document.body.innerHTML = ''; let observer = new MutationObserver(  (mutationRecords) => console.log(mutationRecords));// 创建两个初始子节点document.body.appendChild(document.createElement('div')); document.body.appendChild(document.createElement('span')); observer.observe(document.body, { childList: true }); // 交换子节点顺序(先删除后添加)document.body.insertBefore(document.body.lastChild, document.body.firstChild);


4.观察子树

说明: 默认情况下,MutationObserver将观察的范围限定为一个元素及其子节点的变化。可以把观察的范围扩展到这个元素的子树(所有后代节点),这需要在 MutationObserverInit对象中将subtree属性设置为true

注意:被观察子树中的节点被移出子树之后仍然能够触发变化事件。这意味着在子树中的节 点离开该子树后,即使严格来讲该节点已经脱离了原来的子树,但它仍然会触发变化事件

// 清空主体document.body.innerHTML = ''; let observer = new MutationObserver(  (mutationRecords) => console.log(mutationRecords));let subtreeRoot = document.createElement('div'),     subtreeLeaf = document.createElement('span'); // 创建包含两层的子树document.body.appendChild(subtreeRoot); subtreeRoot.appendChild(subtreeLeaf); // 观察子树observer.observe(subtreeRoot, { attributes: true, subtree: true }); // 把节点转移到其他子树document.body.insertBefore(subtreeLeaf, subtreeRoot); subtreeLeaf.setAttribute('foo', 'bar');


五、Selectors API

1.querySelector()

说明: 这个方法接收一个CSS选择符参数,也就是在写样式的时候,怎么写选择器,这里就怎么写,它会返回匹配该模式的第一个元素,如果匹配不成功则返回null

如果是在document上用,则会从文档元素开始搜索,如果在element上用,则只从当前元素后代中搜索

// 取得<body>元素let body = document.querySelector("body"); // 取得 ID 为"myDiv"的元素let myDiv = document.querySelector("#myDiv"); // 取得类名为"selected"的第一个元素let selected = document.querySelector(".selected");

2.querySelectorAll()

说明: 这个与上面那个是相似的,只不过它会返回所有匹配到的元素,说白了就是一个NodeList对象,只不过这个对象是静态的,在取值上面,可以通过for-ofitem()方法或中括号语法取元素

// 取得 ID 为"myDiv"的<div>元素中的所有<em>元素let ems = document.getElementById("myDiv").querySelectorAll("em"); // 取得所有类名中包含"selected"的元素let selecteds = document.querySelectorAll(".selected"); // 取得所有是<p>元素子元素的<strong>元素let strongs = document.querySelectorAll("p strong");

3.matches()

说明: 这个方法有点服务于上面两个方法的意为,作用是为了查找是否存在元素满足一段css选择符的选择,满足就返回true,否则就是false

// 检测body中是否存在classpage1的元素if (document.body.matches("body .page1")){}

六、HTML5

1.css类扩展

(1)getElementsByClassName()

说明: 它存在于document对象和所有HTML元素上,它接受一个或多个类名组合而成的字符串,类名之间用空格隔开(类名的顺序无关紧要),在document中调用则会返回文档中所有匹配的元素,如果在某个元素上调用则会返回它后代中匹配的所有元素

// 取得所有类名中包含"username"和"current"元素// 这两个类名的顺序无关紧要let allCurrentUsernames = document.getElementsByClassName("username current");

(2)classList属性

说明: 以前操作属性,可以通过className完成属性的添加、删除、替换,因为这个属性是一个字符串,所以更改这个值之后,需要重新设置这个值才算完成更改,在HTML5里面,新增加了classList这个属性,它简化了这些操作,它的返回值是一个DOMTokenList的实例。与其他集合类型一样,DOMTokenList也有length属性表示自己包含多少项,也可以通过item()或中括号取得个别的元素。

DOMTokenList的实例新增属性:

add(value):向类名列表中添加指定的字符串值 value。如果这个值已经存在,则什么也不做。 contains(value):返回布尔值,表示给定的 value 是否存在。 remove(value):从类名列表中删除指定的字符串值 value。 toggle(value):如果类名列表中已经存在指定的 value,则删除;如果不存在,则添加。

<!-- 假设存在这样的节点 --><div class="bd user disabled">...</div>
// 获取这个节点let divElement = document.getElementsByTagName("div");
// 通过className属性获取这个节点的class,拿到之后对其操作后// 需要重新对className属性赋值// 要删除"user"类let targetClass = "user"; // 把类名拆成数组let classNames = divElement.className.split(/\s+/); // 找到要删除类名的索引let idx = classNames.indexOf(targetClass); // 如果有则删除if (idx > -1) {  classNames.splice(i,1); } // 重新设置类名divElement.className = classNames.join(" ");


// 通过classList属性进行操作// 删除"disabled"类div.classList.remove("disabled"); // 添加"current"类div.classList.add("current"); // 切换"user"类div.classList.toggle("user"); 


2.焦点

document.activeElement: 它保存当前页面获取焦点的元素,由于不同同时让多个节点获取焦点,那也就是这个值只会保留最后一个获取焦点的元素,在页面没完全加载完前,它的值是null,在完全加载完之后,值是document.body document.hasFocus:用于检测文档是否拥有焦点,也就是用户是否正在进行交互,它的返回值是布尔值,表示存在或者不存在 节点.foucs:执行这个方法可以让某个节点获取焦点

3.HTMLDocument的扩展

(1)readyState属性

说明: 它表示文档加载的状态,它的值有下面两个

loading:表示文档正在加载 complete:表示文档加载完成

(2)compatMode属性

说明: 它表示浏览器当前处于什么渲染模式

标准模式:CSS1Compat 混杂模式:BackCompat

(3)head属性

说明: 可以直接使用document.head来获取<head>元素

4.字符集的扩展

(1)characterSet属性

说明: 它表示文档实际使用的字符集,也可以用来指定新字符集,默认值是UTF-16

5.自定义属性

说明: 这个操作是html5允许的操作,但要使用前缀data-以便告诉浏览器,这些属性既不包含与渲染有关的信息,也不包含元素的语义信息,不过命名没有什么要求,在设置完成后,可以通过dataset属性,它的值是一个DOMStringMap的实例,包含一组键/值对映射,在取值时使用data-后面所有的字符拼接成一个字符串,这个字符串是小写的来取值

    <div id="myDiv" data-appId-MaMaMaMaMa="12345" data-myname="Nicholas"></div>
let div = document.getElementById("myDiv");console.log(div.dataset);


6.标记

(1)innerHTML和outerHTML属性

说明: 在读取的时候,它会返回元素所有后代的HTML字符串,在设置的时候,默认是HTML,所以设置一个字符串值的时候,会将其转换成HTML片段,前者是只能操作节点的子元素,后者则能操作节点本身和其子元素

<div id="content">  <p>This is a <strong>paragraph</strong> with a list following it.</p>  <ul>    <li>Item 1</li>    <li>Item 2</li>    <li>Item 3</li>  </ul></div>
let divElement = document.getElementById("content");console.log(divElement.innerHTML);


divElement.innerHTML = "Hello world!";


(2)insertAdjacentHTML()和insertAdjacentText()

说明: 它们表示在指定的位置插入HTML或者文本,它们都有两个参数,第一个是插入的位置选项,第二个是插入的内容

插入位置选项(无大小写之分):

beforebegin:插入当前元素前面,作为前一个兄弟节点; afterbegin:插入当前元素内部,作为新的子节点或放在第一个子节点前面; beforeend:插入当前元素内部,作为新的子节点或放在最后一个子节点后面; afterend:插入当前元素后面,作为下一个兄弟节点。

对于选项中的前面和后面的标准:
假设当前元素是<p>Hello world!</p>,则beforebegin和afterbegin中的begin指开始标签<p>,而 afterend和beforeend中的end指结束标签</p>。

7.scrollIntoView()

说明: 它可以让指定的元素滚动到可视区域,它接受一个参数,这个参数可以是一个布尔值,也可以是一个配置对象

布尔值:

true: 窗口滚动后元素的顶部与视口顶部对齐 false: 窗口滚动后元素的顶部与视口底部对齐

配置对象:

behavior: 滚动是否平滑,取值存在三个, smooth:平滑滚动 instant:通过一次跳跃立刻发生 auto:通过scroll-behavior的计算来得出,默认取值是这个 block:定义垂直方向的对齐,可取的值为"start"、"center"、"end"和"nearest",默 认为 "start"。 inline:定义水平方向的对齐,可取的值为"start"、"center"、"end"和"nearest",默 认为 "nearest"。

七、样式

1.存储元素样式

说明: 支持style属性的html元素在其节点对象上面会存在一个style的对象,它是一个CSSStyleDeclaration类型的实例,在写css属性的时候两个单词使用连字符-链接,但是在对象中就需要驼峰命名了,一般来说,如果需要,获取到了节点,就可以用这个对象进行样式操作,同时也能获取通过style属性设置的样式,注意Float这个css属性,他在style对象中对应的值是cssFloat,而不是float,因为这个在JavaScript是关键字

let myDiv = document.getElementById("myDiv"); // 设置背景颜色myDiv.style.backgroundColor = "red"; // 修改大小myDiv.style.width = "100px"; myDiv.style.height = "200px"; // 设置边框myDiv.style.border = "1px solid black";

在标准模式下,所有尺寸必须带单位,在混杂模式下,尺寸不带单位默认是px

(1)计算样式

说明: 由于style对象只能获取到style属性中的信息,对于写在<style>里面的并没有什么办法,这时可以使用
document.defaultView.getComputedStyle(计算样式的元素, 伪元素字符串)来完成,它的返回值类型跟上面那个是一样的,不过它包括<style>里面的样式

<!DOCTYPE html><html>  <head>    <title>Computed Styles Example</title>    <style type="text/css">      #myDiv {        background-color: blue;        width: 100px;        height: 200px;      }    </style>  </head>  <body>    <div      id="myDiv"      style="background-color: red; border: 1px solid black"    ></div    <script>      let myDiv = document.getElementById("myDiv");      let computedStyle = document.defaultView.getComputedStyle(myDiv, null);      console.log(myDiv.style.width);      console.log(computedStyle.width);    </script>  </body></html>


这个返回对象里面的值是不能够更改的,同时,如果浏览器存在默认的样式,也会在这个返回的对象中展示出来

2.元素尺寸

(1)偏移尺寸

注意: 下面这组属性是只读的,每次访问都会重新计算,所以一般情况避免多次使用,影响性能

节点.offsetParent:返回离当前元素最近的已定位的父元素,如果没有找到这样的父元素,则返回最近的祖先元素。 节点.offsetLeft:返回当前元素左上角相对于offsetParent节点的左边界偏移的像素值 节点.offsetHeight:它返回该元素的像素高度,高度包含该元素的垂直内边距和边框和水平滚动条,且是一个整数 节点.offsetTop:返回当前元素相对于其offsetParent元素的顶部内边距的距离 节点.offsetWidth:返回一个元素的布局宽度,这个宽度包括元素的宽度、内边距、边框以及垂直滚动条

(2)客户端尺寸

说明: 它只元素内部的内容,所以不会包含滚动条的宽度和高度,同时也是只读,避免重复调用

节点.clientHeight:返回元素内部的高度,包含内边距 节点.clientWidth:返回元素内部的宽度,包含内边距

(3)滚动尺寸

注意: 下面这组属性是只读的,每次访问都会重新计算,所以一般情况避免多次使用,影响性能

节点.scrollHeight:返回一个元素的高度,包括内边距以及溢出不可见的部分 节点.scrollLeft:返回内容+内边距的区域举例左侧的像素值,也就是左侧隐藏的像素值,当然这个值也可以自己来设置 节点.scrollTop:返回在垂直方向上举例顶部的像素值,其它与上面那个属性是一样的 节点.scrollWidth:返回一个元素的宽度,包括内边距以及溢出不可见的部分

当然,如果想快捷的确定元素的尺寸可以使用getBoundingClientRect()这个方法,它会给出元素在页面中相对于视口的位置

八、范围

说明: 你在电脑上面用鼠标一按一拉,将一些文字圈起来,此时这些文字会被选中,如果你想操作这块区域,这时范围(range)就可以使用了,它一般是用来出来文档中选择的文本,让其处理的时候更加简单,当然,它也是可以处理节点元素的

1.创建范围

说明: 可以使用document.createRange()创建一个范围,与节点类似,在这个文档中创建的范围不可以在另一个文档中去使用,每个范围都是一个Range的实例,包含以下属性和方法:


collapsed:返回一个表示范围的起始位置和终止位置是否相同的布尔值 commonAncestorContainer:返回文档中以startContainer和endContainer为后代的最深的节点 endContainer:表示范围终点所在的节点 endOffset:表示范围终点在endContainer中的位置,值是数字 startContainer:表示范围起点所在的节点 startOffset:表示范围起点在startContainer中的位置,值是数字

2.简单选择

说明: 最简单使用就是使用selectNode()或selectNodeContents()方,这两个方法都接收一个节点作为参数,并将该节点的信息添加到调用它的范围,selectNode()选择整个节点,包括其后代节点,selectNodeContents()只选择节点的后代

<!DOCTYPE html><html>  <body>    <p id="p1"><b>Hello</b> world!</p>    <script>      let range1 = document.createRange(),        range2 = document.createRange(),        p1 = document.getElementById("p1");      range1.selectNode(p1);      range2.selectNodeContents(p1);      console.log(range1);      console.log(range2);    </script>  </body></html>
  • 调用selectNode():startContainer、endContainer 和 commonAncestorContainer 都 等于传入节点的父节点。在这个例子中,这几个属性都等于 document.body。startOffset 属性等于 传入节点在其父节点 childNodes 集合中的索引(在这个例子中,startOffset 等于 1,因为 DOM 的合规实现把空格当成文本节点),而 endOffset 等于 startOffset 加 1(因为只选择了一个节点)。
  • 调用selectNodeContents():startContainer、endContainer 和 commonAncestor Container 属性就是传入的节点,在这个例子中是<p>元素。startOffset 属性始终为 0,因为范围 从传入节点的第一个子节点开始,而 endOffset 等于传入节点的子节点数量(node.child Nodes.length),在这个例子中等于2。

  • 选定节点后范围可执行的方法:

    setStartBefore(refNode):把范围的起点设置到 refNode 之前,从而让 refNode 成为选区的第一个子节点。startContainer 属性被设置为 refNode.parentNode,而 startOffset属性被设置为 refNode 在其父节点 childNodes 集合中的索引。 setStartAfter(refNode):把范围的起点设置到 refNode 之后,从而将 refNode 排除在选区之外,让其下一个同胞节点成为选区的第一个子节点。startContainer 属性被设置为 refNode.parentNode,startOffset 属性被设置为 refNode 在其父节点 childNodes 集合中的索引加 1。 setEndBefore(refNode):把范围的终点设置到 refNode 之前,从而将 refNode 排除在选区之外、让其上一个同胞节点成为选区的最后一个子节点。endContainer 属性被设置为 refNode. parentNode,endOffset 属性被设置为 refNode 在其父节点 childNodes 集合中的索引。 setEndAfter(refNode):把范围的终点设置到 refNode 之后,从而让 refNode 成为选区的最后一个子节点。endContainer 属性被设置为 refNode.parentNode,endOffset 属性被设置为 refNode 在其父节点 childNodes 集合中的索引加 1。

    3.复杂选择

    说明: 这里存在setStart(参照节点,偏移量)和setEnd(参照节点,偏移量)两个方法,它们可以选择节点的某一部分,这也是其主要的作用,同时setStart的偏移量可以理解成从哪里开始,包括偏移量这个位置,setEnd理解为结束的位置,包括偏移量这个位置

    <p id="p1">    <b>Hello</b>     world!</p>
    // 假设需要选择从"Hello"中的"llo"到" world!"中的"o"的部分// 获取相关节点的引用let p1 = document.getElementById("p1") let helloNode = p1.firstChild.firstChild let worldNode = p1.lastChild// 创建范围let range = document.createRange(); range.setStart(helloNode, 2); range.setEnd(worldNode, 3);


    4.操作范围

    range.deleteContents():删除范围包含的节点 range.extractContents():删除范围包含的节点,但是会将删除的部分返回 range.cloneContents():创建一个范围的副本,然后将这个副本返回,注意返回的不是节点

    let p1 = document.getElementById("p1"),    helloNode = p1.firstChild.firstChild,    worldNode = p1.lastChild,    range = document.createRange();range.setStart(helloNode, 2);range.setEnd(worldNode, 3);range.deleteContents();


    let p1 = document.getElementById("p1"),    helloNode = p1.firstChild.firstChild,    worldNode = p1.lastChild,    range = document.createRange();range.setStart(helloNode, 2);range.setEnd(worldNode, 3);let fragment = range.extractContents();p1.parentNode.appendChild(fragment);


    let p1 = document.getElementById("p1"),        helloNode = p1.firstChild.firstChild,        worldNode = p1.lastChild,range = document.createRange();range.setStart(helloNode, 2);range.setEnd(worldNode, 3);let fragment = range.cloneContents();p1.parentNode.appendChild(fragment);


    5.范围插入

    range.insertNode(插入的节点):在范围选区开始的位置插入一个节点,这个一般用于插入有用的信息 range.surroundContents(插入包含范围的节点):与上面不同的地方在于它可以插入包含范围的节点,其它是一致的,这个一般用于高亮关键词

    let p1 = document.getElementById("p1"),        helloNode = p1.firstChild.firstChild,        worldNode = p1.lastChild,        range = document.createRange();range.setStart(helloNode, 2);range.setEnd(worldNode, 3);let span = document.createElement("span");span.style.color = "red";span.appendChild(document.createTextNode("Inserted text"));range.insertNode(span);


    let p1 = document.getElementById("p1"),        helloNode = p1.firstChild.firstChild,        worldNode = p1.lastChild,range = document.createRange();range.selectNode(helloNode);let span = document.createElement("span");span.style.backgroundColor = "yellow";range.surroundContents(span);


    6.范围折叠

    说明: 通过折叠可以将一段文本的范围折叠成一个点,使范围的起点和终点合并,将原本包含多个节点的范围简化为只包含一个节点或一个位置,从而化简操作,折叠操作可以交给collapse(布尔值),布尔值表示折叠到范围哪一端,true表示折叠到起点,false表示折叠到终点,检测是否折叠的操作可以交给collapsed属性,其返回值也是布尔值,同时它也能检测两个节点是否是相邻的状态

    相邻状态的判定: 在范围的上下文中,我们使用的是边界点和偏移量来定义范围的起点和终点。当范围中的两个节点相邻时,它们的边界点会非常接近,甚至可能在同一个位置上。这就导致了范围的起点和终点重叠。

    <!DOCTYPE html><html>  <head>    <title>折叠将范围变成一个点</title>  </head>  <body>    <p>这是一个示例文本。</p>    <script>      var range = document.createRange();      var textNode = document.querySelector("p").firstChild;      range.setStart(textNode, 2);      range.setEnd(textNode, 7);      console.log("初始范围选中文本: " + range.toString());      range.collapse(false);      console.log("折叠后范围选中文本: " + range.toString());    </script>  </body></html>


    <!DOCTYPE html><html>  <head>    <title>检测元素是否相邻</title>  </head>  <body>    <p id="p1">Paragraph 1</p>    <p id="p2">Paragraph 2</p>    <script>      let p1 = document.getElementById("p1"),        p2 = document.getElementById("p2"),        range = document.createRange();              range.setStartAfter(p1);      range.setStartBefore(p2);    </script>  </body></html>


    假设p1和p1是不相邻的两个节点,那么将其设置成范围的起始位置和结束位置,那么上面所说的collapsed属性的值应该是false,但是结果是true,与猜测相反,那也就得到这两个元素是相邻的

    7.范围比较

    说明: 如果有多个范围,则可以使用
    range.compareBoundaryPoints(常量值,比较的范围)确定范围之间是否存在公共的起点或终点,它的返回值是一个数字,第一个范围的边界点位于第二个范围的边界点之前时返回-1,在两个范围的边界点相等时返回 0,在第一个范围的边界点位于第二个范围的边界点之后时返回1

    常量值取值:

    Range.START_TO_START(0):比较两个范围的起点; Range.START_TO_END(1):比较第一个范围的起点和第二个范围的终点; Range.END_TO_END(2):比较两个范围的终点; Range.END_TO_START(3):比较第一个范围的终点和第二个范围的起点。

    8.范围复制

    range.cloneRange():这个方法会创建调用它的范围的副本,新范围包含与原始范围一样的属性,修改其边界点不会影响原始范围

    9.范围清理

    range.detach():把范围从创建它的文档中剥离,接触对范围的引用,便于垃圾回收将其处理