第10章 - DOM

DOM 就是文档的数据结构,它提供了操作文档的编程接口 API。

10.1 节点层次

文档是由节点组成的树形结构,根节点是文档节点 Documet ,其下是 html 元素节点。

10.1.1 Node 类型

节点的类型

总共有 12 种类型的节点,使用 Node 类可以判断一个节点的类型。注意:在 IE 中只能使用数值,不能使用常量。

常量 说明
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
if (someNode.nodeType == 1){ //使用数值可以在所有浏览器下工作
    alert("Node is an element.");
}

节点的信息

每个节点都有 nodeType 、nodeNamenodeValue 三个属性,可以了解节点的信息。

if (someNode.nodeType == 1){
    value = someNode.nodeName; //将会返回标签名
}

不同节点各属性的含义

不同类型的节点,nodeNamenodeValue 的意义不一样,因此,一般情况下要先判断节点类型。

元素节点各属性的含义

名称 说明
nodeName 元素的标签名称,等同之前的 tagName
nodeType 1
nodeValue 元素节点没有值,所以返回 null

属性节点各属性的含义

名称 说明
nodeName 属性名称
nodeType 2
nodeValue 属性值

文本节点各属性的含义

名称 说明
nodeName #text
nodeType 3
nodeValue 文本内容(不包含 html)

节点的关系

节点的层次结构可以划分为:父子节点、 兄弟节点这两种。 利用节点的层次结构可以获取节点树上的任意其他节点

属性 说明
childNodes 获取当前元素节点的所有子节点,返回一个 NodeList 对象
parentNode 获取当前节点的父节点
previousSibling 获取当前节点的前一个同级节点,第一个节点会返回 null
nextSibling 获取当前节点的后一个同级节点,最后一个节点会返回 null
firstChild 获取当前元素节点的第一个子节点
lastChild 获取当前元素节点的最后一个子节点
ownerDocument 获取该节点的文档根节点,相当于文档对象 document
hasChildNodes 判断是否有子节点

子节点列表

获取字节的的属性 childNodes 是个列表对象,可以像数组一样访问它

var firstChild = someNode.childNodes[0];
var secondChild = someNode.childNodes.item(1);
var count = someNode.childNodes.length;

也可以用数组的 slice 方法将 childNodes 对象转换为数组

//在 IE8 之前不工作
var arrayOfNodes = Array.prototype.slice.call(someNode.childNodes,0);

节点的增删改查

方法 说明
appendChild 将新节点追加到子节点列表的末尾,返回添加的节点
insertBefore 将新节点插入在某个节点的前面,返回被插入的节点
repalceChild 将新节点替换旧节点,返回被替换的节点
removeChild 移除节点,返回被移除的节点
cloneNode 克隆一个节点,可以深克隆,也可以浅克隆

添加节点

var returnedNode = someNode.appendChild(newNode);
alert(returnedNode == newNode); //true
alert(someNode.lastChild == newNode); //true

如果被添加的节点是已经文档中的节点,那么节点将被移动位置

//将第一个节点移动到最后
var returnedNode = someNode.appendChild(someNode.firstChild);
alert(returnedNode == someNode.firstChild); //false
alert(returnedNode == someNode.lastChild); //true

插入节点

//位置参数为 Null,则插入到最后
returnedNode = someNode.insertBefore(newNode, null);
alert(newNode == someNode.lastChild); //true

//插入到第一个
returnedNode = someNode.insertBefore(newNode, someNode.firstChild);
alert(returnedNode == newNode); //true
alert(newNode == someNode.firstChild); //true

//插入到倒数第二个
returnedNode = someNode.insertBefore(newNode, someNode.lastChild);
alert(newNode == someNode.childNodes[someNode.childNodes.length-2]); //true

深克隆和浅克隆

var deepList = myList.cloneNode(true);
alert(deepList.childNodes.length); //3 (IE < 9) or 7 (others)

var shallowList = myList.cloneNode(false);
alert(shallowList.childNodes.length); //0

10.1.2 Document 类型

文档对象代表整个页面文档,是 DOM 的根节点,它是只读的,因此不能添加、删除、替换子节点。其下一般只有惟一一个 html 子节点,是系统自动创建的

属性
nodeType 9
nodeName "#document"
nodeValue null
parentNode null
ownerDocument null
子节点 可以有一个 DocumentType 一个 Element 或者一个注释等等

文档对象的属性

除了具有普通节点的属性和方法之外,document 对象还有一些快捷属性

属性 说明
document.documentElement 返回文档包含的子节点,指向 html 元素
document.body 指向 body 元素
document.doctype 指向 doctype 元素,各个浏览器对这个属性不一致,因此不常用
document.title 返回文档标题
document.URL 返回文档的链接
document.referrer 返回链接到当前页面的那个页面的链接,可能为空
document.domain 返回域名,不能设置为 URL 中不包含的域

