跳到主要内容

DOM 扩展

尽管 DOM API 已经相当不错,但仍然不断有标准或专有的扩展出现,以支持更多功能。2008 年以前,大部分浏览器对 DOM 的扩展是专有的。此后,W3C 开始着手将这些已成为事实标准的专有扩展编制成正式规范。

基于以上背景,诞生了描述 DOM 扩展的两个标准:Selectors API 与 HTML5。这两个标准体现了社区需求和标准化某些手段及 API 的愿景。另外还有较小的 Element Traversal 规范,增加了一些 DOM 属性。专有扩展虽然还有,但这两个规范(特别是 HTML5)已经涵盖其中大部分。

Selectors API

Selectors API(参见 W3C 网站上的 Selectors API Level 1)是 W3C 推荐标准,规定了浏览器原生支持的 CSS 查询 API。支持这一特性的所有 JavaScript 库都会实现一个基本的 CSS 解析器,然后使用已有的 DOM 方法搜索文档并匹配目标节点。虽然库开发者在不断改进其性能,但 JavaScript 代码能做到的毕竟有限。通过浏览器原生支持这个 API,解析和遍历 DOM 树可以通过底层编译语言实现,性能也有了数量级的提升。

Selectors API Level 1 的核心是两个方法:querySelector()querySelectorAll()。在兼容浏览器中,Document 类型和 Element 类型的实例上都会暴露这两个方法。

Selectors API Level 2 规范在 Element 类型上新增了更多方法,比如 matches()find()findAll()。不过,目前还没有浏览器实现或宣称实现 find()findAll()

querySelector()

querySelector() 方法返回文档中与指定选择器或选择器组匹配的第一个 Element 对象。如果找不到匹配项,则返回 null

querySelector() 示例
// 取得<body>元素
let body = document.querySelector('body');

// 取得 ID 为"myDiv"的元素
let myDiv = document.querySelector('#myDiv');

// 取得类名为"selected"的第一个元素
let selected = document.querySelector('.selected');

// 取得类名为"button"的图片
let img = document.body.querySelector('img.button');

在 Document 上使用 querySelector() 方法时,会从文档元素开始搜索;在 Element 上使用 querySelector() 方法时,则只会从当前元素的后代中查询

用于查询模式的 CSS 选择符可繁可简,依需求而定。如果选择符有语法错误或碰到不支持的选择符,则 querySelector() 方法会抛出错误。

扩展

在性能方面,如果你只是需要通过唯一的 ID 来查找元素,那么 getElementById 通常是首选,因为它更快速。但是,如果你需要使用更复杂的选择器来查找元素,那么 querySelector 是更适合的选择,尽管它可能稍微慢一些。

querySelectorAll()

querySelectorAll() 方法跟 querySelector() 一样,也接收一个用于查询的参数,但它会返回所有匹配的节点,而不止一个。这个方法返回的是一个 NodeList 的静态实例

再强调一次,querySelectorAll() 返回的 NodeList 实例一个属性和方法都不缺,但它是一个静态的“快照”,而非“实时”的查询。这样的底层实现避免了使用 NodeList 对象可能造成的性能问题。

querySelector() 一样,querySelectorAll() 也可以在 Document、DocumentFragment 和 Element 类型上使用。

// 取得 ID 为"myDiv"的<div>元素中的所有<em>元素
let ems = document.getElementById('myDiv').querySelectorAll('em');

// 取得所有类名中包含"selected"的元素
let selecteds = document.querySelectorAll('.selected');

// 取得所有是<p>元素子元素的<strong>元素
let strongs = document.querySelectorAll('p strong');

返回的 NodeList 对象可以通过 for-of 循环、item() 方法或 [] 中括号语法取得个别元素。

let strongElements = document.querySelectorAll('p strong');

for (let strong of strongElements) {
strong.className = 'important';
}

