DOM1将HTML和XML文档形象地看作一个层次化的节点树,可以使用js来操作这个节点树,进而改变底层文档的外观和结构定义了一个Node接口,该接口将由DOM中的所有节点类型实现,每个节点都有nodeType属性,用于表示节点的类型.一共有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)
以上属性可以通过Chrome控制台方便查看
我们可以使用someNode.nodeType==Node.xxx
的方式来确定节点的类型,但是更兼容的写法是直接使用数字,因为IE没有公开Node的构造函数.
每个节点都有一个childNodes
属性,其中保存者NodeList.在这里我们需要注意尽管NodeList具有length属性,但是他实际上不是数组.要想将NodeList转换为数组应该采用以下的函数:
'use strict'
let nodeList2Array = nodes => {
let arr = null
try {
arr = Array.prototype.slice.call(nodes)
} catch (e) {
// IE8之前NodeList不是js对象而是COM对象
arr = []
for (let i = 0; i < nodes.length; i++)
arr.push(nodes[i])
}
return arr
}
每个节点都有一个
ownerDocument
属性,该属性为文档节点.任何节点都可以通过someNode.ownerDocument
直接访问文档节点而无需层层回溯.
Document类型
js通过Document类型表示文档.在浏览器中document
是HTMLDocument
(继承自Document)的一个实例,表示整个页面.因为document
是window
上的一个属性,因此可作为全局对象访问.其中document.documentElement
可以取得整个html文档,而document.body
则保存了对body
的引用,这是两个比较常见的方法.
思考:为什么我们没有看到对document类型的节点进行DOM的增删操作?
答:document类型的节点是只读的,代表整个文档树,有且仅有一个子节点.
Tips:
除了这些,document还有一些关于http的东西例如:
document.URI
,document.referrer
,document.domain
.其中只有document.domain
是可写的,需要注意的是新设置的值必须是父域.
//假设页面来自 p2p.wrox.com 域
document.domain = "wrox.com"; // 成功
document.domain = "nczonline.net"; // 出错
//假设页面来自于 p2p.wrox.com 域
document.domain = "wrox.com"; //松散的(成功)
document.domain = "p2p.wrox.com"; //紧绷的(出错!)
当页面中包含来自其他子域的框架或内嵌框架时,能够设置 document.domain 就非常方便了。由
于跨域安全限制,来自不同子域的页面无法通过JavaScript通信。而通过将每个页面的document.domain设置为相同的值,这些页面就可以互相访问对方包含的JavaScript对象了。例如,假设有一个页面加载自www.wrox.com,其中包含一个内嵌框架,框架内的页面加载自p2p.wrox.com。由于document.domain字符串不一样,内外两个页面之间无法相互访问对方的JavaScript对象。但如果将这两个页面的document.domain值都设置为"wrox.com",它们之间就可以通信了。
取得DOM元素
document.getElementsByTagName()
函数接收一个字符串参数,返回结果是HTMLCollection
,该对象和NodeList
非常像似,可以使用数组下标和item(index)
方法来访问.除此之外它还提供了一个按名访问的方法namedItem(key)
:
<img src="" name="img">
<script>
var images = document.getElementsByTagName('img')
var img = images.namedItem('img')
img = images['img']
</script>
总结起来,索引访问会调用item(index)
;按名访问会调用namedItem(key)
方法.
当我们想要取得所有的元素的时候只需要传递*
即可:
var elements = document.getElementsByTagName('*') // get all element
document
上还有一些特殊的集合.document.forms
取得所有表单;document.images
取得所有图片;document.links
取得所有的链接.
Element元素
为DOM元素添加自定义的属性会失败,例如:
var div = document.getElementById('demo')
div.aaa = 1
var t = div.getAttribute('aaa') // null
attributes
属性
attributes
属性中包含一个NamedNodeMap
,通常仅用于序列化DOM结构.例如:
let outputAttributes = element => {
let pairs = []
for (i = 0; i < element.attributes.length; i++) {
let attrName = element.attributes[i].nodeName
let attrValue = element.attributes[i].nodeValue
if (element.attributes[i].specified)
pairs.push(attrName + "=\"" + attrValue + "\"")
}
return pairs.join(" ")
}
IE7及更早的版本会返回HTML元素中所有可能的特性,包括没有指定的特性。换句话说,返回100多个特性的情况会很常见。每个特性节点都有一个名为specified
的属性,这个属性的值如果为true
,则意味着要么是在HTML
中指定了相应特性,要么是通过setAttribute()
方法设置了该特性。
Text节点
文本节点只可能是元素节点的子节点,一般来说文本节点只有一个,但是我们是可以将多个文本节点追加到一个元素节点的.
var div = document.createElement('div')
var textNode1 = document.createTextNode('content1')
var textNode2 = document.createTextNode('content2')
div.appendChild(textNode1)
div.appendChild(textNode2)
document.body.appendChild(div)
console.log(div.childNodes.length) // 2
DOM文档中存在相邻的同胞文本节点很容易导致混乱,因为分不清哪个文本节点表示哪个字符串,normalize()
方法是专门解决此问题的.
再上面的程序中增加以下的代码:
div.normalize()
console.log(div.childNodes.length) // 1
console.log(div.firstChild.nodeValue) // content1content2
需要注意的是在浏览器渲染的时候永远不会出现兄弟文本节点.
相反地,还有一个splitText()
方法用于分割文本节点.
var div = document.createElement('div')
var textNode = document.createTextNode('content1 content2')
div.appendChild(textNode)
document.body.appendChild(div)
var newNode = div.firstChild.splitText(9)
console.log(div.firstChild.nodeValue) // content1
console.log(newNode.nodeValue) // content2
console.log(div.childNodes.length) // 2
分割文本节点是从文本节点中提取数据的一种常用DOM解析技术。
理解NodeList,NamedNodeMap和HTMLCollection
首先,这三个集合都是“动态的”,也就是说:每当文档结构发生变化时,它们都会得到更新。例如以下的代码将导致死循环.
var divs = document.getElementsByTagName('div')
for (var i = 0; i < divs.length; i++) {
var div = document.createElement('div')
document.body.appendChild(div)
}
console.log('done')
解决方法是先将divs.length缓存起来,即仅仅初始化一次,以上的for循环改为下面的:
for(var i = 0,len = divs.length;i < len;i++)
DOM拓展为了性能的考虑,我们应该尽量少直接访问此类动态的实时性的集合,因为每次都会进行文档查询,最好将其缓存起来使用.这也就是为什么有些框架提倡尽量不要使用DOM操作的原因.
Selector API
最常用的是querySelector和querySelectorAll,前者返回匹配选择器的第一个DOM元素,后者返回匹配的NodeList.除此之外还有matchsSelector
用于判断某个元素是否匹配特定选择器(各个浏览器有它自己的私有实现).我们可以自己编写以下的函数用于兼容.
function matchesSelector(element, selector) {
if (element.matchesSelector) {
return element.matchesSelector(selector);
} else if (element.msMatchesSelector) {
return element.msMatchesSelector(selector);
} else if (element.mozMatchesSelector) {
return element.mozMatchesSelector(selector);
} else if (element.webkitMatchesSelector) {
return element.webkitMatchesSelector(selector);
} else {
throw new Error("Not supported matches selector api");
}
}
元素遍历
为了解决IE9及其之前版本由于空格的存在导致不会返回空白文本节点从而造成childNodes
属性不一致的问题,提出了元素遍历api:
- childElementCount :返回子元素(不包括文本节点和注释)的个数。
- firstElementChild :指向第一个子元素; firstChild 的元素版。
- lastElementChild :指向最后一个子元素; lastChild 的元素版。
- previousElementSibling :指向前一个同辈元素; previousSibling 的元素版。
- nextElementSibling :指向后一个同辈元素; nextSibling 的元素版。
使用上面的api不用考虑空白文本节点的问题了:
// 以前的写法
var child = element.firstChild;
while (child != element.lastChild) {
if (child.nodeType == 1) { //检查是不是元素
processChild(child);
}
child = child.nextSibling;
}
// 现在的写法
var child = element.firstElementChild;
while (child != element.lastElementChild) {
processChild(child); //已知其是元素
child = child.nextElementSibling;
}
html5对DOM的扩充
- 原生的通过类名获取元素的方法
getElementsByClassName()
,其中的参数可以传递多个类名,用空格分开. - 类名的集合
classList
提供了add
,remove
,contains
,toggle
(存在删除,没有添加)方便类的添加删除切换.避免了每次操作className
这个单一的字符串.例如,一个有多个类,我们需要删除red
这个类,方法可能是这样:
// 以前的做法
var classNames = div.className.split(/\s+/) // 类名拆分成数组
var pos = -1 // 保存待删除的类在数组中的索引
for(var i = 0;i < classNames.length;i++){
if(classNames[i] === 'red'){
pos = i
break
}
}
classNames.splice(i,1)
div.className = classNames.join(' ')
// 现在的做法
div.classList.remove('red')
- 原生的获取当前焦点元素
document.activeElement
.当页面加载完成后默认的焦点元素是document
,文档加载中的时候此值为null
- HTMLDocument新增加了
readyState
属性表示页面的加载情况,取值有loading
和complete
.这样window.onload
就可以使用document.readyState === 'complete'
来代替了. - html5将标准模式和混杂模式纳入了标准.
document.compatMode
的取值:CSS1Compat(标准),BackCompat(混杂). - 可以使用
document.head
取得head的引用,在此标准之前可以使用document.getElementsByTagName('head')[0]
兼容. - 可自定义
data
属性,通过dataset
来取得KV映射. - 新的滚动api,
scrollIntoView
.
outerHTML
该属性和innerHTML略有区别.在读模式下会返回元素本身和它的子树;在写模式下会替换子树和它本身.举个栗子,有以下的DOM结构:
<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>
var oDiv = document.getElementById('content');
console.log(oDiv.outerHTML);
结果:
<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>
还是以上的DOM
oDiv.outerHTML = '<p>This is a paragraph.</p>'
DOM树将变为:
<p>This is a paragraph.</p>