虽然DOM
为与XML
及HTML
文档交互制定了一系列核心API
,但仍然有几个规范对标准的DOM
进行了扩展。这些扩展中有很多原来是浏览器专有的,但后来成为了事实标准,于是其他浏览器也都提供了相同的实现。
一、选择符API
众多JavaScript
库中最常用的一项功能,就是根据CSS
选择符选择与某个模式匹配的DOM
元素。jQuery(www.jquery.com)的核心就是通过CSS
选择符查询DOM
文档取得元素的引用,从而抛开了getElementById()
和getElementsByTagName()
。
所有实现这一功能的JavaScript
库都会写一个基础的CSS
解析器,然后再使用已有的DOM
方法查询文档并找到匹配的节点。而把这个功能变成原生API
之后,解析和树查询操作可以在浏览器内部通过编译后的代码来完成,极大地改善了性能。
querySelector()方法
querySelector()
方法接收一个CSS
选择符(字符串),返回与该模式匹配的第一个元素,如果没有找到匹配的元素,返回null
。
//取得body 元素
var body = document.querySelector("body");
//取得ID 为"myDiv"的元素
var myDiv = document.querySelector("#myDiv");
//取得类为"selected"的第一个元素
var selected = document.querySelector(".selected");
//取得类为"button"的第一个图像元素
var img = document.body.querySelector("img.button");
通过Document
类型调用querySelector()
方法时,会在文档元素的范围内查找匹配的元素。而通过Element
类型调用querySelector()
方法时,只会在该元素后代元素的范围内查找匹配的元素。如果传入了不被支持的选择符,querySelector()
会抛出错误。
querySelectorAll()方法
querySelectorAll()
方法接收的参数与querySelector()
方法一样,都是一个CSS
选择符,但返回的是所有匹配的元素而不仅仅是一个元素。返回的值实际上是带有所有属性和方法的NodeList
,而其底层实现则类似于一组元素的快照,而非不断对文档进行搜索的动态查询,不是动态的。这样实现可以避免使用NodeList
对象通常会引起的大多数性能问题。如果没有找到匹配的元素,NodeList
就是空的。如果传入了不被支持的选择符,querySelectorAll()
会抛出错误。
要取得返回的NodeList
中的每一个元素,可以使用item()
方法,也可以使用方括号语法。NodeList[i]
或者NodeList.item(i)
二、元素遍历
对于元素间的空格,IE9 及之前版本不会返回文本节点,而其他所有浏览器都会返回文本节点。这样,就导致了在使用childNodes
和firstChild
等属性时的行为不一致。为了弥补这一差异,而同时又保持DOM
规范不变,因此新定义了一组属性,从而可以更方便地查找DOM
元素了。
-
childElementCount
:返回子元素(不包括文本节点和注释)的个数。 -
firstElementChild
:指向第一个子元素;firstChild
的元素版。 -
lastElementChild
:指向最后一个子元素;lastChild
的元素版。 -
previousElementSibling
:指向前一个同辈元素;previousSibling
的元素版。 -
nextElementSibling
:指向后一个同辈元素;nextSibling
的元素版。
三、HTML5与DOM 节点相关部分
与类相关的扩充
1、getElementsByClassName()
方法
该方法接收一个参数,即一个包含一或多个类名的字符串,返回带有指定类的所有元素的NodeList
。传入多个类名时,类名的先后顺序不重要。
//取得所有类中包含"username"和"current"的元素,类名的先后顺序无所谓
var allCurrentUsernames = document.getElementsByClassName("username current");
//取得ID 为"myDiv"的元素中带有类名"selected"的所有元素
var selected = document.getElementById("myDiv").getElementsByClassName("selected");
在document
对象上调用getElementsByClassName()
始终会返回与类名匹配的所有元素,在元素上调用该方法就只会返回后代元素中匹配的元素。因为返回的对象是NodeList
,所以使用这个方法与使用getElementsByTagName()
以及其他返回NodeList
的DOM
方法都具有同样的性能问题。
2、classList
属性
在操作类名时,需要通过className
属性添加、删除和替换类名。因为className
中是一个字符串,所以即使只修改字符串一部分,也必须每次都设置整个字符串的值。
因此,HTML5
为所有元素添加了classList
属性。这个classList
属性是新集合类型DOMTokenList
的实例。与其他DOM
集合类似,DOMTokenList
有一个表示己包含多少元素的length
属性,而要取得每个元素可以使用item()
方法,也可以使用方括号语法。此外,这个新类型还定义如下方法。
-
add(value)
:将给定的字符串值添加到列表中。如果值已经存在,就不添加了。 -
contains(value)
:表示列表中是否存在给定的值,如果存在则返回true
,否则返回false
。 -
remove(value)
:从列表中删除给定的字符串。 -
toggle(value)
:如果列表中已经存在给定的值,删除它;如果列表中没有给定的值,添加它。
//删除"disabled"类
oDiv.classList.remove("disabled");
//添加"current"类
oDiv.classList.add("current");
//切换"user"类
oDiv.classList.toggle("user");
//确定元素中是否包含既定的类名
if (oDiv.classList.contains("bd") && !oDiv.classList.contains("disabled")){
//执行操作
)
//迭代类名
for (var i=0, len=div.classList.length; i < len; i++){
doSomething(div.classList[i]);
}
焦点管理
HTML5
也添加了辅助管理DOM
焦点的功能。首先就是document.activeElement
属性,这个属性始终会引用DOM
中当前获得了焦点的元素。元素获得焦点的方式有页面加载、用户输入(通常是通过按Tab
键)和在代码中调用focus()
方法。
var button = document.getElementById("myButton");
button.focus();
if(document.activeElement === button){
// 当前获取的焦点是button时 需要做的操作
}
默认情况下,文档刚刚加载完成时,document.activeElement
中保存的是document.body
元素的引用。文档加载期间,document.activeElement
的值为null
。
另外就是新增了document.hasFocus()
方法,这个方法用于确定文档(页面)是否获得了焦点。
var button = document.getElementById("myButton");
button.focus();
alert(document.hasFocus()); //true
通过检测文档是否获得了焦点,可以知道用户是不是正在与页面交互。
HTMLDocument的变化
1、readyState
属性
Document
的readyState
属性有两个可能的值:
-
"loading"
,正在加载文档; -
"complete"
,已经加载完文档。
使用document.readyState
的最恰当方式,就是通过它来实现一个指示文档已经加载完成的指示器。在这个属性得到广泛支持之前,要实现这样一个指示器,必须借助onload
事件处理程序设置一个标签,表明文档已经加载完毕。document.readyState 属性的基本用法如下。
if (document.readyState == "complete"){
//执行操作
}
2、兼容模式
自从IE6 开始区分渲染页面的模式是标准的还是混杂的,检测页面的兼容模式就成为浏览器的必要功能。IE 为此给document
添加了一个名为compatMode
的属性,这个属性就是为了告诉开发人员浏览器采用了哪种渲染模式。在标准模式下,document.compatMode
的值等于"CSS1Compat"
,而在混杂模式下,document.compatMode
的值等于"BackCompat"
。
3、head 属性
作为对document.body
引用文档的<body>
元素的补充,HTML5
新增了document.head
属性,引用文档的<head>
元素。要引用文档的<head>
元素,可以结合使用这个属性和另一种后备方法。
var head = document.head || document.getElementsByTagName("head")[0];
// 实现document.head 属性的浏览器包括Chrome 和Safari 5。
字符集属性
HTML5
新增了几个与文档字符集有关的属性。其中,charset
属性表示文档中实际使用的字符集,也可以用来指定新字符集。默认情况下,这个属性的值为"UTF-16"
,但可以通过<meta>
元素、响应头部或直接设置charset
属性修改这个值。
alert(document.charset); //"UTF-16"
document.charset = "UTF-8";
自定义数据属性
HTML5
规定可以为元素添加非标准的属性,但要添加前缀data-
,目的是为元素提供与渲染无关的信息,或者提供语义信息。这些属性可以任意添加、随便命名,只要以data-
开头即可。
添加了自定义属性之后,可以通过元素的dataset
属性来访问自定义属性的值。dataset
属性的值是DOMStringMap
的一个实例,也就是一个名值对儿的映射。在这个映射中,每个data-name
形式的属性都会有一个对应的属性,只不过属性名没有data-
前缀(比如,自定义属性是data-myname
,那映射中对应的属性就是myname
)。
HTML
<div id="myDiv" data-appId="12345" data-myname="Nicholas"></div>
JS
var div = document.getElementById("myDiv");
//取得自定义属性的值
var appId = div.dataset.appId;
var myName = div.dataset.myname;
//设置值
div.dataset.appId = 23456;
div.dataset.myname = "Michael";
//有没有"myname"值呢?
if (div.dataset.myname){
alert("Hello, " + div.dataset.myname);
}
插入标记
1.、innerHTML
属性
在读模式下,innerHTML
属性返回与调用元素的所有子节点(包括元素、注释和文本节点)对应的HTML 标记。在写模式下,innerHTML
会根据指定的值创建新的DOM
树,然后用这个DOM
树完全替换调用元素原先的所有子节点。。如果设置的值仅是文本而没有HTML
标签,那么结果就是设置纯文本,设置innerHTML
之后,可以像访问文档中的其他节点一样访问新创建的节点。
使用innerHTML
属性也有一些限制。比如,在大多数浏览器中,通过innerHTML
插入<script>
元素并不会执行其中的脚本。IE8
及更早版本是唯一能在这种情况下执行脚本的浏览器,但必须满足一些条件。一是必须为<script>
元素指定defer
属性,二是<script>
元素必须位于(微软所谓的)“有作用域的元素”(scoped element
)之后。<script>元素被认为是“无作用域的元素”(NoScope element
),也就是在页面中看不到的元素,与<style>
元素或注释类似。如果通过innerHTML
插入的字符串开头就是一个“无作用域的元素”,那么IE 会在解析这个字符串前先删除该元素。换句话说,以下代码达不到目的:
div.innerHTML = "<script defer>alert('hi');<\/script>"; //无效
div.innerHTML = "_<script defer>alert('hi');<\/script>";
div.innerHTML = "<div> </div><script defer>alert('hi');<\/script>";
div.innerHTML = "<input type=\"hidden\"><script defer>alert('hi');<\/script>";
第一行代码会在<script>
元素前插入一个文本节点。事后,为了不影响页面显示,你可能需要移除这个文本节点div.removeChild(div.firstChild);
第二行代码采用的方法类似,只不过使用的是一个包含非换行空格的<div>
元素。如果仅仅插入一个空的<div>
元素,还是不行;必须要包含一点儿内容,浏览器才会创建文本节点。同样,为了不影响页面布局,恐怕还得移除这个节点。
第三行代码使用的是一个隐藏的<input>
域,也能达到相同的效果。不过,由于隐藏的<input>
域不影响页面布局,因此这种方式在大多数情况下都是首选。
但在IE8 及更早版本中,<style>
也是一个“没有作用域的元素”,因此必须像下面这样给它前置一个“有作用域的元素”:
div.innerHTML = "_<style type=\"text/css\">body {background-color: red; }</style>";
div.removeChild(div.firstChild);
2、outerHTML
属性
在读模式下,outerHTML
返回调用它的元素(包含调用元素本身)以及所有子节点的HTML
标签。在写模式下,outerHTML
会根据指定的HTML
字符串创建新的DOM
子树,然后用这个DOM
子树完全替换调用元素。
div.outerHTML = "<p>This is a paragraph.</p>";
// p元素会完全替换div元素
等同于
var p = document.createElement("p");
p.appendChild(document.createTextNode("This is a paragraph."));
div.parentNode.replaceChild(p, div);
3、insertAdjacentHTML()
方法
将指定的文本解析为HTML
或XML
,并将结果节点插入到DOM
树中的指定位置。它不会重新解析它正在使用的元素,因此它不会破坏元素内的现有元素。这避免了额外的序列化步骤,使其比直接innerHTML
操作更快。
它接收两个参数:插入位置和要插入的HTML
文本。第一个参数必须是下列值之一:
-
"beforebegin"
,在当前元素之前插入一个紧邻的同辈元素; -
"afterbegin"
,在当前元素之下插入一个新的子元素或在第一个子元素之前再插入新的子元素; -
"beforeend"
,在当前元素之下插入一个新的子元素或在最后一个子元素之后再插入新的子元素; -
"afterend"
,在当前元素之后插入一个紧邻的同辈元素。
//作为前一个同辈元素插入
element.insertAdjacentHTML("beforebegin", "<p>Hello world!</p>");
//作为第一个子元素插入
element.insertAdjacentHTML("afterbegin", "<p>Hello world!</p>");
//作为最后一个子元素插入
element.insertAdjacentHTML("beforeend", "<p>Hello world!</p>");
//作为后一个同辈元素插入
element.insertAdjacentHTML("afterend", "<p>Hello world!</p>");
4、内存与性能问题
在删除带有事件处理程序或引用了其他JavaScript
对象子树时,就有可能导致内存占用问题。假设某个元素有一个事件处理程序(或者引用了一个JavaScript
对象作为属性),在使用前述innerHTML
、outerHTML
属性将该元素从文档树中删除后,元素与事件处理程序(或JavaScript
对象)之间的绑定关系在内存中并没有一并删除。如果这种情况频繁出现,页面占用的内存数量就会明显增加。因此,在使用innerHTML
、outerHTML
属性和insertAdjacentHTML()
方法时,最好先手工删除要被替换的元素的所有事件处理程序和JavaScript
对象属性
不可避免地,创建和销毁HTML
解析器也会带来性能损失,所以最好能够将设置innerHTML
或outerHTML
的次数控制在合理的范围内,所以要尽量避免下面这种操作。
for (var i=0, len=values.length; i < len; i++){
ul.innerHTML += "<li>" + values[i] + "</li>"; //要避免这种频繁操作!!
}
可以先将单独构建字符串,之后一次把结果字符串赋值给innerHTML
var itemsHtml = "";
for (var i=0, len=values.length; i < len; i++){
itemsHtml += "<li>" + values[i] + "</li>";
}
ul.innerHTML = itemsHtml;
// 这个例子的效率要高得多,因为它只对innerHTML 执行了一次赋值操作。
scrollIntoView()方法
scrollIntoView()
可以在所有HTML
元素上调用,通过滚动浏览器窗口或某个容器元素,调用元素就可以出现在视口中。如果给这个方法传入true
作为参数,或者不传入任何参数,那么窗口滚动之后会让调用元素的顶部与视口顶部尽可能平齐。如果传入false
作为参数,调用元素会尽可能全部出现在视口中,(可能的话,调用元素的底部会与视口顶部平齐。)不过顶部不一定平齐。
//让元素可见
document.forms[0].scrollIntoView();
四、专有扩展
虽然所有浏览器开发商都知晓坚持标准的重要性,但在发现某项功能缺失时,这些开发商都会一如既往地向DOM
中添加专有扩展,以弥补功能上的不足。表面上看,这种各行其事的做法似乎不太好,但实际上专有扩展为Web
开发领域提供了很多重要的功能,这些功能最终都在HTML5
规范中得到了标准化。即便如此,仍然还有大量专有的DOM
扩展没有成为标准,(并不是说明它们将来不会被写进标准)
1、文档模式
IE8
引入了一个新的概念叫“文档模式”(document mode
)。页面的文档模式决定了可以使用什么功能。换句话说,文档模式决定了你可以使用哪个级别的CSS
,可以在JavaScript
中使用哪些API
,以及如何对待文档类型(doctype
)。到了IE9
,总共有以下4 种文档模式。
-
IE5
:以混杂模式渲染页面(IE5 的默认模式就是混杂模式)。IE8 及更高版本中的新功能都无法使用。 -
IE7
:以IE7 标准模式渲染页面。IE8 及更高版本中的新功能都无法使用。 -
IE8
:以IE8 标准模式渲染页面。IE8 中的新功能都可以使用,因此可以使用Selectors API、更多CSS2 级选择符和某些CSS3 功能,还有一些HTML5 的功能。不过IE9 中的新功能无法使用。 -
IE9
:以IE9 标准模式渲染页面。IE9 中的新功能都可以使用,比如ECMAScript 5、完整的CSS3以及更多HTML5 功能。这个文档模式是最高级的模式。
要强制浏览器以某种模式渲染页面,可以使用HTTP
头部信息X-UA-Compatible
,或通过等价的<meta>
标签来设置:
比如,要想让文档模式像在IE7 中一样,可以使用下面这行代码:
<meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7">
如果不打算考虑文档类型声明,而直接使用IE7 标准模式,那么可以使用下面这行代码:
<meta http-equiv="X-UA-Compatible" content="IE=7">
这里IE
的版本(IEVersion
)有以下一些不同的值。
-
Edge
:始终以最新的文档模式来渲染页面。忽略文档类型声明。对于IE8,始终保持以IE8 标准模式渲染页面。对于IE9,则以IE9 标准模式渲染页面。 -
EmulateIE9
:如果有文档类型声明,则以IE9 标准模式渲染页面,否则将文档模式设置为IE5。 -
EmulateIE8
:如果有文档类型声明,则以IE8 标准模式渲染页面,否则将文档模式设置为IE5。 -
EmulateIE7
:如果有文档类型声明,则以IE7 标准模式渲染页面,否则将文档模式设置为IE5。 -
9
:强制以IE9 标准模式渲染页面,忽略文档类型声明。 -
8
:强制以IE8 标准模式渲染页面,忽略文档类型声明。 -
7
:强制以IE7 标准模式渲染页面,忽略文档类型声明。 -
5
:强制将文档模式设置为IE5,忽略文档类型声明。
通过document.documentMode
属性可以知道给定页面使用的是什么文档模式。这个属性是IE8
中新增的,它会返回使用的文档模式的版本号(在IE9 中,可能返回的版本号为5、7、8、9):
var mode = document.documentMode;
知道页面采用的是什么文档模式,有助于理解页面的行为方式。无论在什么文档模式下,都可以访问这个属性。
2、contains()
方法
在实际开发中,经常需要知道某个节点是不是另一个节点的后代。IE为此率先引入了contains()
方法,以便不通过在DOM
文档树中查找即可获得这个信息。调用contains()
方法的应该是祖先节点,也就是搜索开始的节点,这个方法接收一个参数,即要检测的后代节点。如果被检测的节点是后代节点,该方法返回true
;否则,返回false
。
alert(document.documentElement.contains(document.body)); //true
这个例子测试了<body>
元素是不是<html>
元素的后代,在格式正确的HTML
页面中,以上代码返回true
。
3、插入文本
前面介绍过,IE
原来专有的插入标记的属性innerHTML
和outerHTML
已经被HTML5
纳入规范。但另外两个插入文本的专有属性则没有这么好的运气。这两个没有被HTML5
看中的属性是innerText
和outerText
。
-
innerText
属性
通过innertText
属性可以操作元素中包含的所有文本内容,包括子文档树中的文本。在通过innerText
读取值时,它会按照由浅入深的顺序,将子文档树中的所有文本拼接起来。在通过innerText
写入值时,结果会删除元素的所有子节点,插入包含相应文本值的文本节点。
设置innerText
属性移除了先前存在的所有子节点,完全改变了DOM
子树。此外,设置innerText
属性的同时,也对文本中存在的HTML
语法字符(小于号、大于号、引号及和号)进行了编码。再看一个例子。
div.innerText = "Hello & welcome, <b>\"reader\"!</b>";
会得到以下dom树
<div id="content">Hello & welcome, <b>"reader"!</b></div>
设置innerText
永远只会生成当前节点的一个子文本节点,而为了确保只生成一个子文本节点,就必须要对文本进行HTML
编码(因为可能字符串会包含HTML
的标签,这样可能会生成子元素,所以要进行HTML
编码)。利用这一点,可以通过innerText
属性过滤掉HTML 标签。方法是将innerText
设置为等于innerText
,这样就可以去掉所有HTML
标签(因为innerText
属性读的时候会拿到元素中所有的文档内容并拼接起来,这样即使有HTML
标签被插入了,在读的时候也会被过滤掉,然后在赋值。)
// HTML
<div class="oDiv">
"Hello & welcome, <b>\"reader\"!</b>"
</div>
// JS
var oDiv = document.getElementsByClassName('oDiv')[0];
console.log(oDiv.innerText); // "Hello & welcome, \"reader\"!";
oDiv.innerText = oDiv.innerText; ;
-
outerText
属性
在读取文本值时,outerText
与innerText
的结果完全一样。但在写模式下,outerText
就完全不同了:outerText
不只是替换调用它的元素的子节点,而是会替换整个元素(包括子节点)。
div.outerText = "Hello world!";
等同于
var text = document.createTextNode("Hello world!");
div.parentNode.replaceChild(text, div);
由于这个属性会导致调用它的元素不存在,因此并不常用。也建议尽可能不要使用这个属性。
4、滚动
如前所述,HTML5
之前的规范并没有就与页面滚动相关的API
做出任何规定。但HTML5
在将scrollIntoView()
纳入规范之后,仍然还有其他几个专有方法可以在不同的浏览器中使用。下面列出的几个方法都是对HTMLElement
类型的扩展,因此在所有元素中都可以调用。
scrollIntoViewIfNeeded(alignCenter)
:只在当前元素在视口中不可见的情况下,才滚动浏览器窗口或容器元素,最终让它可见。如果当前元素在视口中可见,这个方法什么也不做。如果将可选的alignCenter
参数设置为true
,则表示尽量将元素显示在视口中部(垂直方向)。Safari 和Chrome 实现了这个方法。scrollByLines(lineCount)
:将元素的内容滚动指定的行高,lineCount
值可以是正值,也可以是负值。Safari 和Chrome 实现了这个方法。scrollByPages(pageCount)
:将元素的内容滚动指定的页面高度,具体高度由元素的高度决定。Safari 和Chrome 实现了这个方法。
希望大家要注意的是,
scrollIntoView()
和scrollIntoViewIfNeeded()
的作用对象是元素的容器,而scrollByLines()
和scrollByPages()
影响的则是元素自身。
//在当前元素不可见的时候,让它进入浏览器的视口
document.images[0].scrollIntoViewIfNeeded();
//将页面主体往回滚动1 页
document.body.scrollByPages(-1);