for (let i = 0; i < strongElements.length; ++i) {
strongElements.item(i).className = 'important';
}

for (let i = 0; i < strongElements.length; ++i) {
strongElements[i].className = 'important';
}

querySelector() 方法一样,如果选择符有语法错误或碰到不支持的选择符,则 querySelectorAll() 方法会抛出错误。

matches()

matches() 方法(在规范草案中称为 matchesSelector())接收一个 CSS 选择符参数,如果元素匹配则该选择符返回 true,否则返回 false

matches() 示例
<ul id="birds">
<li>橙翅鹦鹉</li>
<li class="endangered">菲律宾鹰</li>
<li>大白鹈鹕</li>
</ul>

<script type="text/javascript">
var birds = document.getElementsByTagName('li');

for (var i = 0; i < birds.length; i++) {
if (birds[i].matches('.endangered')) {
console.log(birds[i].textContent + '快濒临灭绝!');
}
}
</script>
注意

有一些浏览器使用前缀,在非标准名称 matchesSelector() 下实现了这个方法!但为了代码的可维护性和跨浏览器兼容性,建议尽可能使用标准名称 matches()

元素遍历

IE9 之前的版本不会把元素间的空格当成空白节点,而其他浏览器则会。这样就导致了 childNodesfirstChild 等属性上的差异。为了弥补这个差异,同时不影响 DOM 规范,W3C 通过新的 Element Traversal 规范定义了一组新属性。

Element Traversal API 为 DOM 元素添加了 5 个属性:

  • childElementCount: 返回子元素数量(不包含文本节点和注释);
  • firstElementChild: 指向第一个 Element 类型的子元素(Element 版 firstChild);
  • lastElementChild: 指向最后一个 Element 类型的子元素(Element 版 lastChild);
  • previousElementSibling: 指向前一个 Element 类型的同胞元素( Element 版 previousSibling);
  • nextElementSibling: 指向后一个 Element 类型的同胞元素(Element 版 nextSibling)。

在支持的浏览器中,所有 DOM 元素都会有这些属性,为遍历 DOM 元素提供便利。这样开发者就不用担心空白文本节点的问题了。

举个例子,过去要以跨浏览器方式遍历特定元素的所有子元素,代码大致是这样写的:

<ul id="parent">
<li>1</li>
<li>2</li>
<li>3</li>
</ul>

<script type="text/javascript">
let parentElement = document.getElementById('parent');
let currentChildNode = parentElement.firstChild;

let children = [];

// 没有子元素,firstChild 返回 null,跳过循环
while (currentChildNode) {
// 需要判断是否节点元素,而非文本、注释节点
if (currentChildNode.nodeType === 1) {
children.push(currentChildNode);
}
currentChildNode = currentChildNode.nextSibling;
}

console.log(children); //  [li, li, li]
</script>

使用 Element Traversal 属性之后,以上代码可以简化如下:

<ul id="parent">
<li>1</li>
<li>2</li>
<li>3</li>
</ul>

<script type="text/javascript">
let parentElement = document.getElementById('parent');
// highlight-success
let currentChildNode = parentElement.firstElementChild;

let children = [];

// highlight-success-start
while (currentChildNode) {
children.push(currentChildNode);
currentChildNode = currentChildNode.nextElementSibling;
}
// highlight-success-end

console.log(children); // [li, li, li]
</script>

HTML5

HTML5 代表着与以前的 HTML 截然不同的方向。在所有以前的 HTML 规范中,从未出现过描述 JavaScript 接口的情形,HTML 就是一个纯标记语言。JavaScript 绑定的事,一概交给 DOM 规范去定义。

然而,HTML5 规范却包含了与标记相关的大量 JavaScript API 定义。其中有的 API 与 DOM 重合,定义了浏览器应该提供的 DOM 扩展。

CSS类扩展

