在上一篇的分享当中,我们简单介绍了BOM 与DOM,也了解到JavaScript 是怎么通过它们提供的方法来与浏览器进行沟通。
当一个网页被载入到浏览器时,浏览器会首先分析这个HTML 文档,然后会依照这份HTML 的内容解析成DOM (Document Object Model,即文件对象模型)。
而DOM 是W3C 制定的一个规范,它是独立于平台与语言的标准。换言之,只要遵守这样的规范,不管是什么平台或者是什么语言开发,都可以通过DOM 提供的API 来操作DOM 的内容、结构与样式。
所以说,DOM 是网页的根本,懂得控制DOM就可以控制整个网页,做出良好的互动体验。
那么在今天的分享中,我们就继续来介绍DOM API 查找节点的方法吧。
前言:<script>标签放哪里有区别吗?
针对<script>
标签放哪里,这个题目其实没有标准答案,一般你会看到有两种版本:
- 放在
<head> ... </head>
之间 - 放在
</body>
之前
还有人会说为什么放在<head> ... </head>
里面的JavaScript没有作用?我觉得这说的有出入,这里我们简单来讲一下问题所在。
那么我们来试试上一篇介绍过的,先以document.querySelector
取得id="hello"
的节点,然后通过textContent
来修改内容。
先来试试把<script>
标签放在</body>
之前。在jsbin里面马上执行看看,看起来似乎很ok呢!
接着,我们试着把<script>
标签移到<head> ... </head>
之间:
咦?怎么什么都没有呢?而且也没有错误信息,JavaScript真的如大家说的一样,很垃圾吗?
冷静一下,容我解释一下。
前面说过,当一个网页被载入到浏览器时,浏览器会先分析这个HTML 文档,由上而下依序来读取解析:
所以上面jsbin例子中,当浏览器在<head> ... </head>
之间遇到<script>
标签时,就会暂停解析网页,并且立即执行<script>
里的内容,直到script执行完毕后再继续解析网页。
当<head> ... </head>
里的<script>
想要尝试去寻找<div id="hello">
这个标签,但因为还没解析到网页本体,所以也无从取得。
不是浏览器坏掉,也不是JavaScript太渣,而是因为我们不理解浏览器执行的原理所造成的误会。
这里是浏览器加载一个有 <script> 标签的网站所发生的事情:
- 拉取 HTML 页面
- 开始解析 HTML
- 解析到 <script> 标签之后准备获取 script 文件.
- 浏览器获取script文件。同时,html 解析中断并且阻断页面上其他html的解析。
- 一段时间后,script下载完成并且执行。
- 继续解析HTML文档的其他部分(解析script之后的html代码)
第4步导致了很不好的用户体验,直到script文件全部下载完成之前HTML都不能得到解析。
那么,当我们把<script>
标签放在</body>
结束之前,由于DOM已经解析完成,所以document.querySelector
就可以顺利取得id="hello"
的节点,并且把'HELLO'
的字串放在网页里啦!
这样说起来,<script>
标签是不是就不适合放在<head> ... </head>
之间呢?
也不能这么说,这点认真要讲的话之后或许可以用一整篇来说明这个。
DOM 节点的选取
上一篇文章说过,document
对象是DOM tree的根节点,所以当我们要存取HTML时,都从document对象开始。而DOM的节点类型除了HTML元素节点(element nodes)外,还有文字节点(text nodes)、注释节点(comment nodes)等。
而常见的DOM 选取方法有下列这些:
// 根据传入的值,找到 DOM 中 id 为 'xxx' 的元素。
document.getElementById('xxx');
// 针对给定的 tag 名称,返回所有符合条件的 NodeList 对象(节点的集合)
document.getElementsByTagName('xxx');
// 针对给定的 class 名称,返回所有符合条件的节点集合
document.getElementsByClassName('xxx');
// 针对给定的 Selector 条件,返回第一个 或 所有符合条件的节点集合
document.querySelector('xxx');
document.querySelectorAll('xxx');
DOM 节点的类型
DOM 常用的节点类型有下面几种:
可以通过节点类型常数或是对应数值来判断:
document.nodeType === Node.DOCUMENT_NODE; //true
document.nodeType === 9; //true
其他不常用或是已经废弃的部分可以参考:MDN Node.nodeType一节。
DOM 节点间的查找遍历(Traversing)
由于DOM 节点有分层的概念,于是节点与节点之间的关系,我们大致上可以分成以下两种:
- 父子关系:
除了document
之外,每一个节点都会有个上层的节点,我们通常称之为「父节点」 (Parent node),而相对地,从属于自己下层的节点,就会称为「子节点」 (Child node)。 - 兄弟关系:有同一个「父节点」的节点,那么他们彼此之间就是「兄弟节点」(Siblings node)。
而隔层的节点基本上没有直接关系。
上图中水平方向的邻层节点为父子关系,垂直方向的同层节点为兄弟关系。
Node.childNodes
所有的DOM节点对象都有childNodes
属性,且此种属性无法修改。
我们可以通过Node.hasChildNodes()
来检查某个DOM节点是否有子节点。
var node = document.querySelector('#hello');
// 如果 node 內有子元素
if( node.hasChildNodes() ) {
// 可以通过 node.childNodes[n] (n 为数字索引) 取得对应的节点
// 注意,NodeList 对象內容为即时更新的集合
for (var i = 0; i < node.childNodes[i].length; i++) {
// ...
};
}
Node.childNodes
返回的可能会有这几种:
- HTML 元素节点(element nodes)
- 文字节点(text nodes),包含空格
- 注释节点(comment nodes)
Node.firstChild
Node.firstChild
可以取得Node
节点的第一个子节点,如果没有子节点则返回null
。
要注意的是,子节点包括空白节点,如下面例子:
<p>
<span>span 1</span>
<span>span 2</span>
<span>span 3</span>
</p>
<script>
var p = document.querySelector('p');
// tagName 属性可以取得 node 的标签名称
console.log(p.firstChild.tagName); // undefined
</script>
因为取得的是<p>
与第一个<span>
中间的换行字元,所以p.firstChild.tagName
会得到undefined
。所以改成这样:
<p><span>span 1</span><span>span 2</span><span>span 3</span></p>
<script>
var p = document.querySelector('p');
// tagName 属性可以取得 node 的标签名称
console.log(p.firstChild.tagName); // "SPAN"
</script>
把中间的换行与空白移除,就会得到预期中的"SPAN"
了。
Node.lastChild
Node.lastChild
可以取得Node
节点的最后一个子节点,如果没有子节点则返回null
。
与Node.firstChild
一样的是,子节点也包括空白节点,所以像这样:
<p>
<span>span 1</span>
<span>span 2</span>
<span>span 3</span>
</p>
<script>
var p = document.querySelector('p');
// textContent 属性可以取得节点内的文字内容
console.log(p.lastChild.textContent); // "" (换行字元)
</script>
得到的会是一个换行字元的空字符串。
移除节点之间多余的空白后:
<p><span>span 1</span><span>span 2</span><span>span 3</span></p>
<script>
var p = document.querySelector('p');
// textContent 属性可以取得节点内的文字内容
console.log(p.lastChild.textContent); // "span 3"
</script>
输出的就会是正确的"span 3" 啦。
Node.parentNode
那么相较于Child系列,parentNode
就单纯一些。
通过Node.parentNode
可以用来取得父元素,返回值可能会是一个元素节点(Element node)、根节点(Document node)或DocumentFragment节点。
<p><span>span 1</span><span>span 2</span><span>span 3</span></p>
<script>
var el = document.querySelector('span');
console.log( el.parentNode.nodeName ); // "P"
</script>
Node.previousSibling
看完了DOM父与子之后,接着来看看兄弟节点。
通过Node.previousSibling
可以取得同层之间的前一个节点,如果node已经是第一个节点且前面无节点,则返回null
。
<p><span>span 1</span><span>span 2</span><span>span 3</span></p>
<script>
var el = document.querySelector('span');
console.log( el.previousSibling ); // null
// document.querySelectorAll 会取得所有符合条件的集合,
// 而 document.querySelectorAll('span')[2] 指的是「第三个」符合条件的元素。
var el2 = document.querySelectorAll('span')[2];
console.log( el2.previousSibling.textContent ); // "span 2"
</script>
Node.nextSibling
与Node.previousSibling
类似,通过Node.nextSibling
可以取得同层之间的下一个节点,如果node已经是最后一个节点,则返回null
。
<p><span>span 1</span><span>span 2</span><span>span 3</span></p>
<script>
// document.querySelector 会取得第一个符合条件的元素
var el = document.querySelector('span');
console.log( el.nextSibling.textContent ); // "span 2"
</script>
document.getElementsBy**与document.querySelector/ document.querySelectorAll的差异
今天分享了很多关于DOM的选取以及查找遍历的方式,其中,像是document.getElementById
以及document.querySelector
因为取得的一定只会有一个元素/节点,所以不会有index与length属性。
而document.getElementsBy**
(注意,这里有个s)以及document.querySelectorAll
则分别返回HTMLCollection与NodeList。
这两者其实是有点差别的,HTMLCollection只收集HTML element 节点,而NodeList除了HTML element 节点,也包含文字节点、属性节点等。当然两者也有类似的地方,虽然不能使用数组的method,但这两种都可以用数组索引的方式来存取内容,也就是伪数组。
另一个需要注意的地方是,HTMLCollection
/NodeList
在大部分情况下是即时更新的,但通过document.querySelector
/document.querySelectorAll
取得的NodeList是静态的。
啥意思呢?举个例子:
<div id="outer">
<div id="inner">inner</div>
</div>
<script>
// <div id="outer">
var outerDiv = document.getElementById('outer');
// 所有的 <div> 标签
var allDivs = document.getElementsByTagName('div');
console.log(allDivs.length); // 2
// 清空 <div id="outer"> 下的节点
outerDiv.innerHTML = '';
// 因为清空了<div id="outer"> 下的节点,所以只剩下 outer
console.log(allDivs.length); // 1
</script>
如果改成document.querySelector
的写法:
<div id="outer">
<div id="inner">inner</div>
</div>
<script>
// <div id="outer">
var outerDiv = document.getElementById('outer');
// 所有的 <div> 标签
var allDivs = document.querySelectorAll('div');
console.log(allDivs.length); // 2
// 清空 <div id="outer"> 下的节点
outerDiv.innerHTML = '';
// document.querySelector 返回的是静态的 NodeList,不受 outerDiv 更新影响
console.log(allDivs.length); // 2
</script>
那么以上就是今天所要介绍的内容啦。