《JavaScript高级程序设计》第十二章,DOM2和DOM3。
事实上,我有点不太喜欢看DOM。
DOM1主要定义HTML和XML文档的底层结构(?),DOM2和DOM3则引入了更多的交互,分为几个模块:
- 核心:在DOM1核心基础上构建,给节点添加了更多方法/属性
- 视图:定义了基于样式的不同视图(?)
- 事件:说明了如何用事件与DOM文档交互
- 样式:定义了怎么通过编程来访问/改变CSS样式
- 遍历和范围:引入了新的遍历、选择DOM元素接口
- HTML:在DOM1级HTML基础上构建,添加了更多方法/属性/接口
可以通过调用hasFeature()
来判断浏览器是否支持以上模块。
1 | let supportDOM2Core = document.implementation.hasFeature("Core", "2.0"); |
下面就分模块来叙述,除了把事件放到下一章讲。
PS: 有一说一,这一篇我感觉自己不会想再看一遍…就当它是manual好了。
核心、视图、HTML
命名空间相关变化
XML的命名空间可以让不同XML文档的元素混在一起而不用担心冲突。不过,XHTML支持XML命名空间,但HTML不支持XML命名空间,所以下面的都是XHTML的例子。先举几个例子吧。
比如下面的例子里,命名空间用xmlns
来指定,也就是http://www.w3.org/1999/xhtml
。这里所有元素都默认是该命名空间的元素。
1 | <html xmlns="http://www.w3.org/1999/xhtml"> |
如果想显式地指定命名空间,可以在xmlns
处定义一个前缀,并把该前缀放到元素的tag
前面。如果想要把一些属性也限制在命名空间内的话,也把前缀加到属性名前面即可。
1 | <xhtml:html xmlns:xhtml="http://www.w3.org/1999/xhtml"> |
上面的例子都只用了一个命名空间,看上去没有什么必要,这里举一个混合两种语言的情况。
这里<svg>
元素及其所有子元素都属于它自身的命名空间http://www.w3.org/2000/svg
。虽然这个文档是XHTML
文档,但因为有命名空间,其中的SVG代码也是有效的。
1 | <html xmlns="http://www.w3.org/1999/xhtml"> |
那我们就会遇到一些问题了,譬如创建一个元素时,它会属于哪个命名空间呢?查询一个特殊标签名时,应该把结果包含在哪个命名空间呢?要解决这些,就要引出DOM2核心在DOM1核心基础上新增的一些与命名空间有关的属性/方法了。
Node类型
对于Dode类型,DOM2新增了和命名空间有关的属性。
localName
:不带前缀的节点名称namespaceURL
:命名空间URL或null
prefix
:命名空间前缀或null
举个例子,<html>
元素的localName
和tagName
都是"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)
:返回匹配元素的NodeListhasAttributeNS(namespaceURL, localName)
:判断元素是否有namespaceURL
命名空间的localName
特性removeAttributeNS(namespaceURL, localName)
:删除特性setAttributeNS(namespaceURL, qualifiedName, value)
:设置特性值setAttributeNodeNS(attNode)
:设置特性节点
* 这里的特性attribute指的是诸如id
、href
、name
等。
NamedNodeMap类型
由于特性是通过NamedNodeMap
表示的,所以NamedNodeMap
也新增了的命名空间有关的方法,不过以下方法只针对特性使用。
getNamedItemNS(namespaceURL, localName)
:取出项目removeNamedItemNS(namespaceURL, localName)
:移除项目setNamedItemNS(node)
:添加node,此节点已事先指定了命名空间信息
其他变化
DocumentType类型
publicId
systemId
internalSubset
(内部子集,HTML中用的少,XML中更常见)
1 |
1 | console.log(document.doctype.publicId); // "-//W3C//DTD HTML 4.01//EN" |
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 | let doctype = document.implementation.createDocumentType( |
Node类型
isSupported(特性名, 特性版本号)
:判断节点是否支持该特性isSameNode(node)
:判断两个节点是否为同一个对象isEqualNode(node)
:判断两个节点是否为一样的类型、有一样的属性setUserData(key, value, handler)
:给节点添加额外数据,设置的数据可通过getUserData(key)
取得。handler
参数为处理函数,会在带有数据的节点被复制、删除、重命名、引入一个文档时调用。其类型为function (operationType, key, value, sourceNode, targetNode)
,其中operationType
为1~4
四个数字,依次表示上述操作。
样式
访问元素样式
常规样式
元素通过style
设置的样式可以通过element.style
属性来获取(注意驼峰/连接符)。DOM2给这个style
添加了一系列属性和方法。
cssText
:style
特性中的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
导入的样式)title
:ownerNode
中title
属性值type
:样式类型,对于CSS样式来说是"type/css"
cssRules
:样式表中的样式规则集合ownerRule
:指向表示导入的规则(针对@import
导入的样式),或null
(针对其它)deleteRule(index)
:删除规则insertRule(rule, index)
:添加规则
也可以通过<link>
或<style>
元素的sheet
属性获取CSSStyleSheet
对象。
规则属性
CSSRule
是表示样式表中每一条规则的基类,最常见的是表示样式的继承类CSSStyleRule
(其它的规则有诸如@import
、@font-face
、page
、charset
),CSSStyleRule
有下面这些属性。
cssText
:整个规则文本parentRule
:当当前规则为导入的规则时,此属性为导入规则,否则为null
parentStyleSheet
:当前规则所属的样式表selectorText
:当前规则的选择符文本style
:CSSStyleDeclaration
对象,可以通过此设置/取得规则中特定的样式值type
:表示规则类型的常量值。对于样式规则,为1
举个例子
1 | div.box { |
1 | let sheet = document.styleSheets[0]; // 假设此规则位于页面第一个样式表中 |
规则方法
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>
最近的一个具有大小的元素。
要想知道某个元素在页面的偏移量,可以将自身和offsetParent
的offsetLeft
和offsetTop
递归累加直至根元素。
1 | function getElementLeft(element) { |
客户区
clientHeight
clientWidth
如上,也是只读的,滚动条占用的空间不计算在内。
滚动
scrollHeight
scrollWidth
scrollLeft
scrollRight
如上。有些元素(如<html>
)会自动地添加滚动条,有些元素则要通过overflow
属性设置才能滚动。
因为不同浏览器在对非滚动元素的数值定义不同,因此在确定文档的总高度的时候,必须取得scrollWidth
/clientWidth
和scrollHeight
/clientHeight
中的最大值,才能保证跨浏览器时得到精确的结果。
一个方法
每个元素都有一个getBoundingClientRect()
方法,它返回一个矩形对象,含四个属性:left
、top
、right
、bottom
。这些属性表示元素在页面中相对于视口的位置,不过不同浏览器对文档的原点
定义不同,因此略有差异。
遍历
DOM2定义了两个辅助完成DOM树遍历的类型:NodeIterator
、TreeWalker
,用它们可以基于给定起点进行DFS。
1 | let supportsTraversals = document.implementation.hasFeature("Traversal", "2.0"); |
NodeIterator
创建
创建一个迭代器的方法是document.createNodeIterator()
,有四个参数:
root
:搜索起点DOM节点whatToShow
:要访问的节点类型的数字代码filter
:NodeFilter
对象,过滤函数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 | // Both are OK |
操作
nextNode()
previousNode()
举一个遍历DOM树的例子:
1 | const div = document.getElementById("div1"); |
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
中终点的偏移量commonAncestorContainer
:startContainer
和endContainer
共同的祖先
用范围实现简单的选择
selectNode()
:选择整个节点包括子节点selectNodeContents()
:只选择子节点
1 |
|
1 | const range1 = document.createRange(); |
还有一些相对选择的方法:
setStartBefore(refNode)
:将范围起点设置在refNode
前,因此refNode
为范围选区的第一个子节点,此时:startContainer
:refNode.parentNode
startOffset
:refNode
在其父节点的childNodes
中的下标
setStartAfter(refNode)
:将范围起点设置在refNode
后,因此refNode
下一个同辈节点为范围选区的第一个子节点,此时:startContainer
:refNode.parentNode
startOffset
:refNode
在其父节点的childNodes
中的下标加一
setEndBefore(refNode)
:将范围终点设置在refNode
前,因此refNode
上一个同辈节点为范围选区的最后一个子节点,此时:endContainer
:refNode.parentNode
endOffset
:refNode
在其父节点的childNodes
中的下标
setEndAfter(refNode)
:将范围终点设置在refNode
后,因此refNode
为范围选区的最后一个子节点,此时:endContainer
:refNode.parentNode
endOffset
:refNode
在其父节点的childNodes
中的下标加一
用范围实现复杂的选择
setStart(refNode, offset)
:参数分别会变成startContainer
、startOffset
setEnd(refNode, offset)
:参数分别会变成endContainer
、endOffset
以上方法可以模拟selectNode()
和selectNodeContents()
,不过它们的特性在于可以选择节点内的一部分。
1 | const p1 = document.getElementById("p1"); |
操作范围
创建范围时,内部会为它创建一个文档片段(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 | const p1 = document.getElementById("p1"); |
* 看完这一章我人没了…看得头疼