自 HTML4 被广泛采用以来,Web 开发中一个主要的变化是 class 属性用得越来越多,其用处是为元素添加样式以及语义信息。自然地,JavaScript 与 CSS 类的交互就增多了,包括动态修改类名,以及根据给定的一个或一组类名查询元素,等等。为了适应开发者和他们对 class 属性的认可,HTML5 增加了一些特性以方便使用 CSS 类。

1.getElementsByClassName()

getElementsByClassName() 是 HTML5 新增的最受欢迎的一个方法,暴露在 document 对象和所有 HTML 元素上。这个方法脱胎于基于原有 DOM 特性实现该功能的 JavaScript 库,提供了性能高好的原生实现。

返回一个包含了所有指定类名的子元素的类数组对象。当在 document 对象上调用时,会搜索整个 DOM 文档,包含根节点。你也可以在任意元素上调用 getElementsByClassName() 方法,它将返回的是以当前元素为根节点,所有指定类名的子元素。

语法
/**
* elements 是一个实时集合,包含了找到的所有元素。
* names 是一个字符串,表示要匹配的类名列表;类名通过空格分隔
*/
let elements = document.getElementsByClassName(names);
getElementsByClassName() 示例
// 获取所有 class 为 'test' 的元素:
document.getElementsByClassName('test');

// 获取所有 class 同时包括 'red' 和 'test' 的元素。
document.getElementsByClassName('red test');

// 在 id 为'main'的元素的子节点中,获取所有 class 为'test'的元素
document.getElementById('main').getElementsByClassName('test');

2.classList 属性

Element.classList 是一个只读属性,返回一个元素 class 属性的动态 DOMTokenList 集合。这可以用于操作 class 集合。

与其他 DOM 集合类型一样,DOMTokenList 也有 length 属性,也可以通过 item()[] 中括号取得个别的元素。此外,DOMTokenList 还增加了以下方法。

  • add(value): 向类名列表中添加指定的字符串值 value。如果这个值已经存在,则什么也不做。
  • contains(value): 返回布尔值,表示给定的 value 是否存在。
  • remove(value): 从类名列表中删除指定的字符串值 value。
  • toggle(value,force):

如果类名列表中已经存在指定的 value,则删除;如果不存在,则添加。

force 可选,如果包含该值,设置后会将方法变成单向操作。如果设置为 false,仅会删除标记列表中匹配的给定标记,且不会再度添加。如果设置为 true,仅在标记列表中添加给定标记,且不会再度删除。

  • replace(oldToken, newToken)

可以将列表中一个已存在的 oldToken 替换为一个新 newToken。如果第一个参数在列表中不存在, replace() 立刻返回 false ,而不会将新 token 字符串添加到列表中。

API 示例
const div = document.createElement('div');
div.className = 'foo';

// 初始状态:<div class="foo"></div>
console.log(div.outerHTML);

// 使用 classList API 移除、添加类值
div.classList.remove('foo');
div.classList.add('anotherclass');

// <div class="anotherclass"></div>
console.log(div.outerHTML);

// 如果 visible 类值已存在,则移除它,否则添加它
div.classList.toggle('visible');

// 添加 force 参数,来转变函数行为 add 或 remove
div.classList.toggle('visible', i < 10);

console.log(div.classList.contains('foo'));

// 添加或移除多个类值
div.classList.add('foo', 'bar', 'baz');
div.classList.remove('foo', 'bar', 'baz');

// 使用展开语法添加或移除多个类值
const cls = ['foo', 'bar'];
div.classList.add(...cls);
div.classList.remove(...cls);

// 将类值 "foo" 替换成 "bar"
div.classList.replace('foo', 'bar');

// 迭代类名
for (let item of div.classList) {
}

添加了 classList 属性之后,除非是完全删除或完全重写元素的 class 属性,否则 className 属性就用不到了。

焦点管理

