超级面板
文章目录
最新文章
最近更新
文章分类
标签列表
文章归档

Javascript 原生查询 DOM 节点或元素的方法

最近在重新学习基础知识,本文对常用的 js 进行 DOM 查询的操作进行整理。

基本查询

本节主要介绍在文档中通过 id、class、name、css选择器直接进行查询的方式。

id 选择器

使用 document.getElementById,根据 id 返回元素,返回值是 Element,如果不存在,则返回 null。

这个接口很简单,只有几点需要注意:

  • 字母 d 小写,这里容易出错。
  • 元素的 id 是大小写敏感的。
  • 如果存在多个 id 一致(这样是不规范的),则返回第一个查找到的元素。
  • 该方法不会搜索不在文档中的元素,当创建了一个新元素,必须插入文档中才能被查询到。

类选择器

使用 document.getElementsByClassName,根据 class 查找元素,返回值是 HTMLCollection,如果不存在,则返回 null。

这个函数也很简单,看几个例子:

获取所有 class 中同时包括 ‘red’ 和 ‘test’ 的元素:

// 空格隔开多个 class
document.getElementsByClassName('red test');
// 类似于 jQuery 的
$('.red.test');

id 为 ‘main’ 的元素的子节点中,获取所有 class 为’test’的元素

document.getElementById('main').getElementsByClassName('test');
// 类似于 jQuery 的
$('#main .test');

HTMLCollection 是一个类数组对象,可以通过 call、apply 使用数组的一些方法,下面是一个查找所有 class 属性为 ‘test’ 的 div 元素的例子:

var testElements = document.getElementsByClassName('test');
var testDivs = Array.prototype.filter.call(testElements, function(testElement){
return testElement.nodeName === 'DIV';
});

// 也可以使用下文的 css 选择器查询
var testDivs = document.querySelectorAll('div.test');

标签选择器

使用 document.getElementsByTagName,根据标签名查找元素,返回值是 HTMLCollection( W3C 规范说明这些元素是 HTMLCollection,然而这个方法在 WebKit 内核的浏览器中返回一个 NodeList),如果不存在,则返回 null。

该方法类似于 document.getElementsByClassName 不再详细说明。

name 选择器

document.getElementsByName 根据元素的 name 属性来查找元素,返回值是 NodeList,如果不存在,则返回 null。

对于不存在 name 属性的 HTML 元素,如果为其设置 name 属性,则该元素也能被选择到,最常见的比如 div 元素。

注意,这个选择器在不同浏览器的效果是不同的,比如在 IE 和 Opera 浏览器下,这个方法也会返回 id 属性为这个值的元素。在使用的时候,应该小心使用,尽量保证 name 不和其他元素的 id 一致。

css 选择器

常用的选择器如 id、class、属性选择器 都可以使用 document.querySelectordocument.querySelectorAll 进行查找,如果没有找到匹配的元素,则返回 null。他们的区别在于 document.querySelector 返回第一个匹配选择器的元素,而 document.querySelectorAll 返回所有匹配的元素(NodeList)。

这个接口使用深度优先的先序遍历文档的节点进行查找:

<div>
<div class="test">first find.</div>
</div>
<div class="test">second find.</div>

document.querySelectorAll(".test").forEach(function(item) {
console.log(item.innerText);
})

// "first find."
// "second find."

参数也可以是一系列逗号分隔的多个 css 选择器字符串,搜索的元素顺序和选择器的顺序无关:

<div class="test1">test1.</div>
<div class="test2">test2.</div>

document.querySelectorAll(".test1, .test2").forEach(function(item) {
console.log(item.innerText);
})

// "test1."
// "test2."

该方法也可以用在元素上:

<div class="test"><div class="test1">test.test1</div></div>
<div class="test1">root.test1</div>

var ele = document.querySelector('.test');
console.log(ele.querySelector('.test1').innerText);

// "test.test1"

但是,在查看 Selectors API 规范 的时候注意到:

Even though the method is invoked on an element, selectors are still evaluated in the context of the entire document.

这段话的意思是说,即使在元素上调用函数,选择器仍然是在整个文档的上下文环境进行选择,如下示例:

<div class="test"><div class="test1">test.test1</div></div>
<div class="test1">root.test1</div>

var ele = document.querySelector('.test');
console.log(ele.querySelector('body .test1').innerText);

// "test.test1"

选择器 body .test1 并不在 test 的上下文环境中,但是仍然选中了,它的运行模式更像在整个文档中选择,然后过滤出属于该元素子元素的内容,这样做无疑性能很差。

如果指定的选择器不合法,会抛出 SYNTAX_ERR 异常,所以对于不确定的选择器,需要使用 try...catch 处理异常:

