共计 12493 个字符,预计需要花费 32 分钟才能阅读完成。
- 尽管 DOM 作为 API 已经非常完善了,但为了实现更多功能,仍然会有一些标准或专有的拓展。2008 年之前,浏览器中几乎所有的拓展都是专有的,此后 W3C 着手将一些已经成为事实标准的专有拓展标准化,并写入规范中。
- 对 DOM 的两个主要拓展是 Selectors API (选择 API)和 HTML5。这两个拓展都源自开发社区,而将某些常见做啊及 API 标准化,一直是众望所归。
选择符 API
- jQuery 的核心就是通过 CSS 选择符查询 DOM 文档取得元素的引用,从而抛开了
getElementById()
和gettElementByTagName()
。 - Selectors API 是又 W3C 发起制定的一个标准,致力于让浏览器原生支持 CSS 查询。所有实现这一功能的 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 selected = document.querySelector("img.button");
- 如果传入了不支持的选择符,会抛出错误。
querySelectorAll()
方法
- 和上面类似,但返回的不仅仅是匹配的第一个元素,而是一个
NodeList
的实例,如果没有匹配,NodeList
就是空的。
// 取得某 div 中所有 em 元素
var ems = document.getElementById("myDiv").querySelectorAll("em");
// 取得类为“selected”的所有元素
var selected = document.querySelectorAll(".selected");
// 取得所有 p 元素中的所有 strong 元素
var strongs = document.querySelectorAll("p strong");
- 取得返回
NodeList
中的每一个元素,可以使用item()
方法,也可以使用方括号语法。
matchesSelector()
方法
- Selector API Level2 规范为 Element 类型新增了一个
matchesSelector()
方法。接收一个参数,即 CSS 选择符,如果调用元素与该选择符匹配返回 true,否则返回 false。 - 截止 2011 年年中,还没有浏览器支持该方法,不过有一些实验性的实现。因此如果你想使用这个方法,最好编写一个包装函数:
function matchesSelector(element, selector) {if (element.matchesSelecotr) {return element.matchesSelecotr(selector)
} else if (element.msMatchesSelecotr) {return element.msMatchesSelecotr(selector)
} else if (element.mozMatchesSelecotr) {return element.mozMatchesSelecotr(selector)
} else if (element.webkitMatchesSelecotr) {return element.webkitMatchesSelecotr(selector)
} else {throw new Error("Not supported.");
}
}
元素遍历
-
对于元素间的空格,IE9 及之前版本不会返回文本节点,而其他所有浏览器都会返回文本节点。为了弥补这一差异,而又同时保持 DOM 规范不变,Element Traversal 规范新定义了一组属性。
-
childElementCount
: 返回子元素(不包括文本节点和注释)的个数 -
firstElementChild
: 指向第一个子元素;firstChild
的元素版 -
lastElementChild
: 指向最后一个子元素;lastChild
的元素版 -
previousElementSibling
: 指向前一个同辈元素;previousSibling
的元素版 -
nextElementSibling
: 指向前一个同辈元素;nextSibling
的元素版
-
// 跨浏览器遍历某元素的所有子元素
// 老版的兼容性代码
var i,
len,
child = element.firstChild;
while(child != element.lastChild) {
// 检查是不是元素
if (child.nodeType == 1) {processChild(child);
}
child = child.nextSibling;
}
// 使用新版的方法
var i,
len,
child = element.firstChild;
while(child != element.lastElementChild) {processChild(child);
child = child.nextElementSibling;
}
- 支持 Element Traversal 规范的浏览器有 IE9+ Firefox3.5+ Safari4+ Chrome Opera10+
HTML5
- HTML5 所有之前的版本对 JavaScript 接口的描述不过三言两语,主要篇幅都用于定义标记,与 JavaScript 相关的内容一概交由 DOM 规范去定义。
- HTML5 规范则围绕如何使用新增标记定义了大量 JavaScript API。其中一些 API 与 DOM 重叠,定义了浏览器应该支持的 DOM 拓展。
- 由于 HTML5 涉及的面非常广,本节只讨论与 DOM 节点相关的内容。
与类(class)相关的扩充
- HTML5 新增了很多 API,致力于简化 CSS 类的用法。
getElementsByClassName()
方法
// 取得所有类中包含 "username" 和 "current" 的元素
// 类名的先后顺序无所谓
var allCurrentUsernames = docment.getElementsByClassName("username current");
// 取得 ID 为 "myDiv" 的元素中带有类名 "selected" 的所有元素
var selected = document.getElementById("myDiv").getElemenstByClassName("selected");
- 支持
getElementsByClassName()
方法的浏览器 IE9+ Firefox3+ Safari3.1+ Chrome Opera9.5+
classList 属性
- 在操作类名时,需要通过
className
属性添加、删除和替换类名。因为className
中是一个字符串,所以即使只是修改字符串一部分,也必须每次都设置整个字符串的值。 - HTML5 新增了一种操作类名的方式,可以让操作更简单也更安全,那就是为所有元素添加了
classList
属性。 -
classList
属性是新集合类型 DOMTokenList 的实例。与其他 DOM 集合类似,DOMTokenList 有一个表示自己包含多少元素的 length 属性,而要去的每个元素可以使用item()
方法,或者方括号语法。此外,这个新类型还定义如下方法:-
add(value)
: 将给定的字符串值添加到列表中。如果值已经存在,就不添加了。 -
contains(value)
: 表示列表中是否存在给定的值,如果存在返回 true,反之 false。 -
remove(value)
: 从列表中删除给定的字符串。 -
toggle(value)
: 如果列表中已经存在给定的值,删除它;如果没有,添加它。
-
- 有了
classList
属性,除非你需要删除所有类名,或者完全重写元素的 class 属性,否则就用不到className
属性了。 - 支持
classList
的浏览器 Firefox3.6+ Chrome
焦点管理
- HTML5 添加了辅助 DOM 焦点的功能。
-
document.activeElement
属性,始终会引用 DOM 中当前获得了焦点的元素。元素获得焦点的方式有页面加载、用户输入(通常是通过 Tab 键)和在代码中调用focus()
方法。
var button = document.getElementById("myButton");
button.focus();
console.log(document.activeElement === button); // true
- 默认情况下,文档刚刚加载完成时,
document.activeElement
中保存的是document.body
元素的引用。文档加载期间,document.activeElement
的值为null
-
document.hasFocus()
方法,这个方法用于确定文档是否获得了焦点。
var button = document.getElementById("myButton");
button.focus();
console.log(document.hasFocus()); // true
- 支持的浏览器 IE4+ Firefox3+ Safari4+ Chrome Opera8+
HTMLDocument 的变化
readyState 属性
-
Document.readyState
属性有两个可能的值:-
loading
正在加载文档 -
complete
已经加载完文档
-
- 支持的浏览器 IE4+ Firefox3.6+ Safari Chrome Opera9+
if (document.readyState == "complete") {...}
兼容模式
- 自从 IE6 开始区分渲染页面的模式是标准的还是混杂的,检测页面的兼容性就成为浏览器的必要功能。IE 为此给 document 添加了一个名为
compatMode
的属性,告诉开发人员浏览器采用了哪种渲染模式。 -
document.compatMode
标准模式下等于 ”CSS1Compat”, 混杂模式下等于 ”BackCompat”。 - 最终 HTML5 将这个属性纳入标准
- 支持的浏览器 IE Firefox Safari3.1 Chrome Opera
if (document.compatMode == "CSS1Compat") {console.log("Standards mode");
} else {console.log("Quirks mode");
}
head 属性
- HTML5 新增了
document.head
属性,与docuemnt.body
对应 - 支持的浏览器 Chrome Safari 5+
var head = document.head || document.getElementsByTagName("head")[0];
字符集属性
- HTML5 新增了几个与文档字符集有关的属性
-
charset
属性表示文档中实际使用的字符集,也可以用来指定新字符集。默认值是 ”UTF-16″,可以通过<meta>
元素、响应头部或直接设置charset
属性修改这个值。 - 支持的浏览器 IE Safari Opera Chrome。Firefox 支持
document.Characterset
。
console.log(document.charset); // "UTF-16"
document.charset = "UTF-8";
-
defaultCharset
表示根据默认浏览器及操作系统的设置,当前文档默认的字符集应该是什么。如果文档没有使用默认的字符集,那charset
和defaultCharset
属性值可能会不一样。
if (document.charset != document.defaultCharset) {console.log("Custom character set being used.");
}
- 支持的浏览器 IE Safari Chrome。
自定义数据属性
- HTML5 规定可以为元素添加非标准的属性,但要添加前缀
data-
,目的是为元素提供与渲染无关的信息,或者提供语义信息。这些属性可以任意添加、随便明明,只要以data-
开头即可。
<div id="myDiv" data-appId="12345" data-myname="Nicholas"></div>
- 添加了自定义属性之后,可以通过元素的
dataset
属性来访问自定义属性的值。dataset
属性的值时 DOMStringMap 的实例,也就是一个名值对的映射。在这个映射中,每个data-name
形式的属性都会有一个对应的属性,只不过属性没有data-
前缀。
var div = document.getElementById("myDiv");
// 取得自定义属性的值
var appId = div.dataset.appId;
var myName = div.dataset.myname;
// 设置值
div.dataset.appId = 2345;
div.dataset.myname = "Michael";
// 有没有 "myname" 值呢?if (div.dataset.myname) {console.log("Hello," + div.dataset.myname);
}
插入标记
- 虽然 DOM 为操作节点提供了细致入微的控制手段,但在需要给文档插入大量 HTML 标记的情况下,通过 DOM 操作仍然非常麻烦,因为不仅要创建一系列 DOM 节点,还要小心按照正确的顺序把它们连接起来。
- 相对而言,直接插入 HTML 字符串不仅更简单,速度也更快。以下插入标记的 DOM 拓展已经纳入了 HTML5 规范。
innerHTML 属性
- 在读模式下,
innerHTML
属性返回与调用元素的所有子节点(包括元素、注释和文本节点)对应的 HTML 标记。 - 在写模式下,
innerHTML
会根据指定的值创建新的 DOM 树,然后用这个 DOM 树完全替换调用元素原先的所有子节点。
<div id="content">
<p>This is a <strong>paragraph</strong> with a list following it.</p>
<ul>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
</div>
<!-- 对于上面的 div 来说 innerHTML 属性会返回如下字符串 -->
<p>This is a <strong>paragraph</strong> with a list following it.</p>
<ul>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
- 不同浏览器返回的文本格式不会有所不同。IE 和 Opera 会将所有标签转换为大小写形式,而 Safari、Chrome、Firefox 则会原原本本的按照原先文档中(或指定这些标签时)的格式返回 HTML,包括空格和缩进。
- 在写模式下,传入
innerHTML
的值都会按照浏览器处理 HTML 的标注方式转换为元素(同样因浏览器而异)。如果设置的值仅是纯文本而没有 HTML 标签,那么结果就是设置纯文本。
div.innerHTML = "Hello world!";
div.innerHTML = "Hello & welcom, <b>\"reader\"!</b>";
// 以上操作得到:<div id="content">Hello & welcome, <b>"reader"!</b></div>
-
使用
innerHTML
属性也有一些限制,大多数浏览器中通过innerHTML
插入<script>
元素并不会执行其中的脚本。IE8 及更早的版本是唯一能够在这种情况下执行脚本的浏览器,但必须满足 2 个条件:-
<script>
元素指定 defer 属性 -
<script>
元素必须位于(微软所谓的)“有作用域的元素”(scoped element)之后。<script>
元素被认为是“无作用域的元素”(NoScoped element),也就是在页面中看不到的元素,与<style>
元素或注释类似。
-
-
没有 (不支持)
innerHTML
属性的元素有:<col>
<colgroup>
<frameset>
<head>
<html>
<style>
<table>
<tbody>
<thead>
<tfoot>
<tr>
- IE8 及更早版本
<title>
元素也没有
- Firefox 在内容类型为
application/xhtml+xml
的 XHTML 文档中设置innerHTML
有严格的限制。在 XHTML 文档中使用innerHTML
时,XHTML 代码必须完全符合要求。
outerHTML 属性
- 在读模式下,
outerHTML
返回调用它的元素及所有子节点的 HTML 标签。 - 在写模式下,
outerHTML
会根据指定的 HTML 字符串创建新的 DOM 子树,然后用这个 DOM 子树完全替换调用元素。 - 在元素上调用 outerHTML 会返回相同的代码,包括元素本身。由于浏览器解析和解释 HTML 标记的不同,结果可能会有所不同。
<div id="content">
<p>This is a <strong>paragraph</strong> with a list following it.</p>
<ul>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
</div>
<!-- 在 div 上调用 outerHTML 会返回相同的代码,包括 div 本身 -->
<div id="content">
<p>This is a <strong>paragraph</strong> with a list following it.</p>
<ul>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
</div>
- 使用
outerHTML
属性,会替换调用的元素本身
div.outerHTML = "<p>This is a paragraph.</p>";
// 上面的代码等价于下面的代码
var p = document.createElement("p");
p.appendChild(document.createTextNode("This is a paragraph."));
div.parentNode.replaceChild(p, div);
- 支持的浏览器 IE4+ Safari4+ Chrome Opera8+ Firefox8+
insertAdjacentHTML() 方法
-
insertAdjacentHTML()
接收两个参数:插入位置和要插入的 HTML 文本。 -
第一个参数必须是下列之一:
- “beforebegin” 在当前元素之前插入一个紧邻的同辈元素
- “afterbegin” 在当前元素之下插入一个新的子元素或在第一个子元素之前再插入新的子元素
- “beforeend” 在当前元素之下插入一个新的子元素或在最后一个子元素之后再插入新的子元素
- “afterend” 在当前元素之后插入一个紧邻的同辈元素
- 第二个参数是 HTML 字符串,如果浏览器无法解析该字符串,就会抛出错误。
// 作为前一个同辈元素插入
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>");
- 支持的浏览器 IE Firefox8+ Safari Opera Chrome
内存与性能问题
- 使用本节介绍的方法替换子节点可能会导致浏览器的内存占用问题,尤其是 IE,问题更加明显。
- 在删除带有时间处理程序或引用了其他 JavaScript 对象子树时,就有可能导致内存占用问题。假设某个元素有一个时间处理程序(或者引用了一个 JavaScript 对象作为属性),在使用前述某个属性将该元素从文档树删除后,元素与事件处理程序(或 JavaScript 对象)之间的绑定关系在内存中并没有一并删除。如果这种情况频繁出现,页面占用的内存数量就会明显增加。
- 因此,在使用
innerHTML
outerHTML
insertAdjacentHTML()
时,最好先手工删除要被替换的元素的所有事件处理程序和 JavaScript 对象属性(第 13 章将进一步讨论事件处理程序)。 - 不过使用这几个属性,特别是
innerHTML
仍然还是可以为我们提供很多遍历的。插入大量 HTML 标记时,设置innerHTML
或outerHTML
时就会创建一个 HTML 解析器,这个解析器是在浏览器级别的代码(通常是 C ++ 编写的)基础上运行的,因此比执行 JavaScript 快的多。 - 创建和销毁 HTML 解析器也会带来性能损失,所以最好能够将设置
innerHTML
或outerHTML
的次数控制在合理的范围内。
scrollIntoView()方法
- 如何滚动页面也是 DOM 规范没有解决的一个问题。HTML5 最终选择了
scrollIntoView()
作为标准方法。 -
scrollIntoView()
可以在所有 HTML 元素上调用,通过滚动浏览器窗口或某个容器元素,调用元素就可以出现在视口中。- 如果传入 true 作为参数,或者不传任何参数,那么窗口滚动之后会让调用元素的顶部与视口顶部尽可能的平齐。
- 如果传入 false 作为参数,调用元素会尽可能全部出现在视口中,(可能的话,调用元素的底部会与视口顶部平齐)不过顶部不一定平齐。
- 当页面发生变化时,一般会用这个方法来吸引用户的注意力。实际上,为某个元素设置焦点也会导致浏览器滚动并显示出获得焦点的元素。
- 支持的浏览器 IE Firefox8+ Safari Opera
专有拓展
- 虽然所有浏览器开发商都知晓坚持标准的重要性,但在发现某项功能缺失时,这些开发商都会一如既往的向 DOM 中添加专有拓展,以弥补功能上的不足。
- 表面上看不太友好,但实际上专有拓展为 Web 开发领域提供了很多重要的功能,这些功能最终都在 HTML5 规范中得以标准化。
- 即便如此,仍然还有大量专有的 DOM 拓展没有成为标准。编写本书时,它们还是专有功能,而且只得到了少数浏览器的支持
文档模式
-
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>
标签来设置。
<meta http-equiv="X-UA-Compatible" content="IE=IEVersion">
<meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7">
<meta http-equiv="X-UA-Compatible" content="IE=7">
-
这里的 IE 版本(IEVersion)有以下不同的值,不一定与上述 4 中文档模式对应。
-
Edge
: 始终以最新的文档模式来渲染页面。忽略文档类型声明。 -
EmulateIE9
: 如果有文档类型声明,则以 IE9 标准模式渲染,否则将文档模式设置为 IE5。 -
EmulateIE8
: 如果有文档类型声明,则以 IE8 标准模式渲染,否则将文档模式设置为 IE5。 -
EmulateIE7
: 如果有文档类型声明,则以 IE7 标准模式渲染,否则将文档模式设置为 IE5。 -
9
: 强制以 IE9 标准模式渲染页面,忽略文档类型声明 -
8
: 强制以 IE8 标准模式渲染页面,忽略文档类型声明 -
7
: 强制以 IE7 标准模式渲染页面,忽略文档类型声明 -
5
: 强制将文档模式设置为 IE5,忽略文档类型声明
-
-
document.documentMode
属性可以知晓给定页面使用的是什么文档模式,它会返回文档模式的版本号(在 IE9 中,可能返回的版本号为 5、7、8、9)
children 属性
- IE9 之前的版本处理空白符与其他浏览器有差异,因此出现了
children
属性。这个属性是 HTMLCollection 的实例,只包含元素中同样还是元素的子节点。除此之外,children
与childNodes
没有区别。 - IE8 及更早版本的
children
属性中也会包含注释节点,但 IE9 之后的版本只返回元素节点。 - 支持的浏览器 IE5 Firefox 3.5 Safari2(有 bug) Safari3 Opera8 和 Chrome
contains() 方法
-
contains()
祖先节点调用这个方法,接收一个参数是要检查的后代节点。如果祖先节点包含传入的后代节点返回 true,否则 false。 - 支持的浏览器 IE9+ Firefox Safari Opera Chrome
document.documentElement.contains(document.body); // true
- 使用 DOM Level 3
compareDocumentPosition()
也能够确定节点间的关系。 - 这个方法确定两个节点间的关系,返回表示该关系的位掩码(bitmask)
掩码 | 节点关系 |
---|---|
1 | 无关(给定的节点不在当前文档中) |
2 | 居前(给定的节点在 DOM 树中位于参考节点之前) |
4 | 居后(给定的节点在 DOM 树中位于参考节点之后) |
8 | 包含(给定的节点是参考节点的祖先) |
16 | 包含(给定的节点是参考节点的后代) |
- 支持的浏览器 IE9+ Firefox Safari Opera9.5 Chrome
- 为模仿
contains()
方法,应该关注的是掩码 16. 可以对compareDocumentPosition()
的结果执行按位与。
var result = document.documentElement.compareDocumentPosition(document.body);
console.log(!!(result & 16));
- 执行上面的代码后,结果会变成 20(表示“居后”的 4 加上表示“被包含”的 16)。对掩码 16 执行安慰操作会返回一个非零数值,而两个逻辑非操作符会将该数值转换成布尔值。
- 注意:回值可以是值的组合。例如,返回 20 意味着在 p2 在 p1 内部(16),并且 p1 在 p2 之前(4)。
- 使用一些浏览器及能力检测,就可以写出如下所示的一个通用的
contains
函数:
function contains(refNode, otherNode) {
if (typeof refNode.contains == "function" &&
(!client.engine.webkit || client.engine.webkit >= 522)) {
// 浏览器支持 contains 方法直接使用
return refNode.contains(otherNode);
} else if (typeof refNode.compareDocumentPosition == "function") {
// 浏览器支持 compareDocumentPosition 方法
// 用返回结果和 16 进行按位与,再转换成布尔值返回
return !!(refNode.compareDocumentPosition(otherNode) & 16);
} else {
// 针对 Safari 设计的验证方法
// 在文档树中向上递归验证是否有 refNode
// 到达文档树顶端,parentNode 的值为 null,循环结束
var node = otherNode.parentNode;
do {if (node === refNode) {return true;} else {node = node.parentNode;}
} while (node !== null);
return false;
}
}
插入文本
-
IE 原来有
innerHTML
和outerHTML
已被 HTML5 纳入规范。但另外两个插入文本的专有属性则没有这么好的运气。innerText
outerText
滚动
-
scrollIntoView()
纳入规范后,仍有几个专有方法可以在不同浏览器中使用。下列都是对 HTMLElement 类型的扩展,因此在所有元素中都可以调用:-
scrollIntoViewIfNeeded(alignCenter)
: 只在当前元素在视口中不可见的情况下,才滚动浏览器窗口或容器元素,最终让它可见。如果当前元素可见,这个方法什么都不会做。如果将可选的alignCenter
参数设置为 true,则表示尽量将元素显示在视口中部(垂直方向)。Safari 和 Chrome 实现了这个方法 -
scrollByLines(lineCount)
: 将元素的内容滚动到指定的行高,lineCount
可以是正值也可以是负值。Safari 和 Chrome 实现了这个方法 -
scrollByPages(lineCount)
: 将元素的内容滚动到指定的页面高度,具体高度由元素的高度决定。Safari 和 Chrome 实现了这个方法
-
- 需要注意的是,
scrollIntoView()
和scrollIntoViewIfNeeded()
的作用对象是元素的容器,而scrollByLines()
和scrollByPages()
影响的是元素自身
// 将页面主体滚动 5 行
document.body.scrollByLines(5);
// 在当前元素不可见的时候,让它进入浏览器的视口
document.images[0].scrollIntoViewIfNeeded();
// 将页面主体往回滚动 1 页
document.body.scrollByPages(-1);
-
scrollIntoView()
是唯一一个所有浏览器都支持的方法,因此最常用。