HTML5 增加了辅助 DOM 焦点管理的功能。首先是 document.activeElement,始终包含当前拥有焦点的 DOM 元素。页面加载时,可以通过用户输入(按 Tab 键或代码中使用 focus() 方法)让某个元素自动获得焦点。

示例
let button = document.getElementById('myButton');
button.focus();
console.log(document.activeElement === button); // true

默认情况下,document.activeElement 在页面刚加载完之后会设置为 document.body。而在页面完全加载之前,document.activeElement 的值为 null

其次是 document.hasFocus() 方法,该方法返回布尔值,表示文档是否拥有焦点:

let button = document.getElementById('myButton');
button.focus();
console.log(document.hasFocus()); // true

确定文档是否获得了焦点,就可以帮助确定用户是否在操作页面。

备注

第一个方法可以用来查询文档,确定哪个元素拥有焦点,第二个方法可以查询文档是否获得了焦点,而这对于保证 Web 应用程序的无障碍使用是非常重要的。无障碍 Web 应用程序的一个重要方面就是焦点管理,而能够确定哪个元素当前拥有焦点(相比于之前的猜测)是一个很大的进步。

HTMLDocument 扩展

HTML5 扩展了 HTMLDocument 类型,增加了更多功能。与其他 HTML5 定义的 DOM 扩展一样,这些变化同样基于所有浏览器事实上都已经支持的专有扩展。为此,即使这些扩展的标准化相对较晚,很多浏览器也早就实现了相应的功能。

1.readyState 属性

readyState 是 IE4 最早添加到 document 对象上的属性,后来其他浏览器也都依葫芦画瓢地支持这个属性。最终,HTML5 将这个属性写进了标准。document.readyState 属性有两个可能的值:

  • loading: 表示文档正在加载;
  • interactive: 表示文档已被解析,正在加载状态结束(可交互);
  • complete: 表示文档加载完成。

实际开发中,最好是把 document.readState 当成一个指示器,以判断文档是否加载完毕。在这个属性得到广泛支持以前,通常要依赖 onload 事件处理程序设置一个标记,表示文档加载完了。这个属性的基本用法如下:

switch (document.readyState) {
case 'loading':
console.log('loading');
case 'interactive':
console.log('interactive');
case 'complete':
console.log('complete');
}

// loading
// interactive
// complete

2.compatMode 属性

document.compatMode 表明当前文档的渲染模式是怪异模式/混杂模式还是标准模式。

mode = document.compatMode;

mode 是一个枚举值(enumerated value),可能的值有:

  • BackCompat:文档为怪异模式。
  • CSS1Compat:文档不是标准模式。
示例
if (document.compatMode == 'CSS1Compat') {
console.log('Standards mode / 标准模式');
} else {
console.log('Quirks mode / 怪异模式');
}

3.head 属性

作为对 document.body(指向文档的 <body> 元素)的补充,HTML5 增加了 document.head 属性,指向文档的 <head> 元素。可以像下面这样直接取得 <head> 元素:

let head = document.head;

字符集属性

在 HTML5 中,确实增加了一些与文档字符集相关的新属性。其中,document.characterSet 属性表示文档实际使用的字符集,但是可以通过 <meta> 元素或响应头来修改。

<meta charset="UTF-8" />

<script>
console.log(document.characterSet); // UTF-8
</script>

document.characterSet 属性默认值取决于浏览器和文档的实际编码。在大多数情况下,浏览器会将其设置为文档的实际字符编码,例如 "UTF-8"、"ISO-8859-1" 等。

自定义数据属性

HTML5 允许给元素指定非标准的属性,但要使用前缀 data- 以便告诉浏览器,这些属性既不包含与渲染有关的信息,也不包含元素的语义信息。除了前缀,自定义属性对命名是没有限制的,data- 后面跟什么都可以。

示例
<div id="myDiv" data-appId="12345" data-myname="Nicholas"></div>