document.documentElement 的例程

var html = document.documentElement; //返回 <html> 元素对象
alert(html === document.childNodes[0]); //true
alert(html === document.firstChild); //true

文档对象的域

修改域的时候,只能修改子域

//page from p2p.wrox.com
document.domain = "wrox.com"; //成功
document.domain = "nczonline.net"; //不在同一个域中,出错!

设置为同样的主域名,不同页面之间的对象可以进行通信

//page from p2p.wrox.com
document.domain = "wrox.com"; //可以通信
document.domain = "p2p.wrox.com"; //这样不能通信

查找元素

利用文档对象可以查找元素对象。
注意,必须等文档加载完毕,才能查找和操作 DOM 操作对象,可以将相关代码放在 onload 事件中

方法 说明
document.getElementsById 返回一个元素
document.getElementsByTagName 返回同一种标签的集合,可以用通配符
document.getElementsByName 返回有相同名字的元素集合,可以用通配符

元素集合

getElementsByTagName 和 getElementsByName 这两个查找方法会返回元素的集合

var images = document.getElementsByTagName("img");

alert(images.length); //output the number of images
alert(images[0].src); //output the src attribute of the first image
alert(images.item(0).src); //output the src attribute of the first image

var myImage = images.namedItem("myImage");
var myImage = images["myImage"];

特殊集合的快捷访问

文档对象有几个属性,可以直接获取某些元素集合

属性 说明
document.anchors 锚点集合
document.applets 小程序集合
document.forms 表单集合
document.images 图像集合
document.links 链接集合

DOM 一致性检测

DOM 本身分很多级别,不同浏览器实现的程度也有所不同,可以用 document.implementation.hasFeature 方法检测

var hasXmlDom = document.implementation.hasFeature("XML", "1.0");

下面的表格时可以进行检测的功能和版本号

功能 版本号 说明
Core 1.0, 2.0, 3.0
XML 1.0, 2.0, 3.0
HTML 1.0, 2.0
Views 2.0
StyleSheets 2.0
CSS 2.0
CSS2 2.0
Events 2.0, 3.0
UIEvents 2.0, 3.0
MouseEvents 2.0, 3.0
MutationEvents 2.0, 3.0
HTMLEvents 2.0
Range 2.0
Traversal 2.0
LS 3.0
LS-Async 3.0
Validation 3.0

文档写入

document.write方法可以向文档写入内容,有两个需要注意的地方

  • 包含 "</script>" 这个特殊字符串的时候要转义一下
  • 当文档加载完毕后调用 document.write方法,将重写整个文档
document.write("<script type=\"text/javascript\" src=\"file.js\">" + "<\/script>");

10.1.3 Element 类型

元素对象是网页中最基本的对象

属性
nodeType 1
nodeName 元素的标签名
nodeValue null
parentNode null
ownerDocument Document 或者 Element
子节点

获取元素对象的标签名

除了用节点的 nodeName 之外,也可以直接用 tagName 属性获取标签名

if (element.tagName == "div"){ //AVOID! Error prone!
    //do something here
}

if (element.tagName.toLowerCase() == "div"){ //Preferred - works in all documents
    //do something here
}

元素的属性

元素对象的所有属性都可以读取和设置

属性 说明
tagName 获取元素节点的标签名,一般返回大写字符串
innerHTML 获取元素节点里的内容,非 W3C DOM 规范,但是个浏览器都支持
id 元素节点的 id 名称
title 元素节点的 title 属性值
style CSS 内联样式属性值
className CSS 元素的类,因为 class 是关键字,所以使用 className
bbb 不支持自定义属性,
onclick 返回事件函数的代码

元素的特性

通过特性除了可以访问元素的属性之外,还可以:

  • 自定义特性,前缀为 data- ,自定义特性名都被转换为小写
  • 通过属性访问 style 返回的是对象,通过特性访问 style 返回字符串
  • 通过属性访问事件,返回函数对象,通过特性访问事件,返回字符串

特性的相关方法

  • getAttribute() 方法
  • setAttribute() 方法
  • removeAttribute() 方法

特性对象数组 attributes

attributes 是包含特性对象的键值对数组,可以通过名字或者索引号获取其中的对象

attrName = element.attributes[i].nodeName;

element.attributes["id"].nodeValue = "someOtherId";

可以通过特性对象数据增删改查元素的特性,但是相关方法并不方便,不如直接使用元素对象的特性相关方法。
所以,一般情况下不使用特性,只在枚举特性和自定义特性的情况下使用

动态创建元素对象

