0%

JavaScript学习笔记(12)

《JavaScript高级程序设计》第十二章,DOM2和DOM3。
事实上,我有点不太喜欢看DOM。


DOM1主要定义HTML和XML文档的底层结构(?),DOM2和DOM3则引入了更多的交互,分为几个模块:

  • 核心:在DOM1核心基础上构建,给节点添加了更多方法/属性
  • 视图:定义了基于样式的不同视图(?)
  • 事件:说明了如何用事件与DOM文档交互
  • 样式:定义了怎么通过编程来访问/改变CSS样式
  • 遍历和范围:引入了新的遍历、选择DOM元素接口
  • HTML:在DOM1级HTML基础上构建,添加了更多方法/属性/接口

可以通过调用hasFeature()来判断浏览器是否支持以上模块。

1
2
3
4
5
let supportDOM2Core = document.implementation.hasFeature("Core", "2.0");
let supportDOM3Core = document.implementation.hasFeature("Core", "3.0");
let supportDOM2HTML = document.implementation.hasFeature("HTML", "2.0");
let supportDOM2Views = document.implementation.hasFeature("Views", "2.0");
let supportDOM2XML = document.implementation.hasFeature("XML", "2.0");

下面就分模块来叙述,除了把事件放到下一章讲。

PS: 有一说一,这一篇我感觉自己不会想再看一遍…就当它是manual好了。

核心、视图、HTML

命名空间相关变化

XML的命名空间可以让不同XML文档的元素混在一起而不用担心冲突。不过,XHTML支持XML命名空间,但HTML不支持XML命名空间,所以下面的都是XHTML的例子。先举几个例子吧。

比如下面的例子里,命名空间用xmlns来指定,也就是http://www.w3.org/1999/xhtml。这里所有元素都默认是该命名空间的元素。

1
2
3
4
5
6
7
8
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Example XHTML page</title>
</head>
<body>
Hello world!
</body>
</html>

如果想显式地指定命名空间,可以在xmlns处定义一个前缀,并把该前缀放到元素的tag前面。如果想要把一些属性也限制在命名空间内的话,也把前缀加到属性名前面即可。

1
2
3
4
5
6
7
8
<xhtml:html xmlns:xhtml="http://www.w3.org/1999/xhtml">
<xhtml:head>
<xhtml:title>Example XHTML page</xhtml:title>
</xhtml:head>
<xhtml:body xhtml:class="home">
Hello world!
</xhtml:body>
</xhtml:html>

上面的例子都只用了一个命名空间,看上去没有什么必要,这里举一个混合两种语言的情况。

这里<svg>元素及其所有子元素都属于它自身的命名空间http://www.w3.org/2000/svg。虽然这个文档是XHTML文档,但因为有命名空间,其中的SVG代码也是有效的。

1
2
3
4
5
6
7
8
9
10
11
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Example XHTML page</title>
</head>
<body>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1"
viewBox="0 0 100 100" style="width:100%; height:100%">
<rect x="0" y="0" width="100" height="100" style="fill:red"/>
</svg>
</body>
</html>

那我们就会遇到一些问题了,譬如创建一个元素时,它会属于哪个命名空间呢?查询一个特殊标签名时,应该把结果包含在哪个命名空间呢?要解决这些,就要引出DOM2核心在DOM1核心基础上新增的一些与命名空间有关的属性/方法了。

Node类型

对于Dode类型,DOM2新增了和命名空间有关的属性。

  • localName:不带前缀的节点名称
  • namespaceURL:命名空间URL或null
  • prefix:命名空间前缀或null

举个例子,<html>元素的localNametagName都是"html";<s:svg>元素的localName"svg"tagName"s:svg"

对于Dode类型,DOM3新增了一些与命名空间有关的方法。

  • isDefaultNamespace(namespaceURL):判断URL是不是当前节点的默认命名空间
  • lookupNamespaceURL(prefix):返回命名空间
  • lookupPrefix(namespaceURL):返回前缀

Document类型

  • createElementNS(namespaceURL, tagName):创建元素
  • createAttributeNS(namespaceURL, attributeName):创建特性
  • getElementsByTagNameNS(namespaceURL, tagName):返回匹配元素的NodeList