定义了自定义数据属性后,可以通过元素的 dataset 属性来访问。dataset 属性是一个 DOMStringMap 的实例,包含一组键/值对映射。元素的每个 data-name 属性在 dataset 中都可以通过 data- 后面的字符串作为键来访问。

在 HTML 属性名以 data- 开头。它只能包含字母、数字、破折号(-)、句号(.)、冒号(:)和下划线(_)。任意的 ASCII 大写字母(A 到 Z)都会转换为小写。

在 JavaScript 中,当访问 dataset 属性时,属性名会根据 HTML 中定义的属性名进行转换,去掉 data- 前缀,并将破折号后的字母大写,以获得属性的“驼峰”命名。

语法
<!--定义自定义数据属性-->
<div id="user" data-name-name="myName"></div>

<script>
<!--读、写、删自定义数据属性-->
const el = document.querySelector('#user');

// 小驼峰形式读取属性
console.log(el.dataset.nameName); // myName

el.dataset.nameName = 'newMyName';
console.log(el.dataset.nameName); // myName

delete el.dataset.nameName;
</script>

自定义数据属性非常适合需要给元素附加某些数据的场景,比如链接追踪和在聚合应用程序中标识页面的不同部分。另外,单页应用程序框架也非常多地使用了自定义数据属性。

插入标记

DOM 虽然已经为操纵节点提供了很多 API,但向文档中一次性插入大量 HTML 时还是比较麻烦。相比先创建一堆节点,再把它们以正确的顺序连接起来,直接插入一个 HTML 字符串要简单(快速)得多。HTML5 已经通过以下 DOM 扩展将这种能力标准化了。

1.innerHTML 属性

在读取 innerHTML 属性时,会返回元素所有后代的 HTML 字符串,包括元素、注释和文本节点。而在写入 innerHTML 时,则会根据提供的字符串值以新的 DOM 子树替代元素中原来包含的所有节点。

示例
// 包含元素,以浏览器处理HTML元素的方式转换为元素
document.body.innerHTML = '<h1>Hello World!</h1>';

// 如果赋值中不包含任何 HTML 标签,则直接生成一个文本节点
document.body.innerHTML = 'Hello World!';

不同浏览器在处理 innerHTML 属性时可能会有不同的行为,尤其是在返回文本内容时。一些浏览器(例如 IE 和 Opera)可能会将所有元素标签转换为大写,并且可能会移除不必要的空格和缩进,以便更容易地解析和处理文档内容。而其他浏览器(例如 Safari、Chrome 和 Firefox)通常会尽可能地保留文档源代码的格式,包括空格和缩进等,并且不会强制将元素标签转换为大写。

2.旧 IE 中的 innerHTML

在所有现代浏览器中,通过 innerHTML 插入的 <script> 标签是不会执行的。而在 IE8 及之前的版本中,只要这样插入的 <script> 元素指定了 defer 属性,且 <script> 之前是“受控元素”(scopedelement),那就是可以执行的。 <script> 元素与 <style> 或注释一样,都是“非受控元素”(NoScopeelement),也就是在页面上看不到它们。IE 会把 innerHTML 中从非受控元素开始的内容都删掉,也就是说下面的例子是行不通的:

// 行不通
div.innerHTML = "<script defer>console.log('hi');</script>";

在这个例子中,innerHTML 字符串以一个非受控元素开始,因此整个字符串都会被清空。为了达到目的,必须在 <script> 前面加上一个受控元素,例如文本节点或没有结束标签的元素(如 <input>)。因此,下面的代码就是可行的:

// 以下都可行
div.innerHTML = "_<script defer>console.log('hi');</script>";
div.innerHTML = "<div>&nbsp;</div><script defer>console.log('hi');</script>";
div.innerHTML =
'<input type="hidden"><script defer>console.log(\'hi\');</script>';

