DOM是针对HTML和XML文档的一个API(应用程序编程接口)。
它具有跨平台和语言中立的特点。
DOM将HTML和XML描述成一个多层节点构成的结构。
每个节点都有不同的特点和表现特征。
不同的节点之间具有一定的层级关系(父子、祖先和兄弟)。
文档节点是每个DOM的根节点,被称为文档元素,而每个DOM只能有一个文档元素。
Node类型
每个文档节点都可以看作一个Node,JavaScript中通过Node类实现了DOM中的文档节点。
Node一共有12种类型,DOM中的任何节点必定属于这些类型中的某一种。
- 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);
我们常用的类型一般是 1(元素节点) 和 3(文本节点)。除了这两种外,还可以再了解一下类型9(文档节点)。在JavaScript中,
document.nodeType === 9
。
跟节点关系相关的一些属性
- childNodes:表示该节点下的子节点列表,它是一个伪数组,并且动态获取的。关于动态获取的意思,举例说明。
<html>
<head>
<title>Sample Page</title>
</head>
<body>
<p>Hello World!</p>
</body>
</html>
body节点的childNodes是节点p,假如有如下代码:
var sons = document.body.childNodes;
console.log(sons.length); // 1
// 如果我们动态给body添加一个子节点
document.body.appendChild(document.createElement('div'));
// 那么sons.length = ?
console.log(sons.length);
// sons.length === 2;
- nextElementSibling / previousElementSibling:两个属性分别表示当前节点的下一个兄弟节点和上一个兄弟节点
- parentNode:每个节点都有一个parentNode属性,指向其父节点;
- lastChild / firstChild: 分别指向childNodes中的最后一个和第一个元素。
- hasChildNodes:这个方法在节点包含一或多个子节点的情况下返回 true
- ownerDocument:该属性指向表示整个文档的文档节点
操作节点
- appendChild:向childNodes中追加一个新的节点;
- insertBefore: 向childNodes中的某个特定位置追加一个新的节点;
- replaceChild:替换childNodes中的某个节点
- removeChild:删除childNodes中的某个节点
- cloneNode:复制当前节点
- normalize: 可以理解为将当前节点下的文本节点排版进行规范性质的优化,例如将相邻的文本节点合并为一个文本节点
Document类型
Document的子节点可以是DocumentType、Element、ProcessingIn-struction或Comment。
- DocumentType: 文档类型,指HTML文件最开始那一行文档版本说明;<!DOCTYPE html>
是一种
- Element: 文档元素,HTML中的html
, body
, div
等之类
- ProcessingIn-Struction: 处理指令,给页面解析器提供额外的信息
- Comment: 文档中的注释; <!--comment info -->
之类
Document中的一些常见属性
- documentElement : 该属性始终指向HTML页面中的
<html>
元素 - doctype: 表示文档类型,该值有可能为空,在页面没有定义文档类型的情况下;不同的浏览器对doctype支持不太一致
- 注释类型: 理论上讲,在html元素外面的注释也算是Document的子节点,but,浏览器实际实现情况却不太一致。不过也可以理解,毕竟是注释,可能应用场景的确太少;
Document 的一些文档信息
- title: 文档标题; 这个属性的值跟
<title>
元素中文本一样,是显示在浏览器标题栏或标签页的信息; - URL: 页面完整的URL
- domain: 只包含当前页面的域名
- referrer: 链接到当前页面的那个页面的URL
注意domain
属性,由于跨域安全问题的设置,并不是可以给domain属性设置任何值。
元素查找
getElementById
,getElementsByTagName
分别通过元素id 和元素名称进行元素查找,这俩方法被所有的浏览器支持,但需要注意的是,低版本的IE(IE7-)对具体细节可能有第二种解释。
一些特殊的集合
- document.anchors: 返回文档中所有的带name特性的
<a>
元素; - document.applets: 返回文档中所有的
<applet>
元素,注意applet
元素不再 推荐使用; - document.forms: 返回文档所有的
<form>
元素; - document.images: 返回文档中所有的
<img>
元素; - document.links: 返回所有带href特性的
<a>
元素;
文档写入功能
四个方法:write
, writeln
, open
, close
write()
和writeln()
, 都会将文本原样地写入文档当中,不同的是writeln
会在文本末尾添加一个换行符。但是这在页面展示上似乎并没有什么明显差异(因为HTML中 除了会导致换行的元素外,<br>
才会产生换行,普通文本换行只是在文档中换行,但是解析后结果一样)。
document.write('<p> current time is <strong>: ' + new Date() + '</strong> </p>');
document.writeln('<p> current time is <strong>: ' + new Date() + '</strong> </p>');
PS:如果在文档加载结束后(load完成)再调用 document.write(),那么输出的内容将会重写整个页面。
方法 open()
和 close()
分别用于打开和关闭网页的输出流。
下面解释下,为什么在文档加载结束后,调用write()/writeln()
方法会覆盖页面内容
在页面加载完成后,调用
write()/writeln()
,会自动触发document.open()方法,而document.open,会擦除原有的文档内容。那么在文档加载期间调用write()/writeln()
之所以不会覆盖页面内容,是因为此时文档处于open状态,不需要调起document.open()方法,因而不会复写文档内容。当调用document.open打开文档写入流后,记得调用document.close()方法关闭写入流。如果在调用document.close()方法后,再调用write()/writeln()
方法,就会覆盖之前写入的内容。
document.open();
document.write('<p>The First write</p>');
document.write('<p>不会覆盖上一行代码写入的数据</p>');
document.close();
document.write('<p>会覆盖之前两行写的数据</p>');
document.close();
Element类型
文档中的Element是除Document外,用的最多的类型了。一般情况下文档中的元素都会有如下几个属性:
- nodeType: Element类型的节点,noteType都是1
- nodeName: 元素的标签名
- nodeValue: 元素的nodeValue都是null
- parentNode: 元素的上级节点,可能是Document,也有可能是另一个Element
- Element的子节点可能是Element,Text, Comment等等
HTML元素的基本特性
- id: 元素在文档中的唯一标识符
- title: 有关元素的附加信息说明,一般通过工具提示条显示出来
- lang: 元素内容的语言代码,
- dir: 语言的方向, 值为‘ltr’ (left-to-right, 从左至右)‘rtl’(right-to-left, 从右至左)
- className: 对应元素的class属性
元素属性操作
getAttribute()
,setAttribute()
,removeAttribute()
三个方法接受的第一个参数都是特性名称(注意,特性名不区分大小写)。
var body = document.body;
body.newattr = 'test';
// 我们可以这样为元素添加自定义的新特性,但是我们无法用getAttribute()方法取到它的value
console.log(body.getAttribute('newattr')); // undefined
// 只有通过setAttribute()方法添加的自定义特性,才可以通过getAttribute()方法取到
body.setAttribute('myattr', 1);
console.log(body.getAttribute('myattr')); // '1'
注意,我们可以通过body.getAttribute('id')
来获取元素特性,也可以通过body.id
同样取得该值。这是元素本身定义好的。但存在一些特殊的特性,我们通过这两种方式,取到的值也许是不一样的。比如:
var body = document.body;
console.log(body.style); // 输出一个对象 (引用类型)
console.log(body.getAttribute('style')); // 输出结果是一个字符串类型
attributes 属性
记住,Element类型是使用attributes属性的唯一一个DOM节点类型。
attributes属性中包含了一系列属性节点,每个节点都对应元素的一个特性。
attributes是一个NamedNodeMap对象,有以下几个方法:
- getNamedItem(name)
: 获取属性节点
- removeNamedItem(name)
: 删除属性节点
- setNamedItem(node)
: 设置属性节点
- item(index)
: 根据index,返回相应的节点
创建元素
createElement()
元素的子节点
先上一段html代码
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
那么<ul>
元素有几个子节点呢?
IE浏览器认为是3个(3个<li>
); 而其它浏览器认为是7个(3个<li>
和4个文本节点(<li>
元素前后的空白符))
console.log(ul.childNodes); // 下面是Chrome 67下的输出结果
// NodeList(7) [text, li, text, li, text, li, text]
如果不想要文本节点,那么只能这么写
<ul><li>1</li><li>2</li><li>3</li></ul>
文本节点
文本节点由 Text 类型表示,包含的是可以照字面解释的纯文本内容。文本可以包含转义后的HTML字符串(<,> 等)
Text 类型的一些常用特性
- nodeType === 3;
- nodeName === ‘#text’;
- nodeValue === nodeData , 节点包含的纯文本字符串;
- parentNode 是一个 Element;
- 没有子节点
Text类型几个操作节点的方法
- appendData(text):给文本节点追加内容text ;
- deleteData(offset, count): 从offset开始,删除长度为count的字符串;
- insertData(offset, text): 在offset位置插入字符串text;
- replaceData(offset, count, text): 在offset位置将长度为count的字符串替换为text;
- splitText(offset):在offset位置切割文本节点,并返回一个新的文本节点,原文本节点只保留offset之前的内容;
- substringData(offset, count):从offset位置开始截取长度为count的内容,原文本节点没有变动;
var t = document.createTextNode('111');
t.nodeType; // 3
t.nodeName; // '#text'
t.nodeValue; // '111'
t.nodeData; // '111'
t.deleteData(1,1);
t.nodeValue; // '11'
t.insertData(2, '234');
t.nodeValue; // '11234'
t.replaceData(0, 1, '')
t.nodeValue; // '1234'
var ts = t.splitText(2);
ts.nodeValue; // '34'
t.nodeValue; // '12'
t.substringData(1,1); // '2'
normalize()方法
正常情况下,一个Element中永远不会存在相邻的文本节点。浏览器如果碰见相邻的文本节点,会合并成一个。
但也不排除存在两个相邻节点的情况————人工操作。
var el = document.createElement('div');
var t = document.createTextNode('hello world\n');
el.appendChild(t);
console.log(el.childNodes.length); // 1
t.splitText(5);
console.log(el.childNodes.length); // 2
// 当然,认为追加文本节点,也会产生相邻文本节点但情况
var t2 = document.createTextNode('another text');
el.appendChild(t2);
console.log(el.childNodes.length); // 3
Node类型的节点都有一个normalize()
方法,用于合并相邻但文本节点。
接着上面的例子讲:
el.normalize();
console.log(el.childNodes.length); // 1
Comment节点
注释节点是通过Comment类型表示,这个类型和Text继承自相同的基类,因此它们具有相同的方法,除了splitText
。
创建注释节点可以通过createComment()
方法。
var c = document.createComment('This is comment content~~~~');
document.body.appendChild(c);
CDATASection类型
CDATASection类型只针对与XML文档。它跟Comment类型,同样跟Text类型继承自同一基类,同样也没有splitText
方法。
在浏览器中解析XML文档,CDATASection类型一般会被当成注释解析。
- nodeType = 4
- nodeName = ‘#cdata-section’
- nodeValue = CDATA区域中的内容
- parentNode是Document 或 Element
- 没有子节点
DocumentType
表示文档类型
- nodeType = 10;
- nodeName :为 doctype 的名称
- nodeValue :值为 null;
- parentNode 是 Document;
- 不支持(没有)子节点
DocumentFragment
表示一个文档片段,可以简单理解为,从Document中任意取下一部分即可看作为DocumentFragment。
- nodeType = 11
- nodeName = ‘#document-fragment’
- nodeValue: null
- parentNode: null
- 子节点跟Document的子节点基本相同,除了没有DocumentType类型。
我们不可以将DocumentFragment本身直接添加到Document当中,但是可以将DocumentFragment中包含的节点添加到Document中。可以将DocumentFragment看作是一个临时仓库,可以存储DOM节点,而且它具有Node类型的所有方法,可以对自身包含的节点进行各种跟Document一样可以进行的操作。
Attr类型
Attr类型在DOM中的定位很尴尬啊~! 理论上讲,它并不是Document的节点(Node)。它只是一般用于描述Node特性的数据,所以把它看作是Node有些牵强。关于它的一些相关内容,了解下就可以了。
- nodeType 的值为 2
- nodeName 的值是特性的名称
- nodeValue 的值是特性的值
- parentNode 的值为 null;
- 在 HTML 中不支持(没有)子节点,在 XML 中子节点可以是 Text 或 EntityReference
Attr类型的几个方法
- createAttribute
, 创建属性
- setAttributeNode
, 将属性添加到元素节点上
- getAttributeNode
, 根据属性名称获取属性
DOM操作技术
动态脚本和动态样式
DOM家在JavaScript和CSS代码有两种方式:引入外部文件和 直接插入代码。
写法很简单,看到这儿的朋友99.99999%都会。
下面写个兼容性比较高的方法,活动下手指。
// 动态写入JavaScript代码
function loadScriptCode(code) {
var script = document.createElement('script');
script.type = 'text/javascript';
try {
script.appendChild(document.createTextNode(code));
} catch{
// IE浏览器认为script是一种特殊的脚本,不允许直接访问子节点
script.text = code;
}
document.body.appendChild(script);
}
function loadStyleCode(code) {
var style = document.createElement('style');
style.type = 'text/css';
try {
style.appendChild(document.createTextNode(code));
} catch{
// IE 浏览器将style看作特殊元素,不允许直接访问其子节点
style.styleSheet.cssText = code;
}
document.head.appendChild(style);
}
表格操作
因为表格的结构比较复杂,有表头、行、列以及单元格等。如果用传统的方式,操作DOM节点,会特别麻烦冗长。
所以,HTML DOM为表格及表格里面的元素添加了一些特殊的属性和方法。
NodeList
NodeList 及其“近亲”NamedNodeMap 和 HTMLCollection,是从整体上透彻理解 DOM 的 关键所在。这三个集合都是“动态的”;换句话说,每当文档结构发生变化时,它们都会得到更新。因此,它们始终都会保存着最新、最准确的信息。从本质上说,所有 NodeList 对象都是在访问 DOM 文档时实时运行的查询。
var divs = document.getElementsByTagName('div');
for(var i = 0; i < divs.length; i++){
document.body.appendChild(document.createElement('div'));
}
一般来说,应该尽量减少访问 NodeList 的次数。因为每次访问 NodeList,都会运行一次基于文 档的查询。所以,可以考虑将从 NodeList 中取得的值缓存起来。