ECMAScript5 考古(一)—— DOM
什么是DOM
DOM
(Document Object Model)文档对象模型,是针对HTML和XML文档的一个API。DOM
描绘了一个层次化的节点树,允许开发人员添加、移除和修改页面。DOM脱胎于Netscape及微软公司创始的DHTML(动态HTML),但现在它已经成为了表现和操作页面标记的真正跨平台、语言中立的方式。1998年10月,DOM1
级规范成为了W3C的推荐标准,为基本的文档结构及查询提供了接口。
DOM中的节点层次
DOM
可以将任何HTML或者XML文档描绘成一个多层节点构成的结构。
一个HTML文档有一个文档节点,可以理解为我们熟知的document
对象对应的节点。
在这段代码中,我们看到文档节点有html
这个子节点,这样的html元素我们称之为文档元素。
ECMAScript中的Node类型
上面一大堆层次、节点之类的名词,实际上大家都听过很多次了,HTML的文档树是我们非常熟悉并且经常操作的,现代web技术中提供了很多框架、库让大家去操作这些元素。那么这些框架或者库的底层原理是什么呢?这才是我们要探究的。
在DOM最底层,定义了一个叫做Node
的类型,这个类型将表示出DOM中所有的元素,无论是HTML元素还是嵌在HTML中的文档。除了IE之外,其他浏览器都可以访问到这个类型。所有的节点类型都继承自Node类型。
每个节点都有一个nodeType属性,用于表明节点的类型。节点类型由Node类中定义的下列12个数值常量表示,任何节点类型都会是其中之一:
- ELEMENT_NODE(1)
- ATTRIBUTE_NODE(2)
- TEXT_NODE(3)
- CDATA_SELECTION_NODE(4)
- ENTITY_REFERENCE_NODE(5)
- ENTITY_NODE(6)
- PROCESSING_INSTRUCTION_NODE(7)
- COMMENT_NODE(8)
- DOCUMENT_NODE(9)
- DOCUMENT_TYPE_NODE(10)
- DOCUMENT_FRAGMENT_NODE(11)
- NOTATION_NODE(12)
要查看一个节点是什么类型的节点,只要访问它的nodeName和nodeType,nodeName保存元素的标签名(文本文档的标签名为#text);而nodeType上述的不同类型的值。
节点之间的关系
对前端编程有一定了解的同学一定知道节点之间会有父子,或者兄弟的关系。在前端编程的时候这些关系对我们的代码逻辑将产生至关重要的作用。例如如果很难访问一个不带任何属性的元素,那我们不妨可以对它的兄弟或者父母下手,通过它的至亲获取它的引用。
孩子节点
在文档的Node类型中,DOM定义了一个childNodes
的属性,里面保存的是一个NodeList
对象。这是一个类似于数组的对象,然而,它并不是Array的一个实例。不相信的同学可以行在浏览器的控制台实践一下。输入如下的代码
// a是一个html的div元素
a.childNodes instanceof Array
// 结果为false
另外,我们在遍历这个NodeList对象时候要注意,它是个动态的对象,即是一个时时刻刻都在变化的对象。如果我们对它不断插入元素的话,有可能会造成死循环。
a.forEach(ele => {
const temp = document.createElement('p')
a.push(temp);
});
除此之外,它的其他操作跟数组类似。可以通过下标访问,可以访问length属性获取元素数量。如果觉得操作这样的一个奇怪的列表不方便,也可以通过Array.prototype.slice.call(someNode.childNodes, 0);
转化为数组再操作。
双亲节点和兄弟节点
除了孩子节点之外,一个节点还有双亲节点,即我们熟悉的parentNode
属性,即指向包着这个元素的节点。在有了双亲节点之后,我们也可以定义出兄弟节点——具有相同双亲节点的其他节点,用nextSibling
获取比自己小的兄弟(在自己之后的兄弟),用previousSibling
获取比自己大的兄弟节点。
操作节点
在有了亲人节点之后,我们能够在理论上定位所有节点。这时DOM还给用户提供了许多操作节点的方法,简直是如虎添翼。其中我们比较常用的Node类型定义的操作方法是appendChild
,即向自己的子节点中添加一个节点,成为childNodes中最后一个元素
这里要注意的是,在DOM中,同一个节点(相同引用)不能出现在不同的位置。如果将父节点的第一个子节点append进父节点中,它将会变成父节点的最后一个节点。
其他常用的API还有:
- replaceChild(newNode, childNode)
- removeChild(childNode)
- cloneNode(true | false)执行深/浅复制
Document类型
可以理解为我们熟知的document对象。nodeType为DOCUMENT_NODE(9),nodeName是#document
。它的子节点是html
元素。常用属性有:
-
body
属性,默认指向标签
-
doctype
属性指向 -
title
指向网页的title -
URL
指向网页完整的url。e.g https://www.google.com -
domain
指向域名 - 访问元素的方法:
getElementById
,getElementsByTagName
-
anchors
返回文档中带有name特性的 -
images
返回文档中所有的标签
-
links
返回文档中所有带有href
属性的
这些都是我们经常用到的属性和方法。由于大家都比较熟悉这些方法,因此不多做介绍。唯一要注意的是domain
属性。域名信息有松散(loose)和紧绷(tight)区别,即google.com
和www.google.com
的区别。如果我们对domain
先赋予一个松散的域名,再赋予一个紧绷的域名,这样可能会导致错误。
document.domain = 'google.com';
document.domain = 'www.google.com'; // 报错
Element类型
这依然是我们非常熟悉的一个类型,在前端编程中操作的主体就是HTMLElement,也就是Element类型的节点。它的NodeType为1,NodeName为标签名。需要注意的是,Element类型的子节点不只是其他Element节点,还可能是Text,Comment节点等。
HTMLElement
HTMLElement是Element类型的一种,即继承了Element类型的所有属性,并且加上了一些特别的属性,比如:
- id
- title
- lang
- dir
- className
Element类型的属性(attribute)
在一段HTML代码中,我们经常会给一些元素赋予属性,如width, name, id等信息。而这些属性对于我们标记元素,或者控制代码逻辑起着至关重要的作用,因此我们得想办法获得它们。DOM给了我们这样的方法:getAttributes(attr)
,通过传入属性的键,获取属性的值。而一些特殊的元素属性如id, class等DOM还给它们设置成了对象属性,比如id,class等,我们只需要访问ele.id
或者ele.className
就可以得知当前元素的id或者class。
能取得元素属性,我们当然也能设置元素的属性。DOM“工具箱”给我们提供的是setAttribute(attr, val)
方法。另外还有removeAttribute(attr)
删除元素属性。
Element类型的创建
这时候肯定是要提到当年在手撸原生JS时常用到的document.createElement(tagName)
啦。这是个动态创建新Element节点的方法,创建之后的节点对象,通过前文提到过的appendChild
方法加入到某个容器中。
当然也有人会用不清真的innerHTML
属性,直接通过插入新的HTML的方法创建子元素。当然也是一种可行的方法,但如果继续看下去就会知道innerHTML为什么在这里不提倡使用。
TEXT类型
HTML代码中另一个十分常见的节点类型是文本节点。简单来说就是嵌套在各种标签中的文字。它的nodeName为#text
,nodeValue
为文本中的文字。这里要注意的是,在之前介绍过的节点类型中,文本节点是第一个拥有nodeValue
属性的节点。而且,作为嵌套在标签中的节点,它的parentNode
是一个Element节点。
创建文本节点
document.createTextNode(nodeValue)
创建一个文本节点,接受一个文本节点文本内容的字符串参数。返回一个新的文本节点,然后通过appendChild方法加入到HTML中。
由于节点的parentNode只有一个,如果取得了一个在HTML中的文本节点,并将它加入到一个标签中,之前的标签中的文本节点会消失。
Comment类型
nodeType
是8,nodeName
是#comment
,nodeValue
是注释的内容。parentNode
是Document或者Element。可以用普通的.nodeValue
属性取得注释内容,也可以通过data
属性取得注释的内容。
Attr类型
元素的特性在DOM中用Attr
类型表示。nodeType
的值是2,nodeName
的值是特性的名字,nodeValue
的值是特性的值。在HTML中,它没有子节点,也没有双亲节点。在一般理解中,特性类型的节点不在DOM树中,尽管他们实实在在存在,但我们并不用十分在意(因为无论是parentNode还是childNodes都无法访问他们)。开发人员一般用getAttribute(), setAttribute(), removeAttribute()
对Attr节点进行访问。这里就不过多赘述了。
DOM的操作
动态样式
动态插入一个样式表。
var link = document.createElement('link');
link.rel = 'stylesheet';
link.href = 'style.css';
link.type = 'text/css';
var head = document.getElementsByTagName('head')[0];
head.appendChild(link);
style标签中的样式插入
var style = document.createElement('style');
style.appendChild(document.createTextNode('body{width:10px;}'));
document.getElementsByTagName('head')[0].appendChild(style);
// 在IE中会报错,直接用style.cssText插入
操作表格
创建表格
/*一般方法*/
var table = document.createElement('table');
table.border = 1;
table.width = 100%;
// 创建tbody
var tbody = document.createElement('tbody');
table.appendChild(tbody);
// 创建第一行
var row1 = document.createElement('tr');
tbody.appendChild(row1);
var cell1_1 = document.createElement('td');
cell1_1.appendChild(document.createTextNode('cell1_1'));
row1.appendChild(cell1_1);
var cell1_2 = document.createElement('td');
cell1_2.appendChild(document.createTextNode('cell1_2'));
row1.appendChild(cell1_2);
DOM为table、tbody、tr添加了API:
- table的API
- tFoot、tHead、tBodies保存着table中几个元素的指针
- rows保存了表格中的所有行。返回HTMLCollection对象。
- createTHead,createTFoot创建tFoot、tHead
- deleteRows(pos)删除某一行
- insertRows(pos)向rows集合中制定位置插入一行。
- rows中的API
- 是deleteRow(pos)
- insertRow(pos)
- cells保存着tr元素中的HTMLCollection
- deleteCell(pos)删除指定位置的cell
- insertCell(pos)向cells集合中插入一个单元格。
nodeList的遍历
nodeList是一个动态的对象。如果对一个nodeList进行如下遍历
for (var i = 0; i < somenodes.length; ++i) {
var div = document.createElement('div');
somenodes.push(div);
}
可以先用一个len
对nodeList
进行快照,再通过迭代遍历。
总结
本文主要介绍了DOM为了操作和解析HTML提供了的API。在HTML文本中,所有的元素都可以看成是node节点,由这些node节点的相互关系构成了能够解析HTML的DOM树。并且node为了方便操作而提供了许多修改、读取信息的API。总之node类型是页面解析和渲染的基础,这为我们后面继续探讨ES5的相关特性打下了基础。