方案一会在 <script>元素前面插入一个文本节点。为了不影响页面排版,可能稍后需要删掉这个文本节点。方案二与之类似,使用了包含空格的 <div> 元素。空 <div> 是不行的,必须包含一点内容,以强制创建一个文本节点。同样,这个 <div> 元素可能也需要事后删除,以免影响页面外观。方案是使用了一个隐藏的 <input> 字段来达成同样的目的。因为这个字段不影响页面布局,所以应该是最理想的方案。

在 IE 中,通过 innerHTML 插入 <style> 也会有类似的问题。多数浏览器支持使用 innerHTML 插入 <style> 元素:

div.innerHTML = '<style type="text/css">body {background-color: red; }</style>';

但在 IE8 及之前的版本中,<style> 也被认为是非受控元素,所以必须前置一个受控元素:

div.innerHTML =
'_<style type="text/css">body {background-color: red; }</style>';
div.removeChild(div.firstChild);

3.outerHTML 属性

读取 outerHTML 属性时,会返回调用它的元素(及所有后代元素)的 HTML 字符串。在写入outerHTML 属性时,调用它的元素会被传入的 HTML 字符串经解释之后生成的 DOM 子树取代。

示例
/* HTML:
<div id="container">
This is a div
</div>
*/

let container = document.getElementById('container');
container.outerHTML = '<h1>Hello World!</h1>';

/*
<div id="container"></div> 被 <h1>Hello World!</h1> 取代了
(不在页面中显示,但仍然在内存中)
*/

此外,虽然元素将在文档中被替换,设置了 outerHTML 属性的变量仍将保持对原始元素的引用:

console.log(container); // <div id="container"></div>

使用 outerHTML 设置 HTML 的原理机制:

/* HTML:
<div id="container">
This is a div
</div>
*/

let container = document.getElementById('container');
let h1 = document.createElement('h1');
h1.appendChild(document.createTextNode('Hello World!'));
container.parentNode.replaceChild(h1, container);

在元素上调用 outerHTML 会返回相同的字符串,包括元素本身。注意,浏览器因解析和解释 HTML 代码的机制不同,返回的字符串也可能不同。(跟 innerHTML 的情况是一样的。)

4.insertAdjacentHTML()与 insertAdjacentText()

关于插入标签的最后两个新增方法是 insertAdjacentHTML()insertAdjacentText()。这两个方法最早源自 IE,它们都接收两个参数:要插入标记的位置和要插入的 HTML 或文本。第一个参数必须是下列值中的一个:

  • beforebegin: 插入当前元素前面,作为前一个同胞节点;
  • afterbegin: 插入当前元素内部,作为新的子节点或放在第一个子节点前面;
  • beforeend: 插入当前元素内部,作为新的子节点或放在最后一个子节点后面;
  • afterend: 插入当前元素后面,作为下一个同胞节点。

注意这几个值是不区分大小写的。第二个参数会作为 HTML 字符串解析(与 innerHTMLouterHTML 相同)或者作为纯文本解析(与 innerTextouterText 相同)。如果是 HTML,则会在解析出错时抛出错误。

妙记参数

假设当前元素是 <p>Hello world!</p>,则 beforebeginafterbegin 中的 begin 指开始标签<p>;故 beforebegin 插入到开始标签的前面,以此类推 afterbegin; 而 afterendbeforeend 中的 end 指结束标签</p>,故 afterend 在结束标签之后,依此类推 beforeend

5.内存与性能问题

使用本节介绍的方法替换子节点可能在浏览器(特别是 IE)中导致内存问题。比如,如果被移除的子树元素中之前有关联的事件处理程序或其他 JavaScript 对象(作为元素的属性),那它们之间的绑定关系会滞留在内存中。如果这种替换操作频繁发生,页面的内存占用就会持续攀升。在使用 innerHTMLouterHTMLinsertAdjacentHTML() 之前,最好手动删除要被替换的元素上关联的事件处理程序和JavaScript 对象。

