# 选择符 API
Selectors API Level 1 的核心是两个方法:querySelector() 和 querySelectorAll() 。在兼容浏览器中, Document 类型和 Element 类型的实例上都会暴露这两个方法。Selectors API Level 2 规范在 Element 类型上新增了更多方法,比如 matches() 、 find() 和 findAll() 。不过,目前还没有浏览器实现或宣称实现 find() 和 findAll()。
# querySelector() 方法
querySelector() 方法接收 CSS 选择符参数,返回匹配该模式的第一个后代元素,如果没有匹配项则返回 null 。如下:
// 取得 body 元素
let body = document.querySelector('body')
// 获取 id 为 mydiv 的元素
let myDiv = document.querySelector('#mydiv')
// 获取类名为 selected 的第一个元素
let selected = document.querySelector('.selected')
// 获取类名为 button 的第一个 img 元素
let img = document.querySelector('img.button')
在 Document 上使用 querySelector() 方法时,会从文档元素开始搜索;在 Element 上使用 querySelector() 方法时,则只会从当前元素的后代中查询。
# querySekectorAll() 方法
querySelectorAll() 方法跟 querySelector() 一样,也接收一个用于查询的参数,但它会返回所有匹配的节点,而不止一个。这个方法返回的是一个 NodeList 的静态实例。需要注意的是:querySelectorAll() 返回的 NodeList 实例一个属性和方法都不缺,但它是一个静态的“快照”,而非“实时”的查询。这样的底层实现避免了使用 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")
// 以下 3 个循环的效果一样
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"
}
# matches() 方法
matches() 方法(在规范草案中称为 matchesSelector())接收一个 CSS 选择符参数,如果元素匹配则该选择符返回 true ,否则返回 false 。例如:
if (dcoument.body.matches('body.page1')) {
// true
}
使用这个方法可以方便地检测某个元素会不会被 querySelector() 或 querySelectorAll() 方法返回。
# 元素遍历
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 )。
举个例子,过去要以跨浏览器方式遍历特定元素的所有子元素,代码大致是这样写的:
let parentElement = document.getElementById('parent');
let currentChildNode = parentElement.firstChild;
// 没有子元素,firstChild 返回 null,跳过循环
while (currentChildNode) {
if (currentChildNode.nodeType === 1) {
// 如果有元素节点,则做相应处理
processChild(currentChildNode);
}
if (currentChildNode === parentElement.lastChild) {
break;
}
currentChildNode = currentChildNode.nextSibling;
}
使用 Element Traversal 属性之后,以上代码可以简化如下:
let parentElement = document.getElementById('parent');
let currentChildElement = parentElement.firstElementChild;
// 没有子元素,firstElementChild 返回 null,跳过循环
while (currentChildElement) {
// 这就是元素节点,做相应处理
processChild(currentChildElement);
if (currentChildElement === parentElement.lastElementChild) {
break;
}
currentChildElement = currentChildElement.nextElementSibling;
}
# HTML 5
HTML5 代表着与以前的 HTML 截然不同的方向。在所有以前的 HTML 规范中,从未出现过描述 JavaScript 接口的情形,HTML 就是一个纯标记语言。JavaScript 绑定的事,一概交给 DOM 规范去定义。然而,HTML5 规范却包含了与标记相关的大量 JavaScript API 定义。其中有的 API 与 DOM 重合,定义了浏览器应该提供的 DOM扩展。
# CSS 类扩展
- getElementsByClassName() 方法
可以通过 document 对象及所有 HTML 元素调用该方法。该方法接收一个参数,即一个包含一或多个类名的字符串,返回带有指定类的所有元素的 NodeList。传入多个类名时,类名的先后顺序不重要。
// 取得所有类名中包含 username 和 current 的元素
let allCurrentUserNames = document.getElementsByClassName('username current')
// 取得 id 为 mydiv 的元素子树中所有包含 selected 类的元素
let selected = document.getElementById('mydiv').getElementsByClassName('selected')
- classList 属性
在操作类名时,需要通过 className 属性添加、删除和替换类名。因为 className 中是一个字符串,所以即使值修改字符串一部分,也必须每次都设置整个字符串的值。 如下代码:
<div class="bd user disabled">...</div>
如果想要删除其中一个:
// 要删除 user 类
let tatgetClass = 'user'
// 把类名拆成数组
let classNames = div.className.split(/\S+/)
// 找到要删除类名的索引
let idx = classNames.indexOf(tatgetClass)
// 如果有则删除
if(idx > -1) {
classNames.splice(idx, 1)
}
// 重新设置类名
div.className = classNames.join(' ')
HTML5 新增了一种操作类名的方式,为所有元素添加了 classList 属性,classList 是一个新的集合类型 DOMTokenList 的实例,与其他 DOM 集合类型一样, DOMTokenList 也有 length 属性表示自己包含多少项,也可以通过 item() 或中括号取得个别的元素。此外,DOMTokenList 还增加了以下方法。
- add(value) ,向类名列表中添加指定的字符串值 value 。如果这个值已经存在,则什么也不做。
- contains(value) ,返回布尔值,表示给定的 value 是否存在。
- remove(value) ,从类名列表中删除指定的字符串值 value 。
- toggle(value) ,如果类名列表中已经存在指定的 value ,则删除;如果不存在,则添加。
上面的例子就可以使用一句代码替换:
div.classList.remove('user')
# 焦点管理
HTML5 增加了辅助 DOM 焦点管理的功能。首先是 document.activeElement ,始终包含当前拥有焦点的 DOM元素。 默认情况下, document.activeElement 在页面刚加载完之后会设置为 document.body 。而在页面完全加载之前, document.activeElement 的值为 null 。
其次是 document.hasFocus() 方法,该方法返回布尔值,表示文档是否拥有焦点:
let button = document.getElementById('myButton')
button.focus()
console.log(document.hasFocus()) // true
# HTMLDocument 扩展
- readyState 属性 document.readyState 属性有两个可能的值:
- loading: 正在加载文档
- complete: 已经加载完文档
实际开发中,最好是把 document.readState 当成一个指示器,以判断文档是否加载完毕。这个属性的基本用法如下:
if (document.readyState === 'complete') {
// 执行操作
}
- 兼容模式
自从 IE6 提供了以标准或混杂模式渲染页面的能力之后,检测页面渲染模式成为一个必要的需求。IE 为 document 添加了 compatMode 属性,这个属性唯一的任务是指示浏览器当前处于什么渲染模式。如下面的例子所示,标准模式下 document.compatMode 的值是 "CSS1Compat" ,而在混杂模式下,document.compatMode 的值是 "BackCompat" :
if (document.compatMode === 'CSS1Compat') {
console.log('Standards mode')
} else {
console.log('Quirks mode')
}
- head 属性
作为对 document.body (指向文档的 body 元素)的补充,HTML5 增加了 document.head 属性,指向文档的 head 元素。可以像下面这样直接取得 head 元素:
let head = document.head || document.getElementByTagName('head')[0]
# 字符集属性
HTML5 增加了几个与文档字符集有关的新属性。其中, characterSet 属性表示文档实际使用的字符集,也可以用来指定新字符集。这个属性的默认值是 "UTF-16" ,但可以通过 meta 元素或响应头,以及新增的 characterSet 属性来修改。下面是一个例子:
console.log(document.characterSet) // "UTF-16"
document.characterSet = 'UTF-8'
# 自定义属性
HTML5 允许给元素指定非标准的属性,但要使用前缀 data- 以便告诉浏览器,这些属性既不包含与渲染有关的信息,也不包含元素的语义信息。
<div id="myDiv" data-appId="12345" data-myname="Nicholas"></div>
定义了自定义数据属性后,可以通过元素的 dataset 属性来访问。 dataset 属性是一个 DOMStringMap 的实例,包含一组键/值对映射。元素的每个 data-name 属性在 dataset 中都可以通过 data- 后面的字符串作为键来访问。
let div = document.getElementById('mydiv')
// 取得自定义属性的值
let appid = div.dataset.appid
let myName = div.dataset.myname
// 设置值
div.dataset.appid = 23456
div.dataset.myname = 'Michael'
// 有没有 myname
if (div.dataset.myname) {
console.log(`Hello, ${div.dataset.myname}`)
}
# 插入标记
- innerHTML 属性
该属性会返回元素所有后代的 HTML 字符串,包括元素、注释和文本节点,如果设置该属性,则会根据提供的字符串值以新的 DOM 子树替代元素中原来包含的所有节点。对于读取 innerHTML 属性,不同的浏览器返回的结果是不同的。如果赋值中不包含任何 HTML 标签,则直接生成一个文本节点。
使用 innerHTNL 插入的 script 标签是不会执行的。多数浏览器支持使用 innerHTML 插入 style 元素。
- outerHTML 属性
读取 outerHTML 属性时,会返回调用它的元素(及所有后代元素)的 HTML 字符串。在写入 outerHTML 属性时,调用它的元素会被传入的 HTML 字符串经解释之后生成的 DOM 子树取代。比如下面的 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 本身。 如果使用 outerHTML 设置 HTML,比如:
div.outerHTML = "<p>This is a paragraph.</p>"
则会得到与执行以下脚本相同的结果:
let p = document.createElement("p");
p.appendChild(document.createTextNode("This is a paragraph."));
div.parentNode.replaceChild(p, div);
新的 p 元素会取代 DOM 树中原来的 div 元素。
- insertAdjacentHTML() 与 insertAdjacentText()
这两个方法时插入标签的方法;它们都接收两个参数:要插入标记的位置和要插入的 HTML 或文本。第一个参数必须是下列值中的一个:
- "beforebegin" ,插入当前元素前面,作为前一个同胞节点;
- "afterbegin" ,在当前元素之下插入一个新的子元素或在第一个子元素之前再插入新的子元素;
- "beforeend" ,在当前元素之下插入一个新的子元素或在最后一个个子元素之前再插入新的子元素;
- "afterend" ,插入当前元素后面,作为下一个同胞节点
假设当前元素是
<p>Hello world!</p>
则 "beforebegin" 和 "afterbegin" 中的 "begin" 指开始标签 p ;而"afterend" 和 "beforeend" 中的 "end" 指结束标签 p 。
// 作为前一个同胞节点插入
element.insertAdjacentHTML("beforebegin", "<p>Hello world!</p>");
element.insertAdjacentText("beforebegin", "Hello world!");
// 作为第一个子节点插入
element.insertAdjacentHTML("afterbegin", "<p>Hello world!</p>");
element.insertAdjacentText("afterbegin", "Hello world!");
// 作为最后一个子节点插入
element.insertAdjacentHTML("beforeend", "<p>Hello world!</p>");
element.insertAdjacentText("beforeend", "Hello world!");
// 作为下一个同胞节点插入
element.insertAdjacentHTML("afterend", "<p>Hello world!</p>"); element.
insertAdjacentText("afterend", "Hello world!");
- scrollIntoView() 方法
scrollIntoView() 方法可以在所有 HTML 元素上调用,通过滚动浏览器窗口或某个容器元素,调用元素就可以出现在视口中。 有两个参数;
alignToTop 是一个布尔值:
- true :窗口滚动后元素的顶部与视口顶部对齐。
- false :窗口滚动后元素的底部与视口底部对齐。
scrollIntoViewOptions 是一个选项对象:
- behavior :定义过渡动画,可取的值为 "smooth" 和 "auto" ,默认为 "auto" 。
- block :定义垂直方向的对齐,可取的值为 "start" 、 "center" 、 "end" 和 "nearest" ,默 认为 "start" 。
- inline :定义水平方向的对齐,可取的值为 "start" 、 "center" 、 "end" 和 "nearest" ,默 认为 "nearest" 。
不传参数等同于 alignToTop 为 true 。
// 确保元素可见
document.forms[0].scrollIntoView()
// 同上
document.forms[0].scrollIntoView(true)
document.forms[0].scrollIntoView({
block: 'start'
})
// 尝试将元素平滑地滚入视口
document.forms[0].scrollIntoView({
behavior: 'smooth',
block: 'start'
})
# 专有扩展
# children 属性
元素的 childNodes 包含所有子节点,这些子节点可能是其他元素、文本节点、注释或处理指令;children 属性是一个 HTMLCollection ,只包含元素的 Element 类型的子节点;
# contains() 方法
DOM 编程中经常需要确定一个元素是不是另一个元素的后代。IE 首先引入了 contains() 方法,让开发者可以在不遍历 DOM 的情况下获取这个信息。 contains() 方法应该在要搜索的祖先元素上调用,参数是待确定的目标节点。
如果目标节点是被搜索节点的后代,contains() 返回 true ,否则返回 false 。下面看一个例子:
console.log(document.documentElement.contains(document.body)) // true
这个例子测试 html 元素中是否包含 body 元素,在格式正确的 HTML 中会返回 true 。