Element类型

  • getAttributeNS(namespaceURL, localName):返回特性
  • getAttributeNodeNS(namespaceURL, localName):返回节点
  • getElementsByTagNameNS(namespaceURL, tagName):返回匹配元素的NodeList
  • hasAttributeNS(namespaceURL, localName):判断元素是否有namespaceURL命名空间的localName特性
  • removeAttributeNS(namespaceURL, localName):删除特性
  • setAttributeNS(namespaceURL, qualifiedName, value):设置特性值
  • setAttributeNodeNS(attNode):设置特性节点

* 这里的特性attribute指的是诸如idhrefname等。

NamedNodeMap类型

由于特性是通过NamedNodeMap表示的,所以NamedNodeMap也新增了的命名空间有关的方法,不过以下方法只针对特性使用。

  • getNamedItemNS(namespaceURL, localName):取出项目
  • removeNamedItemNS(namespaceURL, localName):移除项目
  • setNamedItemNS(node):添加node,此节点已事先指定了命名空间信息

其他变化

DocumentType类型

  • publicId
  • systemId
  • internalSubset(内部子集,HTML中用的少,XML中更常见)
1
2
3
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd"
[<!ELEMENT name (#PCDATA)>]>
1
2
3
console.log(document.doctype.publicId); // "-//W3C//DTD HTML 4.01//EN"
console.log(document.doctype.systemId); // "http://www.w3.org/TR/html4/strict.dtd"
console.log(document.doctype.internalSubset); // "[<!ELEMENT name (#PCDATA)>]"

Document类型

  • importNode(nodeToBeCopied, true | false)
    用来从一个文档中获取一个节点,再将其导入到另一个文档(如果只是用appendChild的话来自另一个文档会报错)。第二个参数表示是否要复制子节点,此方法参数和element.cloneNode()一致。
  • defaultView:一个指向拥有文档的窗口/框架的指针
  • implementation.createDocumentType(文档类型名称, publicId, systemId):创建新的DocumentType节点
  • implementation.createDocument(namespaceURL, 文档元素标签名, 新文档的文档类型):创建新文档
  • implementation.createHTMLDocument(文档标题):创建完整的HTML文档,包括<html><head><title><body>
1
2
3
4
5
6
7
8
9
10
11
12
13
let doctype = document.implementation.createDocumentType(
"html",
"-//W3C//DTD HTML 4.01//EN",
"http://www.w3.org/TR/html4/strict.dtd");

let doc = document.implementation.createDocument(
"http://www.w3.org/1999/xhtml",
"html",
doctype);

let htmldoc = document.implementation.createHTMLDocument("New Doc");
console.log(htmldoc.title); // "New Doc"
console.log(typeof htmldoc.body); // "object"

Node类型

  • isSupported(特性名, 特性版本号):判断节点是否支持该特性
  • isSameNode(node):判断两个节点是否为同一个对象
  • isEqualNode(node):判断两个节点是否为一样的类型、有一样的属性
  • setUserData(key, value, handler):给节点添加额外数据,设置的数据可通过getUserData(key)取得。handler参数为处理函数,会在带有数据的节点被复制、删除、重命名、引入一个文档时调用。其类型为function (operationType, key, value, sourceNode, targetNode),其中operationType1~4四个数字,依次表示上述操作。

样式

访问元素样式

常规样式

元素通过style设置的样式可以通过element.style属性来获取(注意驼峰/连接符)。DOM2给这个style添加了一系列属性和方法。

  • cssTextstyle特性中的CSS代码
  • parentRule:CSS信息的CSSRule对象
  • length:设置的CSS属性的数量
  • item(index):返回给定位置的CSS属性名称
  • getPropertyValue(propertyName):返回该属性对应值(一个字符串)
  • getPropertyCSSValue(propertyName):返回该属性对应值(一个CSSValue对象)
  • getPropertyPriority(propertyName):如果用了!important会返回"important",非则返回""
  • removeProperty(propertyName):删除属性
  • setProperty(propertyName, value, priority):设置属性

其中CSSValue是一个对象,含两个属性:cssText表示该属性值的字符串,cssValueType表示值的类型,是一个数值常量(0表示继承的值,1表示基本的值,2表示值列表,3表示自定义的值)。

计算样式

style只能得到通过style属性设置的样式,DOM2则增强了document.defaultView,提供了getComputedStyle(元素, 伪元素字符串 | null)来获取层叠的样式,该方法返回一个只读的CSSStyleDeclaration对象。

操作样式表

document.styleSheets是表示样式表的CSSStyleSheet类型,包含<link><style>中定义的样式,只读(除了一个例外),有以下属性/方法。

  • disabled:可读写(这个就是例外),表示样式表是否被禁用
  • href:当样式表是<link>包含的,则是样式表的URL,否则为null
  • media:当前样式表支持的所有媒体类型集合,即<link><style>中的media特性值字符串
  • ownerNode:指向拥有当前样式表的节点的指针,即某个<link><style>节点或null(针对@import导入的样式)
  • parentStyleSheet:指向导入样式表的指针(针对@import导入的样式)
  • titleownerNodetitle属性值
  • type:样式类型,对于CSS样式来说是"type/css"
  • cssRules:样式表中的样式规则集合
  • ownerRule:指向表示导入的规则(针对@import导入的样式),或null(针对其它)
  • deleteRule(index):删除规则
  • insertRule(rule, index):添加规则

也可以通过<link><style>元素的sheet属性获取CSSStyleSheet对象。

规则属性

CSSRule是表示样式表中每一条规则的基类,最常见的是表示样式的继承类CSSStyleRule(其它的规则有诸如@import@font-facepagecharset),CSSStyleRule有下面这些属性。

  • cssText:整个规则文本
  • parentRule:当当前规则为导入的规则时,此属性为导入规则,否则为null
  • parentStyleSheet:当前规则所属的样式表
  • selectorText:当前规则的选择符文本
  • styleCSSStyleDeclaration对象,可以通过此设置/取得规则中特定的样式值
  • type:表示规则类型的常量值。对于样式规则,为1

举个例子

1
2
3
4
5
div.box {
background-color: blue;
width: 100px;
height: 200px;
}
1
2
3
4
5
6
7
8
let sheet = document.styleSheets[0]; // 假设此规则位于页面第一个样式表中
let rules = sheet.cssRules || sheet.rules; // 获取规则列表
let rule = rules[0]; // 获取第一条规则
console.log(rule.selectorText); // "div.box"
console.log(rule.style.cssText); // 完整的CSS代码
console.log(rule.style.backgroundColor); // "blue"
console.log(rule.style.width); // "100px"
console.log(rule.style.height); // "200px"

规则方法

  • insertRule(选择符, 规则文本, 插入位置的索引):插入规则
    1
    sheet.insertRule("body", "background-color: silver", 0);
  • deleteRule(要删除的规则的位置):删除规则
    1
    sheet.deleteRule(0);

元素大小

这一节貌似和DOM2并无关系…没事,继续看看吧。

偏移量

  • offsetHeight
  • offsetWidth
  • offsetLeft
  • offsetRight

如上,它们都是只读的。offsetParent属性为包含元素,但不一定与parentNode相等。例如<td>offsetParent<table>而非<tr>,因为<table>是在DOM层次中距离<td>最近的一个具有大小的元素。

要想知道某个元素在页面的偏移量,可以将自身和offsetParentoffsetLeftoffsetTop递归累加直至根元素。

1
2
3
4
5
6
7
8
9
function getElementLeft(element) {
let actualLeft = element.offsetLeft;
let current = element.offsetParent;
while(current !== null) {
actualLeft += current.offsetLeft;
current = current.offsetParent;
}
return actualLeft;
}

客户区

  • clientHeight
  • clientWidth

如上,也是只读的,滚动条占用的空间不计算在内。

滚动

  • scrollHeight
  • scrollWidth
  • scrollLeft
  • scrollRight

如上。有些元素(如<html>)会自动地添加滚动条,有些元素则要通过overflow属性设置才能滚动。

因为不同浏览器在对非滚动元素的数值定义不同,因此在确定文档的总高度的时候,必须取得scrollWidth/clientWidthscrollHeight/clientHeight中的最大值,才能保证跨浏览器时得到精确的结果。

一个方法

每个元素都有一个getBoundingClientRect()方法,它返回一个矩形对象,含四个属性:lefttoprightbottom。这些属性表示元素在页面中相对于视口的位置,不过不同浏览器对文档的原点定义不同,因此略有差异。

遍历

DOM2定义了两个辅助完成DOM树遍历的类型:NodeIteratorTreeWalker,用它们可以基于给定起点进行DFS。

1
2
3
let supportsTraversals = document.implementation.hasFeature("Traversal", "2.0");
let supportsNodeIterator = (typeof document.createNodeIterator == "function");
let supportsTreeWalker = (typeof document.createTreeWalker == "function");

NodeIterator

创建

创建一个迭代器的方法是document.createNodeIterator(),有四个参数:

  • root:搜索起点DOM节点
  • whatToShow:要访问的节点类型的数字代码
  • filterNodeFilter对象,过滤函数
  • entityReferenceExpansion:布尔值,表示是否要扩展实体引用。此参数在HTML页面中没有用,因为其中的实体引用不能扩展

对于whatToShow,它的值以常量形式存在于NodeFilter类型中,稍微举个例子如下。如果想要多种类型,直接用|来或一下即可。

  • NodeFiler.SHOW_ALL
  • NodeFiler.SHOW_ELEMENT
  • NodeFiler.SHOW_ATTRIBUTE
  • NodeFiler.SHOW_TEXT

对于filter,这个NodeFilter对象只有一个方法acceptNode,当需要访问给定节点时返回NodeFilter.FILTER_ACCEPT,否则返回NodeFilter.FILTER_SKIP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Both are OK
let filter = function(node) {
return node.tagName.toLowerCase() == "p"
? NodeFilter.FILTER_ACCEPT
: NodeFilter.FILTER_SKIP;
}

let filter = {
acceptNode: function(node) {
return node.tagName.toLowerCase() == "p"
? NodeFilter.FILTER_ACCEPT
: NodeFilter.FILTER_SKIP;
}
}

操作

  • nextNode()
  • previousNode()

举一个遍历DOM树的例子:

1
2
3
4
5
6
7
const div = document.getElementById("div1");
const iterator = document.createNodeIterator(div, NodeFilter.SHOW_ELEMENT, null, false);
let node = iterator.nextNode();
while(node !== null) {
console.log(node.tagName);
node = iterator.nextNode();
}

TreeWalker

TreeWalker是高级版本的NodeIterator,它还提供了不同方向遍历DOM的方法。

创建

创建方法为document.createTreeWalker(),接收参数与NodeIterator相同。这里不同的是filter多了个不一样的选择NodeFilter.FILTER_REJECT

  • 对于NodeIterator,二者表现一致
    • FILTER_SKIP:仅跳过相应节点,前进到子树中下一节点
    • FILTER_REJECT:仅跳过相应节点,前进到子树中下一节点
  • 对于TreeWalker
    • FILTER_SKIP:仅跳过相应节点,前进到子树中下一节点
    • FILTER_REJECT:跳过相应节点及该节点的整个子树

TreeWalker还有一个属性叫currentNode,表示上一次遍历中返回的节点。可读写,设置它可以修改遍历继续进行的起点。

操作

  • parentNode()
  • firstChild()
  • lastChild()
  • nextSibling()
  • previousSibling()

范围

DOM2在Document类型中定义了createRange(),用它可以创建一个范围(Range类型),来实现对DOM树更精细的控制。Range类型有以下位置信息属性:

  • startContainer:包含范围起点的节点,即选取第一个节点的父节点
  • startOffset:范围在startContainer中起点的偏移量。若startContainer为文本/注释/CDATA节点,此为范围起点之前跳过的字符数量;否则为范围中第一个子节点的索引
  • endContainer:包含范围终点的节点,即选取最后一个节点的父节点
  • endOffset:范围在endContainer中终点的偏移量
  • commonAncestorContainerstartContainerendContainer共同的祖先

用范围实现简单的选择

  • selectNode():选择整个节点包括子节点
  • selectNodeContents():只选择子节点
1
2
3
4
5
6
<!DOCTYPE html>
<html>
<body>
<p id="p1"><b>Hello</b> world</p>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const range1 = document.createRange();
const range2 = document.createRange();
const p1 = document.getElementById("p1");

range1.selectNode(p1);
// startContainer: <body>元素
// endContainer: <body>元素
// commonAncestorContainer: <body>元素
// startOffset: 给定节点在父节点内的索引,即<p>在<body>中的索引,为1(因为把空格也算为一个节点了)
// endOffset: startOffset + 1,因为只选择了一个节点,为2

range2.selectNodeContents(p1);
// startContainer: <p>元素
// endContainer: <p>元素
// commonAncestorContainer: <p>元素
// startOffset: 因为范围从给定节点的第一个子节点开始,始终为0
// endOffset: 子节点的数量,为2

/*
* ⬇-------------range1-------------⬇
* <p id="p1"><b>Hello</b> world!</p>
* ⬆------range2-----⬆
*/

还有一些相对选择的方法:

  • setStartBefore(refNode):将范围起点设置在refNode前,因此refNode为范围选区的第一个子节点,此时:
    • startContainerrefNode.parentNode
    • startOffsetrefNode在其父节点的childNodes中的下标
  • setStartAfter(refNode):将范围起点设置在refNode后,因此refNode下一个同辈节点为范围选区的第一个子节点,此时:
    • startContainerrefNode.parentNode
    • startOffsetrefNode在其父节点的childNodes中的下标加一
  • setEndBefore(refNode):将范围终点设置在refNode前,因此refNode上一个同辈节点为范围选区的最后一个子节点,此时:
    • endContainerrefNode.parentNode
    • endOffsetrefNode在其父节点的childNodes中的下标
  • setEndAfter(refNode):将范围终点设置在refNode后,因此refNode为范围选区的最后一个子节点,此时:
    • endContainerrefNode.parentNode
    • endOffsetrefNode在其父节点的childNodes中的下标加一

用范围实现复杂的选择

  • setStart(refNode, offset):参数分别会变成startContainerstartOffset
  • setEnd(refNode, offset):参数分别会变成endContainerendOffset

以上方法可以模拟selectNode()selectNodeContents(),不过它们的特性在于可以选择节点内的一部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
const p1 = document.getElementById("p1");
const helloNode = p1.firstChild.firstChild;
const worldNode = p1.firstChild;
const range = document.createRange();

range.setStart(helloNode, 2);
range.setEnd(worldNode, 3);

/* 选了从"hello"的"llo"到"world!"的"o"所在范围
* ⬇--range-⬇
* <p id="p1"><b>Hello</b> world!</p>
* 01234 0123456
*/

操作范围

创建范围时,内部会为它创建一个文档片段(Fragment),其中的每个节点都是指向文档中相应节点的指针。我们应该保持选区的DOM格式有效,像上个例子选取了llo</b> wo就不算很好。不过范围本身知道自己缺少哪些开标签/闭标签,因此它会自己将它补全成<b>llo</b> wo,也会对范围外的标签进行补全(给He所在地方补上</b>),也就是最终结果为

1
<p id="p1"><b>He</b><b>llo</b> world!</p>

有几个方法可以对范围进行操作。以下都是range.xxx

  • deleteContents():删除范围所包含的内容
  • extractContents():删除范围所包含的内容,并将该片段返回
  • cloneContents():创建范围对象副本
  • insertNode(node):在范围选区开始处插入节点
  • surroundContents(node):环绕范围选区插入内容
  • collapse(true | false):折叠范围,true表示折叠到开始位置,false表示折叠到结束位置
  • compareBoundaryPoints():比较两个范围是否有公共的边界,如果没有的话哪个在前那个在后
  • cloneRange():复制范围
  • detach():解除范围对文档的引用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const p1 = document.getElementById("p1");
const helloNode = p1.firstChild.firstChild;
const worldNode = p1.firstChild;
const range = document.createRange();
range.setStart(helloNode, 2);
range.setEnd(worldNode, 3);

let span = document.createElement("span");

// <p id="p1"><b>He<span></span>llo</b> world!</p>
range.insertNode(span);

// <p id="p1"><b>He</b><span><b>llo</b> wo</span>rld!</p>
range.surroundContents(span);

// emoji为光标位置
// 原始范围: <p id="p1"><b>He🖱llo</b> wo🖱rld!</p>
// 折叠到开始位置:<p id="p1"><b>He🖱llo</b> world!</p>
// 折叠到结束位置:<p id="p1"><b>Hello</b> wo🖱rld!</p>
range.collapse(true);
console.log(range.collapsed); // true

// PS: collapsed还有个妙用,用来确定范围中两个节点是否紧密相邻
// 比如HTML为:<p id="p1">p1</p><p id="p2">p2</p>
const p1 = document.getElementById("p1");
const p2 = document.getElementById("p2");
range = docuemnt.createRange();
range.setStartAfter(p1);
range.setStartBefore(p2);
console.log(range.collapsed); // true

* 看完这一章我人没了…看得头疼