使用这些属性当然有其方便之处,特别是 innerHTML。一般来讲,插入大量的新 HTML 使用 innerHTML 比使用多次 DOM 操作创建节点再插入来得更便捷。这是因为 HTML 解析器会解析设置给 innerHTML(或 outerHTML)的值。解析器在浏览器中是底层代码(通常是 C++代码),比 JavaScript快得多。不过,HTML 解析器的构建与解构也不是没有代价,因此最好限制使用 innerHTMLouterHTML 的次数。

例如,下面的代码使用 innerHTML 创建了一些列表项

// 效率低下,每次循环就赋值 innerHTML,会导致内存占用
for (let value of values) {
ul.innerHTML += '<li>${value}</li>'; // 别这样做!
}

// 改进代码:
let itemsHtml = '';
for (let value of values) {
itemsHtml += '<li>${value}</li>';
}
ul.innerHTML = itemsHtml; // 只进行最后一次赋值

// 或者
ul.innerHTML = values.map(value => '<li>${value}</li>').join('');

6.跨站点脚本(XSS)

尽管 innerHTML 不会执行自己创建的 <script> 标签,但仍然向恶意用户暴露了很大的攻击面,因为通过它可以毫不费力地创建元素并执行 onclick 之类的属性。

如果页面中要使用用户提供的信息,则不建议使用 innerHTML。与使用 innerHTML 获得的方便相比,防止 XSS 攻击更让人头疼。此时一定要隔离要插入的数据,在插入页面前必须毫不犹豫地使用相关的库对它们进行转义。

scrollIntoView()

DOM 规范中没有涉及的一个问题是如何滚动页面中的某个区域。为填充这方面的缺失,不同浏览器实现了不同的控制滚动的方式。在所有这些专有方法中,HTML5 选择了标准化 scrollIntoView()

scrollIntoView() 方法存在于所有 HTML 元素上,可以滚动浏览器窗口或容器元素以便包含元素进入视口。这个方法的参数如下:

  • alignToTop: 是一个布尔值。

值为 true 表示窗口滚动后元素的顶部与视口顶部对齐,值为 false 表示窗口滚动后元素的底部与视口底部对齐。

  • scrollIntoViewOptions是一个选项对象。
  • behavior:定义过渡动画,可取的值为 smoothauto ,默认为 auto
  • block:定义垂直方向的对齐,可取的值为 startcenterendnearest,默认为 start
  • inline:定义水平方向的对齐,可取的值为 startcenterendnearest,默认为 nearest
示例
const element = document.getElementById('box');

element.scrollIntoView(); // 默认传递 alignToTop true
element.scrollIntoView(false);
element.scrollIntoView({ block: 'end' });
element.scrollIntoView({ behavior: 'smooth', block: 'end', inline: 'nearest' });

专有扩展

尽管所有浏览器厂商都理解遵循标准的重要性,但它们也都有为弥补功能缺失而为 DOM 添加专有扩展的历史。虽然这表面上看是一件坏事,但专有扩展也为开发者提供了很多重要功能,而这些功能后来则有可能被标准化,比如进入 HTML5。

除了已经标准化的,各家浏览器还有很多未被标准化的专有扩展。这并不意味着它们将来不会被纳入标准,只不过目前,它们还只是由部分浏览器专有和采用。

children 属性

IE9 之前的版本与其他浏览器在处理空白文本节点上的差异导致了 children 属性的出现。children 属性是一个 HTMLCollection,只包含元素的 Element 类型的子节点。如果元素的子节点类型全部是元素类型,那 childrenchildNodes 中包含的节点应该是一样的。

示例
/* HTML:
<ul id="parent">
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
*/

let ul = document.getElementById('parent');

console.log(ul.children); // HTMLCollection(3) [li, li, li]
console.log(ul.childNodes); // NodeList(7) [text, li, text, li, text, li, text]

contains() 方法

