跳到主要内容

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 之前的版本不会把元素间的空格当成空白节点,而其他浏览器则会。这样就导致了 childNodes 和 firstChild 等属性上的差异。为了弥补这个差异,同时不影响 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 字符串解析(与 innerHTML 和 outerHTML 相同)或者作为纯文本解析(与 innerText 和 outerText 相同)。如果是 HTML,则会在解析出错时抛出错误。

妙记参数

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

5.内存与性能问题​

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

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

例如,下面的代码使用 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:定义过渡动画,可取的值为 smooth 和 auto ,默认为 auto。
  • block:定义垂直方向的对齐,可取的值为 start 、center、end 和 nearest,默认为 start。
  • inline:定义水平方向的对齐,可取的值为 start 、center、end 和 nearest,默认为 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 类型的子节点。如果元素的子节点类型全部是元素类型,那 children 和 childNodes 中包含的节点应该是一样的。

示例
/* 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 发明的 innerHTML 和 outerHTML 纳入了标准,但还有两个属性没有入选。这两个剩下的属性是 innerText 和 outerText。

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 属性​

outerText 与 innerText 是类似的,只不过作用范围包含调用它的节点。要读取文本值时,outerText 与 innerText 实际上会返回同样的内容。但在写入文本值时,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>

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

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>
*/