mirror of
https://github.com/apachecn/eloquent-js-3e-zh.git
synced 2025-05-23 20:02:20 +00:00
14.
This commit is contained in:
parent
88363557af
commit
993125c921
604
14.md
Normal file
604
14.md
Normal file
@ -0,0 +1,604 @@
|
||||
## 十四、文档对象模型
|
||||
|
||||
当你在浏览器中打开网页时,浏览器会接收网页的HTML文本并进行解析,其解析方式与第11章中介绍的解析器非常相似。浏览器构建文档结构的模型,并使用该模型在屏幕上绘制页面。
|
||||
|
||||
JavaScript在其沙箱中提供了将文本转换成文档对象模型的功能。你可以读取模型,也可以修改模型。模型是一个所见即所得的数据结构,改变模型会使得屏幕上的页面产生相应变化。
|
||||
|
||||
### 13.1 文档结构
|
||||
|
||||
你可以将HTML文件想象成一系列嵌套的箱子。诸如<body>和</body>之类的标签会将其他标签包围起来,而包含在内部的标签也可以包含其他的标签和文本。这里给出上一章中已经介绍过的示例文件。
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>My home page</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>My home page</h1>
|
||||
<p>Hello, I am Marijn and this is my home page.</p>
|
||||
<p>I also wrote a book! Read it
|
||||
<a href="http://eloquentjavascript.net">here</a>.</p>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
该页面结构如下所示。
|
||||
|
||||

|
||||
|
||||
浏览器使用与该形状对应的数据结构来表示文档。每个盒子都是一个对象,我们可以和这些对象交互,找出其中包含的盒子与文本。我们将这种表示方式称为文档对象模型(Document Object Model),或简称DOM。
|
||||
|
||||
我们可以通过全局变量document来访问这些对象。该对象的documentElement属性引用了表示<html>标签的对象。而<html>标签对象又提供了属性head和body,分别保存了这些元素对应的对象。
|
||||
|
||||
### 13.2 树
|
||||
|
||||
回想一下第11章中提到的语法树。其结构与浏览器文档的结构极为相似。每个节点可以使用children引用其他节点,而每个子节点又有各自的children。其形状是一种典型的嵌套结构,每个元素可以包含与其自身相似的子元素。
|
||||
|
||||
如果一个数据结构有分支结构,而且没有任何回环(一个节点不能直接或间接包含自身),并且有一个单一、定义明确的“根节点”,那么我们将这种数据结构称之为树。就DOM来讲,document.documentElement就是其根节点。
|
||||
|
||||
在计算机科学中,树的应用极为广泛。除了表现诸如HTML文档或程序之类的递归结构,树还可以用于维持数据的有序集合,因为在有序的树中寻找或插入一个节点往往比在有序的数组中更高效。
|
||||
|
||||
一棵典型的树有不同类型的节点。Egg语言的语法树有变量、值和应用节点。应用节点常常包含子节点,而变量、值则是叶子节点,也就是没有孩子的节点。
|
||||
|
||||
DOM中也是一样。正规元素(即HTML标签)的节点用于确定文档结构。这些节点可以包含子节点。这类节点中的一个例子是document.body。其中一些子节点可以是叶子节点,比如文本片段或注释(HTML中注释写在<!-和->之间)。
|
||||
|
||||
每个DOM节点对象都包含nodeType属性,该属性包含一个标识节点类型的数字代码。常规元素的值为1,DOM也将该值定义成一个常量属性document.ELEMENT_NODE。文本节点(表示文档中的一段文本)的nodeType属性值是3(document.TEXT_NODE)。注释的值为8(document.COMMENT_NODE)。因此我们可以使用另一种方法来表示文档树:
|
||||
|
||||

|
||||
|
||||
叶子节点是文本节点,而箭头则指出了节点之间的父子关系。
|
||||
|
||||
### 13.3 标准
|
||||
|
||||
并非只有JavaScript会使用数字代码来表示节点类型。本章随后将会展示其他的DOM接口,你可能会觉得这些接口有些奇怪。这是因为DOM并不是为JavaScript而设计的,它定义了一组语言中性的接口,确保也可用于其他系统中,不只是HTML,还有XML。XML是一种通用数据格式,语法与HTML相近。
|
||||
|
||||
这就比较糟糕了。一般情况下标准都是非常易于使用的。但在这里其优势(跨语言的一致性)并不明显。相较于为不同语言提供类似的接口,如果能够将接口与开发者使用的语言进行适当集成,可以为开发者节省大量时间。
|
||||
|
||||
我们举例来说明一下集成问题。比如DOM中每个元素都有childNodes属性。该属性是一个类似于数组的对象,有length属性,也可以使用数字标签访问对应的孩子节点。但该属性是NodeList类型的实例,而不是真正的数组,因此该类型没有诸如slice和forEach之类的方法。
|
||||
|
||||
有些问题是由不好的设计导致的。例如,我们无法在创建新的节点的同时立即为其添加孩子和属性。相反,你首先需要创建节点,然后将孩子节点逐个添加到节点中,最后利用副作用逐个设置属性。大量使用DOM的结果就是代码冗长。
|
||||
|
||||
但这些问题并非无法改善。我们可以使用JavaScript构建自己的抽象,因此可以很容易地编写一些工具函数,以更清晰简短的方式描述你的操作。实际上,许多为浏览器设计的库都提供了这类工具。
|
||||
|
||||
### 13.4 通过树结构访问节点
|
||||
|
||||
DOM节点包含了许多指向相邻节点的链接。下面的图表展示了这一点。
|
||||
|
||||