try {
document.querySelectorAll("##")
} catch(err) {
console.log('error selector.')
}

// "error selector."

注意,本方法返回的 NodeList 是静态的,也即不会随着文档树的变化而更新,而其他接口(如 document.getElementsByNamedocument.getElementsByClassName) 返回的 HTMLCollectionNodeList 都是动态的,内容会随着元素的更新而更新。

使用 CSS 伪类不会返回任何元素。最后再说一次,这个 api 性能不怎么好,如果条件允许,优先使用上面提到的选择器。

Element.matches(selector)

如果元素匹配被指定的选择器字符串(selector),matches 方法返回 true, 否则返回 false。

有一些浏览器使用前缀, 在非标准名称 matchesSelector() 下实现了这个方法,因此可以实现这个 Polyfill:

if (!Element.prototype.matches) {
Element.prototype.matches =
Element.prototype.matchesSelector ||
Element.prototype.mozMatchesSelector ||
Element.prototype.msMatchesSelector ||
Element.prototype.oMatchesSelector ||
Element.prototype.webkitMatchesSelector ||
function(s) {
return [].indexOf.call(document.querySelectorAll(s), this) !== -1;
};
}

关系查询

除了上面的基本查询,还存在一些常用的比如查找父元素,查找兄弟元素,查找子元素等操作,这一节看一下这些已知某个元素查找其相关操作。

查询子孙元素

首先,节点存在的一些属性就可以做到一些基本的查询:

  • childElementCount 返回给定 Node 的子元素数。
  • children 返回 Node 的子元素,是一个动态更新的 HTMLCollection
  • childNodes 返回给定 Node 的子节点,是一个动态更新的 NodeList,与 children 不同点在于,子节点可能会包含文本节点,注释节点等。
  • firstChild 返回给定 Node 的第一个子节点。
  • lastChild 返回给定 Node 的最后一个子节点。

一些查询可能使用的方法:

  • node.contains(otherNode) 返回 otherNode 是否是 node 的后代节点或是 node 节点本身
  • node.hasChildNodes() 返回一个布尔值,表明当前节点是否包含有子节点。
  • node.isEqualNode(otherNode) 返回一个布尔值,表明两个节点是否相等。当两个节点的类型相同,定义特征相同(对元素来说,即 id,孩子节点的数量等等),属性一致等,这两个节点就是相等的。
  • node.isSameNode(otherNode) 返回一个布尔值,表明两个节点是否为同一个节点。

接下来我们使用这些属性和函数来实现一些常见的查询操作:

查询第一个或者最后一个子元素

前面提到了 firstChildlastChild 返回的结果是 Node 的第一个或者最后一个节点,但是并不能直接使用,因为它们可能返回的是一个被填充的文本节点,比如下面的 示例:

<p id="para-01">
<span>First span</span>
<span>Middle span</span>
<span>Last span</span>
</p>

var p01 = document.getElementById('para-01');
console.log(p01.firstChild.nodeName)
console.log(p01.firstChild.nodeType)

// "#text"
// 3

之所以出现这种情况,是因为在 <p> 标签和 <span> 标签之前,有一个换行符和多个空格充当了一个文本节点。在浏览器中,任意多个的空白符都将导致文本节点的插入,包括一个到多个空格符、换行符、制表符等等。

这种情况下我们获得的元素实际上并不是我们想要的元素,需要稍微改写一下:

/**
* Determine whether a node's text content is entirely whitespace.
*
* Throughout, whitespace is defined as one of the characters
* "\t" TAB \u0009
* "\n" LF \u000A
* "\r" CR \u000D
* " " SPC \u0020
*
* @param node A node implementing the |CharacterData| interface (i.e.,
* a |Text|, |Comment|, or |CDATASection| node
* @return True if all of the text content of |nod| is whitespace,
* otherwise false.
* This does not use Javascript's "\s" because that includes non-breaking
* spaces (and also some other characters).
*/
function isAllWhiteSpace(node) {
// Use ECMA-262 Edition 3 String and RegExp features
return !(/[^\t\n\r ]/.test(node.textContent));
}
/**
* Determine if a node should be ignored by the iterator functions.
*
* @param node An object implementing the DOM1 |Node| interface.
* @return true if the node is:
* 1) A |Text| node that is all whitespace
* 2) A |Comment| node
* and otherwise false.
*/
function isIgnorable(node) {
var isComment = node.nodeType === 8; // A comment node
var isWhiteSpace = node.nodeType === 3 && isAllWhiteSpace(node); // a text node and all whitespace
return isComment || isWhiteSpace;
}

