DOM操作技术

前端开发
2018年12月09日
367

DOM(Document Object Model,文档对象模型)是针对HTML和XML文档的一个API。DOM描述了一个层次化的节点树,允许开发人员添加、移除和修改页面的某一部分。

节点层次

DOM可以将任何HTML或XML文档描绘成一个由多层节点构造的结构。节点分为几种不同的类型,每种类型分别表示文档中不同的信息及(或)标记。每个节点都拥有各自的特点、数据和方法,另外也与其他节点存在某种关系。节点之间的关系构成了层次,而所有页面标记则表现为一个以特写节点为根节点的树形结构。

html
<html> <head> <title>Sample Page</title> </head> <body> <p>Hello world!</p> </body> </html>

Node类型

DOM1级定义了一个Node接口,该接口将由DOM中的所有节点类型实现。这个Node接口在JavaScript中是作为Node类型实现的;除IE之外,在其他所有浏览器中都可以访问到这个类型。JavaScript中的所有节点类型都继承自Node类型,因此所有节点类型都共享着相同的基本属性和方法。

每个节点都有一个nodeType属性,用于表明节点的类型。节点类型由在Node类型中定义的下列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)。

通过比较上面这些常量,可以很容易地确定节点的类型。

js
if (someNode.nodeType === Node.ELEMENT_NODE) { // 在IE中无效 alert('Node is an element.'); }

由于IE没有公开Node类型的构造函数,因此上面的代码在IE中会导致错误,为了确定跨浏览器兼容,最好还是将nodeType属性与数字值进行比较。

js
if (someNode.nodeType === 1) { alert('Node is an element.'); }

并不是所有节点类型都受到Web浏览器的支持。开发人员最常用的就是元素节点文本节点

nodeName和nodeValue属性

要了解节点的具体信息,可以使用nodeNamenodeValue这两个属性。这两个属性的值完全取决于节点的类型。

js
if (someNode.nodeType === 1) { value = someNode.nodeName; // nodeName的值是元素的标签名 // 对于元素节点,nodeValue的值始终为null }

节点关系

文档中所有的节点之间都存在这样或那样的关系。节点之间的各种关系可以用传统的家族关系来描述,相当于把文档树比喻成家谱。

子节点

每个节点都有一个childNodes属性,其中保存着一个NodeList对象NodeList是一种类数组对象,用于保存一组有序的节点,可以通过位置来访问这些节点。请注意,虽然可以通过方括号语法来访问NodeList的值,而且这个对象也有length属性,但它并不是Array的实例。NodeList对象的独特之处在于,它实际上是基于DOM结构动态执行查询的结果,因此DOM结构的变化能够自动反映在NodeList对象中。

js
var firstChild = someNode.childNodes[0]; var secondChild = someNode.childNodes[1]; var count = someNode.childNodes.length;

父节点

每个节点都有一个parentNode属性,该属性指向文档树中的父节点。包含在childNodes列表中的所有节点都具有相同的父节点,因此它们的parentNode属性都指向同一个节点。

同胞节点

包含在childNodes列表中的每个节点相互之间都是同胞节点。通过使用列表中每个节点的previousSiblingnextSibling属性,可以访问同一列表中的其他节点。列表中第一个节点的previourSibling属性的值为null,而列表中最后一个节点的nextSibling属性的值也为null。

js
if (someNode.nextSibling === null) { alert('这是最后一个节点'); } else if (someNode.previousSbiling === null) { alert('这是第一个节点'); }

firstChild和lastChild

父节点与其第一个和最后一个子节点也存在特殊关系。父节点的firstChildlastChild属性分别指向其childNodes列表中的第一个和最后一个节点。

在反映这些关系的所有属性当中,childNodes属性与其他属性相比更方便一些,因为只须使用简单的关系指针,就可以通过它访问文档树中的任何节点。

hasChildNodes()

另外,hasChildNodes()也是一个非常有用的方法,这个方法可以检测是否含有子节点。这是比查询childNodes.length更简单的方法。

ownerDocument

所有节点都有一个ownerDocument属性,该属性指向表示整个文档的文档节点。这种关系表示的是任何节点都属于它所在的文档,任何节点都不能同时存在于两个或更多个文档中。通过这个属性,我们可以不必在节点层次中通过层层回溯到达顶端,而是可以直接访问文档节点。

操作节点

因为关系指针都是只读的,所以DOM提供了一些操作节点的方法。

appendChild()

appendChild()用于向childNodes列表的末尾添加一个节点。并且返回新增的节点。

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

insertBefore()

insertBefore()会在指定参照的节点前面插入一个节点。这个方法接受两个参数:要插入的节点和作为参照的节点。如果参照节点是null,则insertBefore()appendChild()执行相同的操作。

js
var returnedNode = someNode.insertBefore(newNode, null); alert(newNode === someNode.lastChild); // true var returnedNode = someNode.insertBefore(newNode, someNode.firstChild); alert(returnedNode === newNode); // true alert(returnedNode === someNode.firstChild); // true var returnedNode = someNode.insertBefore(newNode, someNode.lastChild); alert(newNode === someNode.childNodes[someNode.childNodes.length - 2]); // true

replaceChild()

replaceChild()接受两个参数:要插入的节点和要替换的节点。

js
var returnedNode = someNode.replaceChild(newNode, someNode.firstChild);

在使用replaceChild()插入一个节点时,该节点的所有关系指针都会从被它替换的节点复制过来。尽管从技术上讲,被替换的节点仍然还在文档中,但它在文档已经没有了自己的位置。

removeChild()

removeChild()接受一个参数:要移除的节点。被移除的节点将成为这个方法的返回值。

js
var formerFirstChild = someNode.removeChild(someNode.firstChild);

其他方法

cloneNode()

有两个方法是所有类型的节点都有的。第一个就是cloneNode(),用于创建调用这个方法的节点的一个完全相同的副本。cloneNode()接受一个布尔值参数,表示是否执行深复制。在参数为true的情况下,执行深复制,也就是复制节点及其整个子节点树;在参数为false的情况下,执行浅复制,即只复制节点本身。复制后返回的节点副本属于文档所有,但并没有为它指定父节点。因此,这个副本就成了一个“孤儿”,除非把它们添加到文档中。

normalize()

normalize()这个方法唯一的作用就是处理文档树中的文本节点。由于解析器的实现或DOM操作等原因,可能会出现文本节点不包含文本,或者接连出现两个文本节点的情况。当某个节点上调用这个方法时,就会在该节点的后代节点中查找上述两种情况。如果找到了空文本节点,则删除它;如果找到相邻的文本节点,则将它们合并为一个文本节点。

Document类型

JavaScript通过Document类型表示文档。在浏览器中,document对象是HTMLDocument(继承自Document类型)的一个实例,表示整个HTML页面。而且,document对象window对象的一个属性,因此可以将其作为全局对象来访问。Document节点具有下列特征:

  • nodeType的值为9;
  • nodeName的值为"#document";
  • nodeValue的值为null;
  • parentNode的值为null;
  • ownerDocument的值为null;
  • 其子节点可能是一个DocumentType(最多一个)、Element(最多一个)、ProcessingInstruction或Comment。

Document类型可以表示HTML页面或者其他基于XML的文档。不过,最常见的应用还是作为HTMLDocument实例的document对象

文档的子节点

虽然DOM标准规定Document节点的子节点可以是DocumentType、Element、ProcessingInstruction或Comment,但还有两个内置的访问其子节点的快捷方式。第一个就是documentElement属性,这个属性始终指向HTML页面中的<html>元素。另一个就是通过childNodes列表访问文档元素,但通过documentElement属性则能更快捷、更直接地访问该元素。

js
var html = document.documentELement; alert(html === document.childNodes[0]); // true

document对象还有一个body属性,直接指向<body>元素

js
var body = document.body;

Document另一个可能的子节点是DocumentType。通常将<!DOCTYPE>标签看成一个与文档其他部分不同的实例,可以通过doctype属性来访问它的信息。

js
var doctype = document.doctype;

浏览器对document.doctype的支持差别很大。

  • IE8及之前版本:如果存在文档类型声明,会将其错误地解释为一个注释并把它当作Comment节点;而document.doctype的值始终为null。
  • IE9+及Firefox:如果存在文档类型声明,会将其作为文档的第一个子节点;document.doctype是一个DoucmentType节点,也可以通过document.firstChilddocument.childNodes[0]访问同一个节点。
  • Safari、Chrome和Opera:如果存在文档类型声明,则将其解析,但不作为文档的子节点。document.doctype是一个DocumentType节点,但该节点不会出现在document.childNodes中。

由于浏览器对document.doctype的支持不一致,因此这个属性的用处很有限。

从技术上说,出现在<html>元素外部的注释应该算是文档的子节点。然而,不同的浏览器在是否解析这些注释以及能否正确处理它们等方面,也存在很大差异。

html
<!-- 这是第一条注释 --> <html> <body> </body> </html> <!-- 这是第二条注释 -->
  • IE8及之前版本、Safari3.1及更高版本、Opera和Chrome只为第一条注释创建节点,不为第二条注释创建节点。结果,第一条注释就会成为document.childNodes中的第一个子节点。
  • IE9及更高版本会将两条注释创建为document.childNodes中的一个注释节点。
  • Firefox以及Safari3.1之前的版本会完全忽略这两条注释。

同样,浏览器间的这种不一致性也导致了位置<html>元素外部的注释没什么卵用。

多数情况下,我们都用不着在document对象上调用appendChild()removeChild()replaceChild()方法,因为文档类型(如果存在的话)是只读的,而且它只能有一个元素子节点(该节点通常早就已经存在了)。

文档信息

document对象还有一些标准的Document对象所没有的属性。这些属性提供了document对象所表现的网页的一些信息。

title

title属性包含着<title>元素中的文本——显示在浏览器窗口的标题或标签页上。

js
// 取得文档标题 var title = document.title; // 设置文档标题 document.title = 'New Page';

URL、domain和referrer

URL属性包含页面完整的URL,domain属性中只包含页面的域名,而referrer属性中则保存着来源页面的URL地址,如果没有来源页面的情况,referrer属性中可能会包含空字符串。所有的这些信息都存在于请求的HTTP头部,只不过是通过这些属性让我们能够在JavaScript中访问它们而已。

js
// 取得完整的URL var url = document.URL; // 取得域名 var domain = document.domain; // 取得来源页面的URL var referrer = document.referrer;

URLdomain是相互关联的。

在这3个属性中,只有domain是可以设置的。但由于安全方面的限制,不能将这个属性设置为URL中不包含的域。

js
// 假设页面来自blog.humandetail.com document.domain = 'humandetail.com'; // OK document.domain = 'baidu.com'; // ERROR

浏览器对domain属性还有一个限制,即如果域名一开始是“松散的”(loose),那么不能将它再设置为“紧绷的”(tight)。

js
// 假设页面来自blog.humandetail.com document.domain = 'humandetail.com'; // 松散的(成功) document.domain = 'blog.humandetail.com'; // 紧绷的(失败)

查找元素

getElementById()

getElementById()接收一个参数:要取得的元素的ID。如果找到相应的元素则返回该元素,否则返回null。注意,这里的ID必须与页面中的元素的id严格匹配,包括大小写。

html
<div id="myDiv"></div>
js
var myDiv = document.getElementById('myDiv');

getElementsByTagName()

getElementsByTagName()接收一个参数:要取得的元素的标签名,而返回的是包含零个或多个元素的NodeList

在HTML文档中,这个方法会返回一个HTMLCollection对象,作为一个“动态”集合,该对象与NodeList对象非常类似。

js
var imgs = document.getElementsByTagName('img');

NodeList对象类似,可以使用方括号语法或item()方法来访问里面的项。而这个对象中的元素数量则可以通过其length属性取得。

js
alert(imgs.length); alert(imgs[0].src); alert(imgs.item(0).src);

HTMLCollection对象还有一个方法,叫做namedItem(),使用这个方法可以通过元素的name取得集合中的项。

html
<img src="myImage.gif" name="myImage">
js
var imgs = docuemtn.getElementsByTagName('img'); var myImage = imgs.namedItem('myImage'); // 也可以使用索引 var myImage = imgs['myImage'];

对于HTMLCollection而言,我们可以向方括号中传入数值或字符串形式的索引值。在后台,对数值索引就会调用item(),而对字符串索引就会调用namedItem()

要想取得文档中的所有元素,可以向getElementsByTagName()传递参数'*'

js
var allEl = document.getElementsByTagName('*');

getElementsByName()

getElementsByName()这个方法是HTMLDocument类型才有的方法。这个方法会返回带有给定name特性的所有元素。最常使用getElementsByName()的情况是取得单选按钮。

html
<fieldset> <legend>Which color do you prefer?</legend> <ul> <li> <input type="radio" name="color" value="red" id="colorRed"> <label for="colorRed">Red</label> </li> <li> <input type="radio" name="color" value="green" id="colorGreen"> <label for="colorGreen">Green</label> </li> <li> <input type="radio" name="color" value="blue" id="colorBlue"> <label for="colorBlue">Blue</label> </li> </ul> </fieldset>
js
var radios = document.getElementsByName('color');

getElementsByTagName()类似,getElementsByName()方法返回一个NodeList。但,对于这里的单选按钮来说,namedItem()方法则只会取得第一项(因为每一项的name特性都相同)。

特殊集合

  • document.anchors,包含文档中所有带name属性的<a>元素
  • document.applets,包含文档中所有<applets>元素,因为不再推荐使用<applets>,所以这个集合已经不建议使用了;
  • document.forms,包含文档中所有<form>元素
  • document.images,包含文档中所有<img>元素
  • document.links,包含文档中所有带href属性的<a>元素

DOM一致性检测

由于DOM分为多个级别,也包含多个部分,因此检测浏览器实现了DOM哪些部分就十分必要了。document.implementation属性就是为此提供相应信息和功能的对象,与浏览器对DOM的实现直接对应。

DOM1级只为document.implementation规定了一个方法:hasFeature(),这个方法接受两个参数:要检测的DOM功能的名称及版本号。如果浏览器支持给定名称和版本功能,则该方法返回true。

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

文档写入

有一个document对象的功能已经存在很多年了,那就是将输出流写入到网页中的能力。这个能力体现在下列4个方法中:write()writeln()open()close()。其中writewriteln()方法都失道寡助一个字符串参数:即要写入到输出流中的文本。write()会原样写入,而writeln()则会在字符串的末尾添加一个换行符(\n)。在页面被加载的过程中,可以使用这两个方向向页面动态地加入内容。

html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> </head> <body> <p>The current date and time is: </p> <script> document.write('<strong>' + new Date().toString() + '</strong>') </script> </body> </html>

此外,还可以使用write()writeln()方法动态地包含外部资源。

html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> </head> <body> <script> document.write('<script src="file.js"><\/script>') </script> </body> </html>

注意,如果在文档加载结束后再调用document.write(),那么输出的内容将会重写整个页面。

html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> </head> <body> <p>这里是一串没什么卵用的文本,因为页面会被重写</p> <script> window.onload = function () { document.write('Hello world!'); } </script> </body> </html>

Element类型

除了Document类型之外,ELement类型就要算是Web编程中最常用的类型了。Element类型用于表现HTML或XML元素,提供了对元素标签名、子节点及特性的访问。Element节点具有以下特征:

  • nodeType的值为1;
  • nodeName的值为元素的标签名;
  • nodeValue的值为null;
  • parentNode可能是Document或Element;
  • 其子节点可能是Element、Text、Comment、ProcessingInstruction、CDATASection或EntityReference。

要访问元素的标签名,可以使用nodeName属性,也可以使用tagName属性;这两个属性会返回相同的值。

html
<div id="myDiv"></div>
js
var div = document.getElementById('myDiv'); alert(div.tagName); // 'DIV' alert(div.tagName === div.nodeName); // true

在HTML中,标签名始终都以全部大写表示;而在XML(有时候也包括XHTML)中,标签名则始终会与源代码中的保持一致。

js
// 比较之前最好先将标签名转换为相同的大小写形式。 if (element.tagName.toLowerCase() === 'div') { // ... }

HTML元素

所有HTML元素都由HTMLElement类型表示,不是直接通过这个类型,也是通过它的子类型来表示。HTMLElement类型直接继承自Element并添加了一些属性。添加的这些属性分别对应于每个HTML元素中都存在的下列标准特性。

  • id,元素在文档中的唯一标识符。
  • title,有关元素的附加说明信息。
  • lang,元素内容的语言代码,很少使用。
  • dir,语言的方向,值为“ltr”(left-to-right)或“rtl”(right-to-left),也很少使用。
  • className,与元素的class特性对应,即为元素指定的类名。没有将这个属性命名为class是因为class是ECMAScript的保留字。

上述这些属性都可以用来取得或修改相应的特性值。

html
<div id="myDiv" class="bd" title="Body text" lang="en" dir="ltr"></div>
js
var div = document.getElementById('myDiv'); alert(div.id); // 'myDiv' alert(div.className); // 'bd' alert(div.title); // 'Body text' alert(div.lang); // 'en' alert(div.dir); // 'ltr'

也可以通过赋值修改对应的每个特性。

js
div.id = 'someOtherId'; div.className = 'ft'; div.title = 'Some other text'; div.lang = 'fr'; div.dir = 'rtl'

取得特性(getAttribute)

每个元素都有一个或多个特性,这些特性的用途是给出相应元素或其内容的附加信息。

getAttribute(),接受一个特性名作为参数,返回该特性的值,如果给定的特性名不存在,则返回null。

html
<div id="myDiv" class="bd" myTitle="hello world!">
js
var div = document.getElementById('myDiv'); alert(div.getAttribute('id')); // 'myDiv' alert(div.getAttribute('class')); // 'bd' alert(div.getAttribute('myTitle')); // 'hello world!' alert(div.getAttribute('title')); // null // 对于公认的(非自定义)特性,可以以属性的形式访问 alert(div.id); // 'myDiv' alert(div.myTitle); // undefined(IE除外)

有两类特殊的特性:styleonclick这样的事件处理程序,它们虽然有对应的属性名,但属性的值与通过getAttribute()返回的值并不相同。

设置特性

getAttribute()对应的方法是setAttribute(),这个方法接受两个参数:要设置的特性名和值。如果特性存在则会替换现有的值;如果特性不存在,则创建该属性并设置相应的值。

js
div.setAttribute('id', 'someOtherId'); div.setAttribute('dir', 'rtl');

因为所有特性都是属性,所以直接给属性赋值可以设置特性的值。不过添加自定义属性不会自动转成为元素的特性

js
div.id = 'someOtherId'; div.mycolor = 'red'; // 不会自动转换成为元素的特性

删除特性

removeAttribute()用于彻底删除元素的特性。调用这个方法不仅会清除特性的值,而且也会从元素中完全删除特性。

js
div.removeAttribute('class');

attributes属性

Element类型是使用attributes属性的唯一一个DOM节点类型。attributes属性中包含一个NamedNodeMap,与NodeList类似,也是一个“动态”的集合。元素的每一个特性都由一个Attr节点表示,每个节点都保存在NamedNodeMap对象中。NamedNodeMap对象拥有下列方法。

  • getNamedItem(name):返回nodeName属性等于name的节点;
  • removeNamedItem(name):从列表中移除nodeName属性等于name的节点;
  • setNamedItem(node):向列表中添加节点,以节点的nodeName属性作为索引;
  • item(pos):返回位于数字pos位置处的节点。
js
// 获取元素的id特性 var id = element.attributes.getNamedItem('id').nodeValue; // or var id = element.attributes['id'].nodeValue; // 设置 element.attributes['id'].nodeValue = 'someOtherId';

调用removeNamedItem()方法与在元素上调用removeAttribute()方法效果相同。不会removedNamedItem()会返回被删除特性的Attr节点

js
var oldAttr = element.attributes.removeNamedItem('id');

最后,setNamedItem()是一个很不常用的方法,通过这个方法可以为元素添加一个特性。

js
element.attributes.setNamedItem(newAttr);

由于attributes的方法不够方便,因此开发人员更多的会使用getAttribute()setAttribute()removeAttribute()方法。

如果想要遍历元素的特性,attributes属性倒是可以派上用场。以下代码展示如何迭代元素的每一个特性,然后将它构造成name="value" name="value"这样的字符串格式。

jsx
function outputAttributes (element) { var pairs = new Array(), attrName, attrValue, i, len; for (i = 0, len = element.attributes.length; i < len; i++) { attrName = element.attributes[i].nodeName; attrValue = element.attributes[i].nodeValue; pairs.push(attrName + '="' + attrValue + '"'); } return pairs.join(' '); }

创建元素

使用document.createElement()方法可以创建新元素。这个方法只接受一个参数,即要创建元素的标签名。这个标签名在HTML文档中不区分大小写,而在XML(包括XHTML)文档中,则是区分大小写的。

js
var div = document.createElement('div'); div.id = 'myDiv'; div.className = 'box'; document.body.appendChild(div);

元素的子节点

元素可以有任意数目的子节点和后代节点。元素的childNodes属性中包含了它的所有子节点。

html
<ul> <li>Item 1</li> <li>Item 2</li> <li>Item 3</li> </ul>

如果是IE来解析这段代码,那么<ul>元素会有3个子节点,分别是3个<li>元素。但如果在其他浏览器中,<ul>元素会有7个子节点,包括3个<li>元素和4个文本节点(表示<li>元素之间的空白符)。如果像下面这样将元素间的空白符删除,那么所有浏览器都会返回相同数目的子节点。

html
<ul><li>Item 1</li><li>Item 2</li><li>Item 3</li></ul>

如果需要通过childNodes属性遍历子节点,那么一定不要忘记浏览器间的这一差别。

js
for (var i = 0, len = element.childNodes.length; i < len; i++) { if (element.childNodes[i].nodeType === 1) { // 确保是元素节点 // ... } }

如果想通过某个特定的标签名取得子节点或后代节点该怎么办?实际上,元素也支持getElementsByTagName()方法。

js
var ul = document.getLementById('myList'); var items = ul.getElementsByTagName('li');

Text类型

文本节点由Text类型表示,包含的可以是照字面解释的文本内容。纯文本中可以包含转义后的HTML字符,但不能包含HTMl代码。Text节点具有以下特征:

  • nodeType的值为3;
  • nodeName的值为’#text’;
  • nodeValue的值为节点所包含的文本;
  • parentNode是一个Element;
  • 不支持(没有)子节点。

可以通过nodeValue属性data属性访问Text节点中包含的文本,这两个属性中包含的值相同。

使用下列的方法可以操作节点中的文本:

  • appendData(text):将text添加到节点的末尾。
  • deleteData(offset, count):从offset位置开始删除count个字符。
  • insertData(offset, text):在offset位置插入text。
  • replcaData(offset, count, text):用text替换从offset位置开始到offset+count位置处的文本。
  • splitText(offset):从offset位置将当前文本节点分成两个文本节点。
  • substringData(offset, count):提取从offset位置到offset+count位置处的字符串。

除了这些方法之外,文本节点还有一个length属性,保存着节点中字符的数目,而且nodeValue.lengthtdata.length也保存着同样的值。

创建文本节点

可以使用document.createTextNode()创建新文本节点,这个方法接受一个参数——要插入节点中的文本。作为参数的文本会按照HTML或XMl的格式进行编码。

js
var textNode = document.createTextNode('<strong>Hello</strong>'); // 实际上会插入'&lt;strong&gt;Hello&lt;/strong&gt;'

在创建新文本节点的同时,也会为其设置ownerDocument属性

一般情况下,每个元素只有一个文本子节点。不过在某些情况下也可能包含多个文本子节点。

js
var element = document.createElement('div'); element.className = 'message'; var textNode = document.createTextNode('Hello world!'); element.appendChild(textNode); var anotherTextNode = document.createTextNode('Another Text'); element.appendChild(anotherTextNode); document.body.appendChild(element);

如果两个文本节点是相邻的同胞节点,那么这两个节点的文本会连接起来显示,中间不会有空格。

规范化文本节点

DOM文档中存在相邻的同胞文本节点很容易导致混乱,因为分不清哪个文本节点表示哪个字符串。于是就有了一个将相邻文本节点合并的方法。这个方法是由Node类型定义的(因而在所有节点类型中都存在),名叫normalize()

js
// 接上例 alert(element.childNodes.length); // 2 element.normalize(); alert(element.childNodes.length); // 1 alert(element.firstChild.nodeValue); // 'Hello world!Another Text'

分割文本节点

Text类型提供了一个作用与normalize()相反的方法:splitText()。这个方法会将一个文本节点分成两个文本节点,即按照指定的位置分割nodeValue值。原来的文本节点将包含从开始位置到指定位置之前的内容,新文本节点将包含剩下的文本。这个方法会返回一个新文本节点。

js
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); // 'Helllo' alert(newNode.nodeValue); // ' world' alert(element.childNodes.length); // 2

Comment类型

注释在DOM中是通过Comment类型来表示的。Comment节点具有下列特征:

  • nodeType的值为8;
  • nodeName的值为’#comment’;
  • nodeValue的值是注释的内容;
  • parentNode可能是Document或Element;
  • 不支持(没有)子节点。

Comment类型与Text类型继承自相同的基类,因此它拥有除splitText()之外的所有字符串操作方法。

CDATASection类型

CDATASection类型只针对基于XML的文档,表示的是CDATA区域。与Comment类型类似。CDATASection节点具有以下特征:

  • nodeType的值为4;
  • nodeName的值为’#cdata-section’;
  • nodeValue的值是CDATA区域中的内容;
  • parentNode可能是Document或Element;
  • 不支持(没有)子节点。

DocumentType类型

DocumentType类型在Web浏览器中并不常用。它具有以下特征:

  • nodeType的值为10;
  • nodeName的值为doctype的名称;
  • nodeValue的值是null;
  • parentNode是Document;
  • 不支持(没有)子节点。

在DOM1级中,DocumentType对象不能动态创建,而只能通过解析文档代码的方式来创建。支持它的浏览器会把DocumentType对象保存在document.doctype中。DOM1级描述了DocumentType对象的3个属性:name(文档类型描述的名称)、entities(文档类型描述的实体NamedNodeMap对象)和notations(文档类型描述的符号NamedNodeMap对象)。

DocumentFragment类型

在所有节点类型中,只有DocumentFragment在文档中没有对应的标记。DOM规定文档片段(document fragment)是一种“轻量级”的文档,可以包含和控制节点,但不会像完整的文档那样占用额外的资源。DocumentFragment节点具有以下特征:

  • nodeType的值为11;
  • nodeName的值为’#document-fragment’;
  • nodeValue的值为null;
  • parentNode的值为null;
  • 子节点可以是Element、ProcessingInstruction、Comment、Text、CDATASection或EntityReference。

虽然不能把文档版本添加到文档中,但可以将它作为一个“仓库”来使用,即可以在里面保存将来可能会添加到文档中的节点。

可以使用document.createDocumentFragment()方法来创建文档片段。

js
var fragment = document.createDocumentFragment();

文档版本继承了Node的所有方法,通常用于执行那些针对文档的DOM操作。如果将文档中的节点添加到文档片段中,就会从文档树中移除该节点,也不会从浏览器中再看到该节点。添加到文档片段中的新节点也不属性文档树。可以通过appendChild()insertBefore()将文档片段中的内容添加到文档中。在将文档片段作为参数传递给这两个方法时,只会将文档片段的所有子节点添加到相应位置,文档片段本身永远不会成为文档树的一部分。

Attr类型

元素的特性在DOM中以Attr类型来表示。特性节点具有以下特征:

  • nodeType的值为2;
  • nodeName的值为特性的名称;
  • nodeValue的值为特性的值;
  • parentNode的值为null;
  • 在HTML中不支持(没有)子节点;
  • 在XML中子节点可以是Text或EntityReference。

Attr对象有3个属性:namevaluespecified(布尔值,用于区分特性是在代码中指定的,而且是默认的)。

使用document.createAttribute()关传入特性名称可以创建新的特性节点。

js
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'

DOM操作技术

动态脚本

使用<script>元素可以向页面插入JavaScript代码,一种方式是通过其src特性包含外部文件,另一种方式就是用这个元素本身来包含代码。而这里讨论的动态脚本,指的是在页面加载时不存在,但将来的某一时刻通过修改DOM动态添加脚本。

js
var script = document.createElement('srcipt'); script.type = 'text/javascript'; script.src = 'client.js'; document.body.appendChild(script); // or var script = document.createElement('srcipt'); script.type = 'text/javascript'; script.appendChild(document.createTextNode('function sayHi () { alert("hi"); }')); document.body.appendChild(script);

在Firefox、Safari、Chrome和Opera中,这些DOM代码可以正常运行。但在IE中,则会导致IE将<script>视为一个特殊的元素,不允许DOM访问其子节点。不过,可以使用<script>元素text属性来指定JavaScript代码。

js
var script = document.createElement('srcipt'); script.type = 'text/javascript'; script.text('function sayHi () { alert("hi"); }'); document.body.appendChild(script); // 兼容性版本 var script = document.createElement('srcipt'); script.type = 'text/javascript'; var code = 'function sayHi () { alert("hi"); }'; try { script.appendChild(document.createTextNode(code)); } catch (e) { script.text = code; } document.body.appendChild(script);

以这种方式加载的代码会在全局作用域中执行,而且当脚本执行后立即可用。实际上,这样执行代码与全局作用域中把相同的字符串传递给eval()是一样的。

动态样式

js
var style = document.createElement('style'); style.type = 'text/css'; try { style.appendChild(document.createTextNode('body { background-color: red }')); } catch (e) { style.styleSheet.cssText = 'body { background: red }'; } document.getElementsByTagName('head')[0].appendChild(style);

操作表格

<table>元素是HTML中最复杂的结构之一。要想创建表格,一般都必须涉及表格行、单元格、表头等标签。由于涉及的标签多,因而使用核心DOM方法创建和修改表格往往都免不了要编写大量的代码。

为了方便构建表格,DOM还为<table><tbody><tr>元素添加了一些属性和方法。

<table>元素添加的属性和方法

  • caption:保存着对<caption>元素(如果有)的指针。
  • tBodies:是一个<tbody>元素的HTMLCollection。
  • tFoot:保存着对<tfoot>元素(如果有)的指针。
  • tHead:保存着对<thead>元素(如果有)的指针。
  • rows:是一个表格中所有行的HTMLCollection。
  • createTHead():创建<thead>元素,返回引用。
  • createTFoot():创建<tfoot>元素,返回引用。
  • createCaption():创建<caption>元素,返回引用。
  • deleteTHead():删除<thead>元素
  • deleteTFoot():删除<tfoot>元素
  • deleteCaption():删除<caption>元素
  • deleteRow(pos):删除指定位置的行。
  • insertRow(pos):向rows集合中的指定位置插入一行。

<tbody>元素添加的属性和方法

  • rows: 保存着<tbody>元素中行的HTMLCollection。
  • deleteRow(pos):删除指定位置的行。
  • insertRow(pos):向rows集合中的指定位置插入一行,返回对新插入行的引用。

<tr>元素添加的属性和方法

  • cells:保存着<tr>元素中单元格的HTMLCollection。
  • deleteCell(pos):删除指定的单元格。
  • insertCell(pos):向cells集合中的指定位置插入一个单元格,返回对新插入单元格的引用。

使用NodeList

理解NodeList及其“近亲”NamedNodeMapHTMLCollection,是从整体上透彻理解DOM的关键所有。这三个集合都是“动态的”:换句话说,每当文档结构发生变化时,它们都会得到更新。因此,它们始终都会保存着最新、最准确的信息。从本质上说,所有NodeList对象都是在访问DOM文档时实时运行的查询。例如,下面的代码会导致无限循环:

js
var divs = document.getElementsByTagName('div'), i, div; for (i = 0; i < divs.length; i++) { div = document.createElement('div'); document.body.appendChild(div); }

如果想要迭代一个NodeList,最好是使用length属性初始化第二个变量,然后将迭代器与该变量进行比较。

js
var divs = document.getElementsByTagName('div'), i, len, div; for (i = 0, len = divs.length; i < len; i++) { div = document.createElement('div'); document.body.appendChild(div); }

一般来说,应该尽量减少访问NodeList的次数。因为每次访问,都会运行一次基于文档的查询。