可以用 document.createElement() 方法动态创建元素对象:

  • 通常情况下传入标签名
  • 对于老式浏览器,传入 HTML 代码
var div = document.createElement("div");

var div2 = document.createElement("<div id=\"myNewDiv\" class=\"box\"></div>");

div.id = "myNewDiv";
div.className = "box";

document.body.appendChild(div);

不同浏览器对空白的不同解释

大部分浏览器都将子元素之间的换行符也当做一个文本元素子节点;IE 浏览器只将子元素作为子节点,忽略换行符。
因此,统计子节点时可以对节点类型进行一个判断

for (var i=0, len=element.childNodes.length; i < len; i++){
    if (element.childNodes[i].nodeType == 1){
        //do processing
    }
}

10.1.4 Text 类型

文本节点一般是元素对象中的文字内容,本身也是一个节点

属性
nodeType 3
nodeName #text
nodeValue 文本内容
parentNode 包含该文本的元素节点
子节点

下面是文本节点的增删改查方法及相关属性

属性或方法
appendData(text) 添加
deleteData(offset, count) 删除
insertData(offset, text) 插入
replaceData(offset, count, text) 替换
splitText(offset) 切分
substringData(offset, count) 子串
length 长度

元素最多只能包含一个文本节点,而且必须有内容存在

下面代码中空格也是一个文本节点

<!-- 空格也是一个文本节点 -->
<div> </div>

给文本节点设置新的内容,特殊字符会自动被转义

//输出结果是: "Some <strong>other</strong> message"
div.firstChild.nodeValue = "Some <strong>other</strong> message";

创建文本节点

document.createTextNode() 方法可以创建文本节点

var element = document.createElement("div");
element.className = "message";

var textNode = document.createTextNode("Hello world!");
element.appendChild(textNode);

document.body.appendChild(element);

规范化文本节点

在一个元素内可以添加多个文本节点,规范化方法 element.normalize() 可以将这些文本节点合并起来

var element = document.createElement("div");
element.className = "message";

var textNode = document.createTextNode("Hello world!");
element.appendChild(textNode);

var anotherTextNode = document.createTextNode("Yippee!");
element.appendChild(anotherTextNode);
document.body.appendChild(element);

alert(element.childNodes.length); //2

element.normalize();
alert(element.childNodes.length); //1

alert(element.firstChild.nodeValue); //"Hello world!Yippee!"

切分文本节点

切分文本节点可以将文本从指定位置处切分,原文本节点保留切分点之前内容,返回一个新文本节点包含切分点之后的内容

var element = document.createElement("div");
element.className = "message";

var textNode = document.createTextNode("Hello world!");
element.appendChild(textNode);
document.body.appendChild(element);

var newNode = element.firstChild.splitText(5);

alert(element.firstChild.nodeValue); //"Hello"
alert(newNode.nodeValue); //" world!"
alert(element.childNodes.length); //2

10.1.5 Commet 类型

注释类型和文本类型有同样的父类,这个类型在实际项目中用处不大,可以忽略。

10.1.6 CDATASection 类型

该类型一般只出现在 XML 文档中,对 HTML 文档用处不大,可以忽略。

10.1.7 DocumentType 类型

该类型对象不能动态创建,只能通过 document.doctype 获取

10.1.8 DocumentFragment 类型

  • 有时候需要一次在文档树中添加多个节点,会导致页面频繁刷新。
  • 这时候可以创建一个 DocumentFragment 对象,这个对象和文档中的 Node 一样,但是不会刷新页面,也不会显示。
  • 将需要添加的节点添加到文档碎片对象之下,添加完毕后,再将文档碎片对象添加到文档中的目标节点下。这样,文档碎片对象的子节点会自动成为目标节点的子节点
var fragment = document.createDocumentFragment();
var ul = document.getElementById("myList");
var li = null;

for (var i=0; i < 3; i++){
    li = document.createElement("li");
    li.appendChild(document.createTextNode("Item " + (i+1)));
    fragment.appendChild(li);
}

ul.appendChild(fragment);

10.1.9 Attr 类型

Attr 类型就是特性对象的类型,attributes 属性中的对象都是特性对象。特性节点不出在 DOM 树中,但是可以承载信息。

特性对象有三个常用属性:name、value 和 specified ,其中 specified 用来确定特性是默认值还是用户设置的值

document.createAttribute() 方法可以创建特性节点

var attr = document.createAttribute("align");
attr.value = "left";
element.setAttributeNode(attr);

alert(element.attributes["align"].value); //"left"
alert(element.getAttributeNode("align").value); //"left"
alert(element.getAttribute("align")); //"left"

10.2 DOM 操作技术

10.2.1 动态脚本

动态加载外部脚本

