前言
DOM 表示 由多层节点构成的文档,通过它开发者可以添加、删除和修改页面的各个部分,对于现在的DOM,它是真正跨平台、语言无关的表示和操作网页的方式
一、节点层级
说明: 任何一个html文档
都可以使用DOM
将其表示成一个由节点构成的层级结构,当然,节点的类型很多,这也使得构成的html文档
可以各种各样
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:
1Node.ATTRIBUTE_NODE:
2Node.TEXT_NODE:
3Node.CDATA_SECTION_NODE:
4Node.ENTITY_REFERENCE_NODE:
5Node.ENTITY_NODE:
6Node.PROCESSING_INSTRUCTION_NODE:
7Node.COMMENT_NODE:
8Node.DOCUMENT_NODE:
9Node.DOCUMENT_TYPE_NODE:
10Node.DOCUMENT_FRAGMENT_NODE:
11Node.NOTATION_NODE:
12
这样一个节点的类型可通过与这些常量比较来确定,而这个数值可以使用元素节点.nodeType
来获取
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>
<h1>Hello, World!</h1>
<script>
// 获取h1这个节点
let titleElement = document.querySelector("h1");
// 下面这两个值是相等的,这样就可以确定这是一个元素节点了
console.log(titleElement.nodeType);
console.log(titleElement.ELEMENT_NODE);
</script>
</body>
</html>
(1)nodeName与nodeValue
说明: 这两个属性保存着有关节点的信息,但属性的值完全取决于节点的类型,对元素 而言,nodeName 始终等于元素的标签名,而 nodeValue 则始终为 null
js
// 以上面的html为例:
let titleElement = document.querySelector("h1");
console.log(titleElement.nodeName); // h1
console.log(titleElement.nodeValue); // null
(2)节点关系
说明: 文档内部的节点都会与其他节点存在关系,一般谁在外层谁是父,同层就是兄
,以下面的片段为例
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>
<body>
元素是<html>
元素的子元素,而<html>
元素则是<body>
元素的父元,<head>
元素是<body>
元素的同胞元素,因为它们有共同的父元素<html>
js
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:
获取文档最开头的东西
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>
<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
对象,那么使用起来跟上一个方法相差不大
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>
<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
中,获取的与源代码中标签名的大小写一致
,使用的时候需要注意
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>
<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('需要获取的属性名'):
返回这个节点上面指定属性名对应的属性值
js
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:
获取文本节点包含的字符数
js
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('文本节点的内容'):
创建一个文本节点,当然,创建的内容中小于号、大于号或引号会被转义
,一般来说一个元素只包含一个文本子节点。不过,也可以让元素包含多个文本子节点
js
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:
8nodeName:
"#comment"nodeValue:
注释的内容parentNode:
Document 或 Element 对象子节点的类型:
没有
6.CDATASection类型
说明: 它表示XML
中特有的CDATA
区块,同时继承Text 类型
,因此拥有其拥有的方法,在真正的XML
文档中,可以使用document.createCDataSection()
并传入节点内容来创建CDATA
区块
CDATASection类型的节点的特征:
nodeType:
4nodeName:
"#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:
10nodeName:
文档类型的名称nodeValue:
nullparentNode:
Document 对象子节点的类型:
没有
html
<!DOCTYPE html PUBLIC "-// W3C// DTD XHTML 1.0 Strict// EN" "http://www.w3.org/TR/xhtml1/DTDxhtml1-strict.dtd" [<!ELEMENT name (#PCDATA)>
] >
js
document.doctype.name // html
document.doctype.publicId // -// W3C// DTD HTML 4.01// EN
document.doctype.systemId // "http://www.w3.org/TR/ html4/strict.dtd"
document.doctype.internalSubset // "<!ELEMENT name (#PCDATA)>"
8.DocumentFragment类型
说明: 是一种特殊的节点类型,它允许你在内存中创建一个文档片段,然后将其他节点附加到该片段中。文档碎片不是真实DOM树的一部分,因此对其进行操作不会触发页面重绘
,这样可以提高性能
并减少DOM操作的成本,它可以通过document.createDocumentFragment()
来创建
DocumentFragment类型的节点的特征:
nodeType:
11nodeName:
#document-fragmentnodeValue:
nullparentNode:
null子节点的类型:
Element、ProcessingInstruction、Comment、Text、CDATASection 或 EntityReference
html
<ul id="myList"></ul>
js
// 给ul添加三个li
let 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
js
// 假设需要向页面加入一个foo.js的脚本
let script = document.createElement("script");
script.src = "foo.js";
document.body.appendChild(script);
html
// 如果需要插入代码加载脚本
<script>
function sayHi() {
alert("hi");
}
</script>
js
// 通过DOM操作改写上面的HTML片段
let script = document.createElement("script");
script.appendChild(
document.createTextNode(
"function sayHi(){
alert('hi');
}"
)
);
document.body.appendChild(script);
2.动态样式
说明: 这个与上面是类似的,只不过加载的内容不同,一种使用<link>
引入外部文件,一种是用<style>
写样式代码,动态同样也是页面初始加载不存在,后面才加上去的
html
<!-- 假设加载一个styles.css的文件 -->
<link rel="stylesheet" type="text/css" href="styles.css">
js
// 通过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);
html
<!-- 另一种通过style加载css规则 -->
<style type="text/css">
body {
background-color: red;
}
</style>
js
// 使用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构造函数
并传入一个回调函数
来创建
js
// 这样就创建了一个观察者observer
let 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,默认值则为 falsecharacterDataOldValue:
当为 true 时,记录前一个被监听的节点中发生的文本变化。默认值为 false
js
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"类型的变化始终将这个属性设置为 nullattributeName:
对于"attributes"类型的变化,这里保存被修改属性的名字 其他变化事件会将这个属性设置为 nullattributeNamespace:
对于使用了命名空间的"attributes"类型的变化,这里保存被修改属性的名字 其他变化事件会将这个属性设置为 nulladdedNodes:
对于"childList"类型的变化,返回包含变化中添加节点的 NodeList 默认为空 NodeListremovedNodes:
对于"childList"类型的变化,返回包含变化中删除节点的 NodeList 默认为空 NodeListpreviousSibling:
对于"childList"类型的变化,返回变化节点的前一个同胞 Node 默认为 nullnextSibling:
对于"childList"类型的变化,返回变化节点的后一个同胞 Node 默认为 null
js
let observer = new MutationObserver(
(mutationRecords) => console.log(mutationRecords)
);
observer.observe(document.body, { attributes: true });
document.body.setAttribute('foo', 'bar');
js
// 连续更改
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()
这个方法,它会取消之前加入队列和之后加入队列
的回调函数,也就是停止观察
js
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属性观察
js
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
将其关联到新的节点将其重启
js
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
js
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
属性来设置白名单来进行过滤
js
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
js
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
js
// 假设需要交换两个子节点的位置
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
注意:
被观察子树中的节点被移出子树之后仍然能够触发变化事件。这意味着在子树中的节 点离开该子树后,即使严格来讲该节点已经脱离了原来的子树,但它仍然会触发变化事件
js
// 清空主体
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
上用,则只从当前元素后代中搜索
js
// 取得<body>元素
let body = document.querySelector("body");
// 取得 ID 为"myDiv"的元素
let myDiv = document.querySelector("#myDiv");
// 取得类名为"selected"的第一个元素
let selected = document.querySelector(".selected");
2.querySelectorAll()
说明: 这个与上面那个是相似的,只不过它会返回所有匹配到的元素
,说白了就是一个NodeList
对象,只不过这个对象是静态
的,在取值上面,可以通过for-of
、item()
方法或中括号语法
取元素
js
// 取得 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
js
// 检测body中是否存在class为page1的元素
if (document.body.matches("body .page1")){
}
六、HTML5
1.css类扩展
(1)getElementsByClassName()
说明: 它存在于document
对象和所有HTML元素上,它接受一个或多个类名组合而成的字符串,类名之间用空格隔开(类名的顺序无关紧要)
,在document
中调用则会返回文档中
所有匹配的元素,如果在某个元素
上调用则会返回它后代中
匹配的所有
元素
js
// 取得所有类名中包含"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,则删除;如果不存在,则添加。
html
<!-- 假设存在这样的节点 -->
<div class="bd user disabled">...</div>
js
// 获取这个节点
let divElement = document.getElementsByTagName("div");
js
// 通过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(" ");
js
// 通过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-
后面所有的字符拼接成一个字符串,这个字符串是小写
的来取值
html
<div id="myDiv" data-appId-MaMaMaMaMa="12345" data-myname="Nicholas"></div>
js
let div = document.getElementById("myDiv");
console.log(div.dataset);
6.标记
(1)innerHTML和outerHTML属性
说明: 在读取的时候,它会返回元素所有后代的HTML字符串
,在设置的时候,默认是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>
js
let divElement = document.getElementById("content");
console.log(divElement.innerHTML);
js
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
是关键字
js
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>
里面的样式
html
<!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()
只选择节点的后代
html
<!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
理解为结束的位置,包括偏移量这个位置
html
<p id="p1">
<b>Hello</b>
world!
</p>
js
// 假设需要选择从"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():
创建一个范围的副本,然后将这个副本返回,注意返回的不是节点
js
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();
js
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);
js
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(插入包含范围的节点):
与上面不同的地方在于它可以插入包含范围的节点,其它是一致的,这个一般用于高亮关键词
js
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);
js
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属性
,其返回值也是布尔值
,同时它也能检测两个节点是否是相邻的状态
相邻状态的判定:
在范围的上下文中,我们使用的是边界点和偏移量来定义范围的起点和终点。当范围中的两个节点相邻时,它们的边界点会非常接近,甚至可能在同一个位置上。这就导致了范围的起点和终点重叠。
html
<!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>
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():
把范围从创建它的文档中剥离,接触对范围的引用,便于垃圾回收将其处理