function getFirstChild(parent) {
var res = parent.firstChild;
while (res) {
if (!isIgnorable(res)) return res;
res = res.nextSibling;
}
return null;
}

function getLastChild(parent) {
var res = parent.lastChild;
while (res) {
if (!isIgnorable(res)) return res;
res = res.previousSibling;
}
return null;
}

console.log(getFirstChild(p01).textContent);
console.log(getLastChild(p01).textContent);
// "<span>First span</span>"
// "<span>Last span</span>"

getFirstChild 为例,判断 firstChild 是否为注释节点,如果是则跳过,然后判断是否是内容为空、为制表符的文本节点,如果是则跳过,判断下一个节点。

当然也可以换个思路:使用 childNodes 获取一个元素的所有子节点,并过滤掉注释节点和内容为空、为制表符的文本节点:

function getMeaningNodes(parent) {
return [].filter.call(parent.childNodes, function(node) {
return !isIgnorable(node);
});
}

function getFirstChild(parent) {
var nodes = getMeaningNodes(parent);
return nodes[0] || null;
}

function getLastChild(parent) {
var nodes = getMeaningNodes(parent);
return nodes.length > 0 ? nodes[nodes.length - 1] : null;
}
console.log(getFirstChild(p01).textContent);
console.log(getLastChild(p01).textContent);
// "<span>First span</span>"
// "<span>Last span</span>"

当然一些像 jQuery 的类库会忽略掉文本节点,那么直接使用 ele.children 即可,也不需要再过滤 childNodes 了。

查询第 n 个子节点

这里我们可以使用上一节的 getMeaningNodes 方法返回的 node 列表直接查询:

function getTheNChild(parent, number) {
var nodes = getMeaningNodes(parent);
return nodes.length > number ? nodes[number] : null;
}

console.log(getTheNChild(p01, 0).textContent);
console.log(getTheNChild(p01, 1).textContent);
console.log(getTheNChild(p01, 2).textContent);
console.log(getTheNChild(p01, 3).textContent);

// "First span"
// "Middle span"
// "Last span"
// null

查找指定类型的子元素或节点

可以在元素上使用上面提到的 getElementsByTagNamegetElementsByNamegetElementsByClassName 以及 querySelector 前面已经详细介绍过,不再详细说明。

查找兄弟元素或节点

查找上一个或者下一个兄弟元素或节点

和兄弟元素有关的属性和方法如下:

  • nextSibling 返回当前节点的下一个兄弟节点,没有则返回 null。
  • nextElementSibling 返回当前元素在其父元素的子元素节点中的上一个元素节点,没有则返回 null。
  • previousSibling 返回当前节点的前一个兄弟节点,没有则返回 null。
  • previousElementSibling 返回当前元素在其父元素的子元素节点中的后一个元素节点,没有则返回 null。

这四个属性差不多,区别在于节点可能是注释节点和文本节点,而元素可能会忽略一个文本元素,这个看应用场景,比如 jQuery 就会忽略文本元素:

// previous
$ele.prev();
// 等价于
ele.previousElementSibling;

// next
$ele.next();
// 等价于
ele.nextElementSibling;

查找全部兄弟元素或节点

查找兄弟元素可以选择其父节点的全部子元素,并过滤掉自己:

// jQuery
$ele.siblings();
// 等价于
function getSiblings(ele) {
var children = ele.parentNode.children;
return [].filter.call(children, function(child) {
return child !== ele;
});
}

查找兄弟节点则可以使用父节点的 childNodes 并过滤一下节点,操作和上面类似不再说明。

查找父节点以及祖先节点

和父节点以及祖先节点有关的属性有:

  • parentElement 返回指定的节点的父元素节点。如果该元素没有父节点,或者父节点不是一个元素节点,则返回 null。
  • parentNode 返回指定的节点在 DOM 树中的父节点。一个元素节点的父节点可能是一个元素(Element)节点,也可能是一个文档(Document)节点,或者是个文档片段(DocumentFragment)节点。如果没有父节点,也返回 null。

DocumentDocumentFragment 节点没有父节点,因此也返回 null。如果当前节点刚刚被建立,还没有被插入到DOM树中,则该节点的 parentNode 属性也返回 null。

匹配特定选择器且离当前元素最近的祖先元素,即 closest 的实现:

// jQuery
$ele.closest(selectors);

// in Chrome 41+ and firefox 35+
element.closest(selectors);

// polyfill
function closest(ele, seector) {
const matches = ele.matches || ele.webkitMatchesSelector || ele.mozMatchesSelector || ele.msMatchesSelector;
if (matches) {
while (ele) {
if (matches.call(ele, selector)) {
return ele;
} else {
ele = ele.parentElement;
}
}
}
return null;
}

参考