function loadScript(url){
    var script = document.createElement("script");
    script.type = "text/javascript";
    script.src = url;
    document.body.appendChild(script);
}

loadScript("client.js");

动态生成内联脚本

创建脚本节点对象以后,可以为脚本节点对象在创建一个文本子节点,其中包含代码。但是 IE不支持这种方法,可以直接赋值给 script.text

function loadScriptString(code){
    var script = document.createElement("script");
    script.type = "text/javascript";

    try {
        //适用于现代浏览器
        script.appendChild(document.createTextNode(code));
    } catch (ex){
        //适用于 IE
        script.text = code;
    }

    document.body.appendChild(script);
}


loadScriptString("function sayHi(){alert(‘hi’);}");

10.2.2 动态样式

动态加载外部样式表

function loadStyles(url){
    var link = document.createElement("link");
    link.rel = "stylesheet";
    link.type = "text/css";
    link.href = url;
    var head = document.getElementsByTagName("head")[0];
    head.appendChild(link);
}
 
loadStyles("styles.css");

动态加载内联样式表

function loadStyleString(css){
    var style = document.createElement("style");
    style.type = "text/css";

    try{
        //适用于现代浏览器
        style.appendChild(document.createTextNode(css));
    } catch (ex){
        //适用于 IE
        style.styleSheet.cssText = css;
    }

    var head = document.getElementsByTagName("head")[0];
    head.appendChild(style);
}

loadStyleString("body{background-color:red}");

10.2.3 操作表格

如果用普通的节点方法动态操作表格非常麻烦,下面是一个表格的 HTML 代码

<table border="1" width="100%">
    <tbody>
        <tr>
            <td>Cell 1,1</td>
            <td>Cell 2,1</td>
        </tr>
        <tr>
            <td>Cell 1,2</td>
            <td>Cell 2,2</td>
        </tr>
    </tbody>
</table>

用普通节点方法像下面这样

//创建表格对象
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("Cell 1,1"));
row1.appendChild(cell1_1);

var cell2_1 = document.createElement("td");
cell2_1.appendChild(document.createTextNode("Cell 2,1"));
row1.appendChild(cell2_1);

//创建第二行
var row2 = document.createElement("tr");
tbody.appendChild(row2);

var cell1_2 = document.createElement("td");
cell1_2.appendChild(document.createTextNode("Cell 1,2"));
row2.appendChild(cell1_2);

var cell2_2= document.createElement("td");
cell2_2.appendChild(document.createTextNode("Cell 2,2"));
row2.appendChild(cell2_2);

//加入到文档中
document.body.appendChild(table);

其实表格对象有一些自己特定的属性和方法

属性或方法 说明
caption
tBodies
tFoot
tHead
rows
createTHead()
createTFoot()
createCaption()
deleteTHead()
deleteTFoot()
deleteCaption()
rows
deleteRow(pos)
insertRow(pos)
cells
deleteCell(pos)
insertCell(pos)

利用这些属性和方法创建表格相对简单一点

//创建表格
var table = document.createElement("table");
table.border = 1;
table.width = "100%";

//创建 tbody
var tbody = document.createElement("tbody");
table.appendChild(tbody);

//创建第一行
tbody.insertRow(0);
tbody.rows[0].insertCell(0);
tbody.rows[0].cells[0].appendChild(document.createTextNode("Cell 1,1"));
tbody.rows[0].insertCell(1);
tbody.rows[0].cells[1].appendChild(document.createTextNode("Cell 2,1"));

//创建第二行
tbody.insertRow(1);
tbody.rows[1].insertCell(0);
tbody.rows[1].cells[0].appendChild(document.createTextNode("Cell 1,2"));
tbody.rows[1].insertCell(1);
tbody.rows[1].cells[1].appendChild(document.createTextNode("Cell 2,2"));

//加入到文档中
document.body.appendChild(table);

10.2.4 使用 NodeList

查询获取的 NodeList 数组是动态的,只要文档发生了变化, NodeList 也发生对应的变化。下面的代码会死循环

var divs = document.getElementsByTagName("div"),
        i,div;

for (i=0; i < divs.length; i++){
    //创建新的 div 元素导致 NodeList 的长度动态发生变化,造成死循环
    div = document.createElement("div");
    document.body.appendChild(div);
}

为了达到上述代码想实现的目标,同时避免死循环,可以在循环之前获取 NodeList 的长度

var divs = document.getElementsByTagName("div"),
        i,len,div;

//用临时变量存储 NodeList 的长度,这样去不受动态变化的影响
len=divs.length;

for (i=0; i < len; i++){
    div = document.createElement("div");
    document.body.appendChild(div);
}

推荐阅读更多精彩内容