|
||||
|
||||
尽管图表中每种类型的节点只显示出一条链接,但每个节点都有parentNode属性,用于指向包含该节点的节点。类似的,每个元素节点(节点类型为1)均包含childNodes属性,该属性指向一个类似于数组的对象,用于保存其孩子节点。
|
||||
|
||||
理论上,你可以通过父子之间的链接移动到树中的任何地方。但JavaScript也提供了一些更加方便的额外链接。firstChild属性和lastChild属性分别指向第一个孩子和最后一个孩子,若没有孩子则值为null。类似的,previousSibling和nextSibling指向相邻节点,分别指向拥有相同父亲的前一个节点和后一个节点。对于第一个孩子,previousSibling是null,而最后一个孩子的nextSibling则是null。
|
||||
|
||||
递归函数非常适合用于处理这类嵌套数据结构。下面的递归函数会扫描一个文档,搜索包含特定字符串的文本节点,并在找到一个节点时返回true。
|
||||
|
||||
```html
|
||||
function talksAbout(node, string) {
|
||||
if (node.nodeType == document.ELEMENT_NODE) {
|
||||
for (var i = 0; i < node.childNodes.length; i++) {
|
||||
if (talksAbout(node.childNodes[i], string))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} else if (node.nodeType == document.TEXT_NODE) {
|
||||
return node.nodeValue.indexOf(string) > -1;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(talksAbout(document.body, "book"));
|
||||
// → true
|
||||
```
|
||||
|
||||
文本节点的nodeValue属性指向该节点表示的文本字符串。
|
||||
|
||||
### 13.5 查找元素
|
||||
|
||||
使用父节点、子节点和兄弟节点之间的连接遍历节点确实非常实用,前面的函数就使用这种方法遍历了整个文档。但是如果我们只想查找文档中的特定节点,那么从document.body开始盲目沿着硬编码的链接路径查找节点并非良策。如果程序通过树结构定位节点,就需要依赖于文档的具体结构,而文档结构随后可能发生变化。另一个复杂的因素是DOM会为不同节点之间的空白字符创建对应的文本节点。例如示例文档中的body标签不止包含3个孩子(<h1>和两个<p>元素),其实包含7个孩子:这三个节点、三个节点前后的空格、以及元素之间的空格。
|
||||
|
||||
因此,如果你想获取文档中某个链接的href属性,最好不要去获取文档body元素中第六个孩子的第二个孩子,而最好直接获取文档中的第一个链接,而且这样的操作确实可以实现。
|
||||
|
||||
```html
|
||||
var link = document.body.getElementsByTagName("a")[0];
|
||||
console.log(link.href);
|
||||
```
|
||||
|
||||
所有元素节点都包含getElementsByTagName方法,用于从所有后代节点中(直接或间接孩子节点)搜索包含特定标签名的节点,并返回一个类似于数组的对象。
|
||||
|
||||
你也可以使用document.getElementById来寻找包含特定id属性的某个节点。
|
||||
|
||||
```html
|
||||
<p>My ostrich Gertrude:</p>
|
||||
<p><img id="gertrude" src="img/ostrich.png"></p>
|
||||
|
||||
<script>
|
||||
var ostrich = document.getElementById("gertrude");
|
||||
console.log(ostrich.src);
|
||||
</script>
|
||||
```
|
||||
|
||||
第三个类似的方法是getElementsByClassName,它与getElementsByTagName类似,会搜索元素节点的内容并获取所有包含特定class属性的元素。
|
||||
|
||||
### 13.6 修改文档
|
||||
|
||||
几乎所有DOM数据结构中的元素都可以被修改。元素节点提供了一系列用于修改其内容的方法。removeChild方法用于从文档中移除特定节点。appendChild方法可以添加孩子节点,并将其放置在孩子节点列表末尾,而insertBefore则将第一个参数表示的节点插入到第二个参数表示的节点前面。
|
||||
|
||||
```html
|
||||
<p>One</p>
|
||||
<p>Two</p>
|
||||
<p>Three</p>
|
||||
|
||||
<script>
|
||||
var paragraphs = document.body.getElementsByTagName("p");
|
||||
document.body.insertBefore(paragraphs[2], paragraphs[0]);
|
||||
</script>
|
||||
```
|
||||
|
||||
每个节点只能存在于文档中的某一个位置。因此,如果将段落Three插入到段落One前,会将该节点从文档末尾移除并插入到文档前面,最后结果为“Three/One/Two”。所有将节点插入到某处的方法都有这种副作用——会将其从当前位置移除(如果存在的话)。
|
||||
|
||||
replaceChild方法用于将一个孩子节点替换为另一个孩子节点。该方法接受两个参数,第一个参数是新节点,第二个参数是待替换的节点。待替换的节点必须是该方法调用者的孩子节点。这里需要注意,replaceChild和insertBefore都将新节点作为第一个参数。
|
||||
|
||||
### 13.7 创建节点
|
||||
|
||||
在下面的示例中,我们想要编写一个脚本,将文档中所有的图像(<img>标签)替换成包含其alt属性(用于以文本形式展示图片)的文本。
|
||||
|
||||
这不仅牵涉到删除图像,还涉及添加新的文本节点,并替换原有图像节点。为此我们使用document.createTextNode方法。
|
||||
|
||||
```html
|
||||
<p>The <img src="img/cat.png" alt="Cat"> in the
|
||||
<img src="img/hat.png" alt="Hat">.</p>
|
||||
|
||||
<p><button onclick="replaceImages()">Replace</button></p>
|
||||
|
||||
<script>
|
||||
function replaceImages() {
|
||||
var images = document.body.getElementsByTagName("img");
|
||||
for (var i = images.length - 1; i >= 0; i--) {
|
||||
var image = images[i];
|
||||
if (image.alt) {
|
||||
var text = document.createTextNode(image.alt);
|
||||
image.parentNode.replaceChild(text, image);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
使用字符串调用createTextNode,会得到一个类型为3的DOM节点(文本节点),我们可以将其插入到文档中,展示在屏幕上。
|
||||
|
||||
该循环从节点列表末尾开始遍历图像。我们必须这样反向遍历列表,因为getElementsByTagName之类的方法返回的节点列表是动态变化的。该列表会随着文档改变还改变。若我们从列表头开始遍历,移除掉第一个图像会导致列表丢失其第一个元素,第二次循环时,因为集合的长度此时为1,而i也为1,所以循环会停止。
|
||||
|
||||
如果你想要获得一个稳定不变的节点集合,可以使用数组的slice方法将其转换成实际数组。
|
||||
|
||||
```html
|
||||
var arrayish = {0: "one", 1: "two", length: 2};
|
||||
var real = Array.prototype.slice.call(arrayish, 0);
|
||||
real.forEach(function(elt) { console.log(elt); });
|
||||
// → one
|
||||
// two
|
||||
```
|
||||
|
||||
你可以使用document.createElement方法创建一个正规元素节点(类型为1)。该方法接受一个标签名,返回一个新的空节点,节点类型由标签名指定。
|
||||
|
||||
下面的示例定义了一个elt工具,用于创建一个新的元素节点,并将其剩余参数当作该节点的孩子节点。接着使用该函数为引用添加简单的来源信息。
|
||||
|
||||
|
||||
```html
|
||||
<blockquote id="quote">
|
||||
No book can ever be finished. While working on it we learn
|
||||
just enough to find it immature the moment we turn away
|
||||
from it.
|
||||
</blockquote>
|
||||
|
||||
<script>
|
||||
function elt(type) {
|
||||
var node = document.createElement(type);
|
||||
for (var i = 1; i < arguments.length; i++) {
|
||||
var child = arguments[i];
|
||||
if (typeof child == "string")
|
||||
child = document.createTextNode(child);
|
||||
node.appendChild(child);
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
document.getElementById("quote").appendChild(
|
||||
elt("footer", "—",
|
||||
elt("strong", "Karl Popper"),
|
||||
", preface to the second editon of ",
|
||||
elt("em", "The Open Society and Its Enemies"),
|
||||
", 1950"));
|
||||
</script>
|
||||
```
|
||||
|
||||
### 13.8 属性
|
||||
|
||||
我们可以通过元素的DOM对象的同名属性去访问元素的某些属性,比如链接的href属性。但只有常用的标准属性中很少的一部分是这样的。
|
||||
|
||||
HTML允许你在节点上设定任何属性。这一特性非常有用,因为这样你就可以在文档中存储额外信息。你自己创建的属性不会出现在元素节点的属性中。你需要使用getAttribute和setAttribute方法来访问这些属性。
|
||||
|
||||
```html
|
||||
<p data-classified="secret">The launch code is 00000000.</p>
|
||||
<p data-classified="unclassified">I have two feet.</p>
|
||||
|
||||
<script>
|
||||
var paras = document.body.getElementsByTagName("p");
|
||||
Array.prototype.forEach.call(paras, function(para) {
|
||||
if (para.getAttribute("data-classified") == "secret")
|
||||
para.parentNode.removeChild(para);
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
我建议在自己创建的属性前加上诸如data-之类的前缀,确保这些属性和其他属性不会冲突。
|
||||
|
||||
我们看一个简单的例子,我们编写一个“语法高亮器”,搜索带有data-language属性的<pre>标签(“预格式化”,用于代码和类似的普通文本),并尝试加亮语言中的关键字。
|
||||
|
||||
```html
|
||||
function highlightCode(node, keywords) {
|
||||
var text = node.textContent;
|
||||
node.textContent = ""; // Clear the node
|
||||
|
||||
var match, pos = 0;
|
||||
while (match = keywords.exec(text)) {
|
||||
var before = text.slice(pos, match.index);
|
||||
node.appendChild(document.createTextNode(before));
|
||||
var strong = document.createElement("strong");
|
||||
strong.appendChild(document.createTextNode(match[0]));
|
||||
node.appendChild(strong);
|
||||
pos = keywords.lastIndex;
|
||||
}
|
||||
var after = text.slice(pos);
|
||||
node.appendChild(document.createTextNode(after));
|
||||
}
|
||||
```
|
||||
|
||||
函数highlightCode接受<pre>节点和一个正则表达式(启用全局选项),用于匹配元素包含的程序设计语言中的关键字。
|
||||
|
||||
我们使用属性textContent从节点中获取所有文本,然后将其设置为空字符串,以清空节点。我们不断循环找出文本中的关键字,并为两个关键字之间的文本创建单独的文本节点,而将匹配的文本(即关键字)包裹在<strong>(粗体)元素内。
|
||||
|
||||
我们可以通过循环遍历所有带有data-language属性的<pre>元素,并使用特定语言正确的正则表达式逐个调用highlightCode。
|
||||
|
||||
```html
|
||||
var languages = {
|
||||
javascript: /\b(function|return|var)\b/g /* … etc */
|
||||
};
|
||||
|
||||
function highlightAllCode() {
|
||||
var pres = document.body.getElementsByTagName("pre");
|
||||
for (var i = 0; i < pres.length; i++) {
|
||||
var pre = pres[i];
|
||||
var lang = pre.getAttribute("data-language");
|
||||
if (languages.hasOwnProperty(lang))
|
||||
highlightCode(pre, languages[lang]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
以下是示例代码。
|
||||
|
||||
```html
|
||||
<p>Here it is, the identity function:</p>
|
||||
<pre data-language="javascript">
|
||||
function id(x) { return x; }
|
||||
</pre>
|
||||
|
||||
<script>highlightAllCode();</script>
|
||||
```
|
||||
|
||||
这里有一个常用的属性:class。该属性是JavaScript中的保留字。因为某些历史原因(某些旧版本的JavaScript实现无法处理和关键字或保留字同名的属性),访问class的属性名为className。你也可以使用getAttribute和setAttribute方法,使用其实际名称“class”来访问该属性。
|
||||
|
||||
### 13.9 布局
|
||||
|
||||
你可能已经注意到不同类型的元素有不同的布局。某些元素,比如段落(<p>)和标题(<h1>)会占据整个文档的宽度,并且在独立的一行中渲染。这些元素被称为块(Block)元素。其他的元素,比如链接(<a>或前面示例中提到的<strong>元素则与周围文本在同一行中渲染。这类元素我们称之为内联(Inline)元素。
|
||||
|
||||
对于任意特定文档,浏览器可以根据每个元素的类型和内容计算其尺寸与位置等布局信息。接着使用布局来绘制文档。
|
||||
|
||||
JavaScript中可以访问元素的尺寸与位置。
|
||||
|
||||
属性offsetWidth和offsetHeight给出元素的起始位置(单位是像素)。像素是浏览器中的基本测量单元,一般对应于屏幕上可以显示的最小点。类似的,clientWidth和clientHeight则告知元素内部占据的空间尺寸(忽略边界宽度)。
|
||||
|
||||
```html
|
||||
<p style="border: 3px solid red">
|
||||
I'm boxed in
|
||||
</p>
|
||||
|
||||
<script>
|
||||
var para = document.body.getElementsByTagName("p")[0];
|
||||
console.log("clientHeight:", para.clientHeight);
|
||||
console.log("offsetHeight:", para.offsetHeight);
|
||||
</script>
|
||||
```
|
||||
|
||||
getBoundingClientRect方法是获取屏幕中某个元素精确位置的最有效方法。该方法返回一个对象,包含top、bottom、left和right四个属性,表示元素相对于屏幕左上角的位置(单位是像素)。若你想要知道其相对于整个文档的位置,必须加上其滚动位置,可以通过全局变量pageXOffset和pageYOffset获取。
|
||||
|
||||
我们还需要花些力气才能完成文档的排版工作。为了加快速度,浏览器引擎不会每次立即重新绘制整个文档,而是尽可能等待并推迟重绘操作。当一个修改文档的JavaScript程序结束时,浏览器会计算新的布局,并在屏幕上显示修改过的文档。若程序通过读取offsetHeight和getBoundingClientRect这类属性获取某些元素的位置或尺寸时,为了提供正确的信息,浏览器也需要计算布局。
|
||||
|
||||
如果程序反复读取DOM布局信息或修改DOM,会引发大量布局计算,导致运行非常缓慢。下面的代码展示了一个示例。该示例包含两个不同的程序,使用X字符构建一条线,其长度是2000像素,并计算每个任务的时间。
|
||||
|
||||
```html
|
||||
<p><span id="one"></span></p>
|
||||
<p><span id="two"></span></p>
|
||||
|
||||
<script>
|
||||
function time(name, action) {
|
||||
var start = Date.now(); // Current time in milliseconds
|
||||
action();
|
||||
console.log(name, "took", Date.now() - start, "ms");
|
||||
}
|
||||
|
||||
time("naive", function() {
|
||||
var target = document.getElementById("one");
|
||||
while (target.offsetWidth < 2000)
|
||||
target.appendChild(document.createTextNode("X"));
|
||||
});
|
||||
// → naive took 32 ms
|
||||
|
||||
time("clever", function() {
|
||||
var target = document.getElementById("two");
|
||||
target.appendChild(document.createTextNode("XXXXX"));
|
||||
var total = Math.ceil(2000 / (target.offsetWidth / 5));
|
||||
for (var i = 5; i < total; i++)
|
||||
target.appendChild(document.createTextNode("X"));
|
||||
});
|
||||
// → clever took 1 ms
|
||||
</script>
|
||||
```
|
||||
|
||||
### 13.10 样式
|
||||
|
||||
我们看到了不同的HTML元素会有不同的显示效果。一些元素显示为块,一些则是以内联方式显示。我们还可以添加一些样式,比如使用<strong>加粗内容,或使用<a>使内容变成蓝色,并添加下划线。
|
||||
|
||||
<img>标签显示图片的方式或点击标签<a>时跳转的链接都和元素类型紧密相关。但元素的默认样式,比如文本的颜色、是否有下划线,都是可以改变的。这里给出使用style属性的示例。
|
||||
|
||||
```html
|
||||
<p><a href=".">Normal link</a></p>
|
||||
<p><a href="." style="color: green">Green link</a></p>
|
||||
```
|
||||
|
||||
样式属性可以包含一个或多个声明,格式为属性(比如color)后跟着一个冒号和一个值(比如green)。当包含更多声明时,不同属性之间必须使用分号分隔,比如“color:red;border:none”。
|
||||
|
||||
样式会受到很多因素的影响。例如,display属性控制一个元素是否显示为块元素或内联元素。
|
||||
|
||||
```html
|
||||
This text is displayed <strong>inline</strong>,
|
||||
<strong style="display: block">as a block</strong>, and
|
||||
<strong style="display: none">not at all</strong>.
|
||||
```
|
||||
|
||||
标签block会结束其所在的那一行,因为块元素是不会和周围文本内联显示的。最后一个标签完全不会显示出来,因为display:none会阻止一个元素呈现在屏幕上。这是隐藏元素的一种方式。更好的方式是将其从文档中完全移除,因为随后将其放回去是一件很简单的事情。
|
||||
|
||||
JavaScript代码可以通过节点的style属性操作元素的样式。该属性保存了一个对象,对象中存储了所有可能的样式属性,这些属性的值是字符串,我们可以把字符串写入属性,修改某些方面的元素样式。
|
||||
|
||||
```html
|
||||
<p id="para" style="color: purple">
|
||||
Pretty text
|
||||
</p>
|
||||
|
||||
<script>
|
||||
var para = document.getElementById("para");
|
||||
console.log(para.style.color);
|
||||
para.style.color = "magenta";
|
||||
</script>
|
||||
```
|
||||
|
||||
一些样式属性名包含破折号,比如font-family。由于这些属性的命名不适合在JavaScript中使用(你必须写成style[“font-family”]),因此在JavaScript中,样式对象中的属性名都移除了破折号,并将破折号之后的字母大写(style.fontFamily)。
|
||||
|
||||
### 13.11 层叠样式
|
||||
|
||||
我们把HTML的样式化系统称为CSS,即层叠样式表(Cascading Style Sheets)。样式表是一系列规则,指出如何为文档中元素添加样式。可以在<style>标签中写入CSS。
|
||||
|
||||
```html
|
||||
<style>
|
||||
strong {
|
||||
font-style: italic;
|
||||
color: gray;
|
||||
}
|
||||
</style>
|
||||
<p>Now <strong>strong text</strong> is italic and gray.</p>
|
||||
```
|
||||
|
||||
所谓层叠指的是将多条规则组合起来产生元素的最终样式。在上面的示例中,<strong>标签的默认样式font-weight:bold,会被<style>标签中的规则覆盖,并为<strong>标签样式添加font-style和color属性。
|
||||
|
||||
当多条规则重复定义同一属性时,最近的规则会拥有最高的优先级。因此如果<style>标签中的规则包含font-weight:normal,与默认的font-weight规则冲突,那么文本将会显示为普通样式,而非粗体。属性style中的样式会直接作用于节点,而且往往拥有最高优先级。
|
||||
|
||||
我们可以在CSS规则中使用标签名来定位标签。规则.abc指的是所有class属性中包含“abc”的元素。规则#xyz作用于id属性为“xyz”(应当在文档中唯一存在)的元素。
|
||||
|
||||
```css
|
||||
.subtle {
|
||||
color: gray;
|
||||
font-size: 80%;
|
||||
}
|
||||
#header {
|
||||
background: blue;
|
||||
color: white;
|
||||
}
|
||||
/* p elements, with classes a and b, and id main */
|
||||
p.a.b#main {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
```
|
||||
|
||||
优先级规则(偏向于最近定义的规则)只有在规则特殊性相同的情况下有效。规则的特殊性用于衡量该规则描述匹配元素时的准确性。特殊性取决于规则中的元素数量和类型(tag、class或id)。例如,目标规则p.a比目标规则p或.a更具体,因此有更高优先级。
|
||||
|
||||
p>a{….}这种写法将样式作用于<p>标签的直系孩子。类似的,p a{…}应用于所有的<p>标签中的<a>标签,无论是否是直系孩子。
|
||||
|
||||
### 13.12 查询选择器
|
||||
|
||||
本书不会使用太多样式表。尽管理解样式表对浏览器程序设计至关重要,想要正确解释所有浏览器支持的属性及其使用方式,可能需要两到三本书才行。
|
||||
|
||||
我介绍选择器语法(用在样式表中,确定样式作用的元素)的主要原因是这种微型语言同时也是一种高效的DOM元素查找方式。
|
||||
|
||||
document对象和元素节点中都定义了querySelectorAll方法,该方法接受一个选择器字符串并返回类似于数组的对象,返回的对象中包含所有匹配的元素。
|
||||
|
||||
```html
|
||||
<p>And if you go chasing
|
||||
<span class="animal">rabbits</span></p>
|
||||
<p>And you know you're going to fall</p>
|
||||
<p>Tell 'em a <span class="character">hookah smoking
|
||||
<span class="animal">caterpillar</span></span></p>
|
||||
<p>Has given you the call</p>
|
||||
|
||||
<script>
|
||||
function count(selector) {
|
||||
return document.querySelectorAll(selector).length;
|
||||
}
|
||||
console.log(count("p")); // All <p> elements
|
||||
// → 4
|
||||
console.log(count(".animal")); // Class animal
|
||||
// → 2
|
||||
console.log(count("p .animal")); // Animal inside of <p>
|
||||
// → 2
|
||||
console.log(count("p > .animal")); // Direct child of <p>
|
||||
// → 1
|
||||
</script>
|
||||
```
|
||||
|
||||
与getElementsByTagName这类方法不同,由querySelectorAll返回的对象不是动态变更的。修改文档时其内容不会被修改。
|
||||
|
||||
querySelector方法(没有All)与querySelectorAll作用相似。如果只想寻找某一个特殊元素,该方法非常有用。该方法只返回第一个匹配元素,如果不存在则返回null。
|
||||
|
||||
### 13.13 位置与动画
|
||||
|
||||
position样式属性是一种强大的布局方法。默认情况下,该属性值为static,表示元素处于文档中的默认位置。若该属性设置为relative,该元素在文档中依然占据空间,但此时其top和left样式属性则是相对于默认位置的偏移。若position设置为absolute,会将元素从默认文档流中移除,该元素将不再占据空间,而会与其他元素重叠。其top和left属性则是相对其最近的封闭元素的偏移,其中position属性的值不是static。如果没有任何封闭元素存在,则是相对于整个文档的偏移。
|
||||
|
||||
我们可以使用该属性创建一个动画。下面的文档用于显示一幅猫的图片,该图片会沿着椭圆轨迹移动。
|
||||
|
||||
```html
|
||||
<p style="text-align: center">
|
||||
<img src="img/cat.png" style="position: relative">
|
||||
</p>
|
||||
<script>
|
||||
var cat = document.querySelector("img");
|
||||
var angle = 0, lastTime = null;
|
||||
function animate(time) {
|
||||
if (lastTime != null)
|
||||
angle += (time - lastTime) * 0.001;
|
||||
lastTime = time;
|
||||
cat.style.top = (Math.sin(angle) * 20) + "px";
|
||||
cat.style.left = (Math.cos(angle) * 200) + "px";
|
||||
requestAnimationFrame(animate);
|
||||
}
|
||||
requestAnimationFrame(animate);
|
||||
</script>
|
||||
```
|
||||
|
||||
|
||||
该图像在页面中央,position为relative。为了移动这只猫,我们需要不断更新图像的top和left样式。
|
||||
|
||||
脚本使用request-AnimationFrame在每次浏览器准备重绘屏幕时调用animate函数。animate函数再次调用requestAnimationFrame以准备下一次更新。当浏览器窗口(或标签)激活时,更新频率大概为60次每秒,这种频率可以生成美观的动画。
|
||||
|
||||
若我们只是在循环中更新DOM,页面会静止不动,页面上也不会显示任何东西。浏览器不会在执行JavaScript程序时刷新显示内容,也不允许页面上的任何交互。这就是我们需要requestAnimationFrame的原因,该函数用于告知浏览器JavaScript程序目前已经完成工作,因此浏览器可以继续执行其他任务,比如刷新屏幕,响应用户动作。
|
||||
|
||||
我们将动画生成函数作为参数传递给requestAnimationFrame。该函数比较现在的时间和上一次时间(lastTime变量),确保每一毫秒猫的移动是稳定的,而且动画是圆滑的。如果仅仅每次走几步,猫的动作可能略显迟钝,例如,另一个在相同电脑上的繁重任务可能使得该函数零点几秒之后才会运行一次。
|
||||
|
||||
我们使用三角函数Math.cos和Math.sin来使猫沿着圆弧移动。你可能不太熟悉这些计算,这些计算是本书第一次提及,因此我在这里对这几个计算进行一个大致的介绍。
|
||||
|
||||
Math.cos和Math.sin非常实用,我们可以利用一个1单元的弧度,计算出以点(0,0)为圆心的圆上特定点的位置。两个函数都将参数解释为圆上的一个位置,0表示圆上最右侧那个点,一直逆时针递增到2π(大概是6.28),刚刚走过整个圆。Math.cos可以计算出圆上某一点对应的x坐标,而Math.sin则计算出y坐标。超过2π或小于0的位置(或角度)都是合法的。因为弧度是循环重复的,a+2π与a的角度相同。
|
||||
|
||||

|
||||
|
||||
猫的动画代码保存了一个名为angle的计数器,该变量记录猫在圆上的角度,而且每当调用animate函数时,该计数器的值与流逝的时间成比例递增。我们接着使用这个角度来计算图像元素的当前位置。top样式是Math.sin的结果乘以20,表示圆中的垂直弧度。left样式是Math.cos的结果与200的乘积,因此圆的宽度大于其高度,导致最后猫会沿着椭圆轨迹移动。
|
||||
|
||||
这里需要注意的是样式的值一般需要指定单位。本例中,我们在数字后添加“px”来告知浏览器以像素为计算单位(而非厘米,“ems”,或其他单位)。我们很容易遗漏这个单位。如果我们没有为样式中的数字加上单位,浏览器最后会忽略掉该样式,除非数字是0,在这种情况下使用什么单位,其结果都是一样的。
|
||||
|
||||
### 13.14 本章小结
|
||||
|
||||
JavaScript程序可以通过名为DOM的数据结构查看并修改浏览器中显示的文档。该数据结构描述了浏览器文档模型,而JavaScript程序可以通过修改该数据结构来修改浏览器展示的文档。
|
||||
|
||||
DOM的组织就像树一样,DOM根据文档结构来层次化地排布元素。描述元素的对象包含很多属性,比如parentNode和childNodes这两个属性可以用来遍历DOM树。
|
||||
|
||||
我们可以通过样式来改变文档的显示方式,可以直接在节点上附上样式,也可以编写匹配节点的规则。样式包含许多不同的属性,比如color和display。JavaScript可以直接通过节点的style属性操作元素的样式。
|
||||
|
||||
### 13.15 习题
|
||||
|
||||
#### 13.15.1 创建一张表
|
||||
|
||||
我们在第6章中使用纯文本来构建表格。HTML使得表格排版更加容易。我们可以使用下面的标签结构来构建HTML表格。
|
||||
|
||||
```html
|
||||
<table>
|
||||
<tr>
|
||||
<th>name</th>
|
||||
<th>height</th>
|
||||
<th>country</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Kilimanjaro</td>
|
||||
<td>5895</td>
|
||||
<td>Tanzania</td>
|
||||
</tr>
|
||||
</table>
|
||||
```
|
||||
|
||||
<table>标签中,每一行包含一个<tr>标签。<tr>标签内部则是单元格元素,分为表头(<th>)和常规单元格(<td>)。
|
||||
|
||||
我们这里使用第6章中已经使用过的源数据,源数据存储在沙箱的MOUNTAINS变量中。你也可以从网站上下载([http://eloquentjavascript.net/code/](http://eloquentjavascript.net/code/))数据。
|
||||
|
||||
编写一个函数buildTable,调用者指定一个对象数组,数组中每个对象都包含相同的一组属性,该函数根据数组构建出表示表格的DOM结构。表格应该以属性名称作为表头,表头使用<th>元素包围,每一行代表数组中的一个对象,其属性值存放在<td>元素中。
|
||||
|
||||
Object.keys函数返回某个对象所有属性名称的数组,读者可能会在程序中用到。
|
||||
|
||||
当你完成基本功能后,将元素的style.textAlign属性设置为right,将单元格中的数字右对齐。
|
||||
|
||||
```html
|
||||
<style>
|
||||
/* Defines a cleaner look for tables */
|
||||
table { border-collapse: collapse; }
|
||||
td, th { border: 1px solid black; padding: 3px 8px; }
|
||||
th { text-align: left; }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
function buildTable(data) {
|
||||
// Your code here.
|
||||
}
|
||||
|
||||
document.body.appendChild(buildTable(MOUNTAINS));
|
||||
</script>
|
||||
```
|
||||
|
||||
#### 13.15.2 通过标签名获取元素
|
||||
|
||||
方法getElementsByTagName返回带有特定标签名称的所有子元素。实现该函数,这里注意是函数不是方法。该函数的参数是一个节点和字符串(标签名称),并返回一个数组,该数组包含所有带有特定标签名称的所有后代元素节点。
|
||||
|
||||
你可以使用tagName属性从DOM元素中获取标签名称。但这里需要注意,使用tagName获取的标签名称是全大写形式。可以使用字符串的toLowerCase或toUpperCase来解决这个问题。
|
||||
|
||||
```html
|
||||
<h1>Heading with a <span>span</span> element.</h1>
|
||||
<p>A paragraph with <span>one</span>, <span>two</span>
|
||||
spans.</p>
|
||||
|
||||
<script>
|
||||
function byTagName(node, tagName) {
|
||||
// Your code here.
|
||||
}
|
||||
|
||||
console.log(byTagName(document.body, "h1").length);
|
||||
// → 1
|
||||
console.log(byTagName(document.body, "span").length);
|
||||
// → 3
|
||||
var para = document.querySelector("p");
|
||||
console.log(byTagName(para, "span").length);
|
||||
// → 2
|
||||
</script>
|
||||
```
|
||||
|
||||
#### 13.15.3 猫的帽子
|
||||
|
||||
扩展一下之前定义的用来绘制猫的动画函数,让猫和它的帽子沿着椭圆形轨道边(帽子永远在猫的对面)移动。
|
||||
|
||||
你也可以尝试让帽子环绕着猫移动,或修改成其他有趣的动画。
|
||||
|
||||
为了便于定位多个对象,一个比较好的方法是使用绝对(absolute)定位。这就意味着top和left属性是相对于文档左上角的坐标。你可以简单地在坐标上加上一个固定数字,以避免出现负的坐标。
|
||||
|
||||
```html
|
||||
<img src="img/cat.png" id="cat" style="position: absolute">
|
||||
<img src="img/hat.png" id="hat" style="position: absolute">
|
||||
|
||||
<script>
|
||||
var cat = document.querySelector("#cat");
|
||||
var hat = document.querySelector("#hat");
|
||||
// Your code here.
|
||||
</script>
|
||||
```
|
Loading…
x
Reference in New Issue
Block a user