DOM 编程中经常需要确定一个元素是不是另一个元素的后代。IE 首先引入了 contains() 方法,让开发者可以在不遍历 DOM 的情况下获取这个信息。contains() 方法应该在要搜索的祖先元素上调用,参数是待确定的目标节点。

如果目标节点是被搜索节点的后代,contains() 返回 true,否则返回 false。下面看一个例子:

console.log(document.documentElement.contains(document.body)); // true

另外,使用 DOM Level 3 的 compareDocumentPosition() 方法也可以确定节点间的关系。这个方法会返回表示两个节点关系的位掩码。下表给出了这些位掩码的说明。

掩码节点关系
0x1断开(传入的节点不在文档中)
0x2领先(传入的节点在 DOM 树中位于参考节点之前)
0x4随后(传入的节点在 DOM 树中位于参考节点之后)
0x8包含(传入的节点是参考节点的祖先)
0x10被包含(传入的节点是参考节点的后代)

要模仿 contains() 方法,就需要用到掩码 16(0x10)。compareDocumentPosition() 方法的结果可以通过按位与来确定参考节点是否包含传入的节点,比如:

let result = document.documentElement.compareDocumentPosition(document.body);
console.log(!!(result & 0x10));

以上代码执行后 result 的值为 20(或 0x14,其中 0x4 表示“随后”,加上 0x10“被包含”)。对 result 和 0x10 应用按位与会返回非零值,而两个叹号将这个值转换成对应的布尔值。

IE9 及之后的版本,以及所有现代浏览器都支持 contains()compareDocumentPosition() 方法。

插入标记

HTML5 将 IE 发明的 innerHTMLouterHTML 纳入了标准,但还有两个属性没有入选。这两个剩下的属性是 innerTextouterText

1.innerText 属性

innerText 属性对应元素中包含的所有文本内容,无论文本在子树中哪个层级。在用于读取值时,innerText 会按照深度优先的顺序将子树中所有文本节点的值拼接起来。在用于写入值时,innerText 会移除元素的所有后代并插入一个包含该值的文本节点。

示例
<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>

<script>
let content = document.getElementById('content');
// 读
console.log(content.innerText);

/*输出结果:
This is a paragraph with a list following it.
Item 1
Item 2
Item 3
*/

// 写
content.innerText = 'Hello World';
console.log(content); // <div id="content">Hello World</div>
</script>

注意不同浏览器对待空格的方式不同,因此格式化之后的字符串可能包含也可能不包含原始 HTML 代码中的缩进。

在一个节点上设置 innerText 会移除该节点的所有子节点,并用给定的字符串值的单一文本节点代替它们。

document.body.innerText = 'Hello World';

因为设置 innerText 只能在容器元素中生成一个文本节点,所以为了保证一定是文本节点,就必须进行 HTML 编码。innerText 属性可以用于去除 HTML 标签。通过将 innerText 设置为等于innerText,可以去除所有 HTML 标签而只剩文本,如下所示:

// 执行如下代码后,容器元素的内容只会包含原先的文本内容。
document.body.innerText = document.body.innerText;

2.outerText 属性

outerTextinnerText 是类似的,只不过作用范围包含调用它的节点。要读取文本值时,outerTextinnerText 实际上会返回同样的内容。但在写入文本值时,outerText 就大不相同了。写入文本值时,outerText 不止会移除所有后代节点,而是会替换整个元素。

示例
<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>

<script>
let content = document.getElementById('content');
// 读
console.log(content.outerText);

/*输出结果:
This is a paragraph with a list following it.
Item 1
Item 2
Item 3
*/

// 写
content.outerText = 'Hello World';

// <div id="content"></div> 整个元素即全部子节点被代替
</script>

outerTextouterHTML 一样,替换节点时,当前节点并不会被释放,而是仍然存在内存中,可以被引用。

let content = document.getElementById('content');
content.outerText = 'Hello World';

console.log(content);
/*输出结果:
<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>
*/