1. Document Structure
再看上一章的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>
这个HTML文档的结构
如下:
document
在js代码中,document是一个全局变量,它代表了整个HTML文档。 document.documentElement 就是 <html> 这个tag对应的对象。 document.head, document.body 分别对应<head> 和 <body> 对象。
2. Trees
还记得第11章的语法树吗?那么多递归访问,把我转的晕头转向。DOM也是这样的语法树。树根是 document.documentElement。
上图中每一个方框都是node。node有多种类型(nodeType),每种类型在js中都有一个对应的常量值,常见的三种类型是:
1. regular elements:
document.ELEMENT_NODE: 1
上图中的灰色方框
2. text nodes:
document.TEXT_NODE: 3
上图中的淡蓝色方框
3. comments
document.COMMENT_NODE: 8
<!-- 这是注释 -->
3. The Standard
DOM 不是专门为js设计的,甚至都不是专门为HTML设计的,它是一种语言中立的接口,或者说它是为XML设计的接口。而XML是一种更宽泛领域的超语言,HTML只是它的一个小应用。所以,后面会看到,我们在使用DOM操纵HTML时,经常会感觉有些别扭。例如,每一个element 都有childNodes属性,通过该属性可以访问它的所有子节点,而childNodes不是Array类型,它是NodeList类型,它没有slice和forEach方法。
如果操作DOM的行为比较多,代码会非常的冗长而难看。还好,已经有写第三方库提供了简单的方法,例如 jQuery。
4. Moving through the tree
上图描述了一个DOM结构,以及访问各个node的方法:
childNodes, parentNode, firstChild, previousSibling, nextSibling, lastChild
举个例子,在HTML中查找是否存在 "book" 这个单词:
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
5. Finding Elements
三个函数:
1. getElementsByTagName()
var link = document.body.getElementsByTagName("a")[0];
console.log(link.href);
2. getElementById()
<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>
3. getElementsByClassName()
这个函数和getElementsByTagName()类似。 <p class="big"> ... </p>
6. Changing the Document
functions:
removeChild()
appendChild()
insertBefore()
replaceChild()
例子:
<p>One</p>
<p>Two</p>
<p>Three</p>
<script>
var paragraphs = document.body.getElementsByTagName("p");
document.body.insertBefore(paragraphs[2], paragraphs[0]);
</script>
一个node只能存在于一个位置,所以,把第2个p插入第0个p时,会先把它从原来的位置删除。
注意,replaceChild() 和 insertBefore() 的第一个参数是新插入的节点,第二个节点是参考位置的节点。7. Creating Nodes
先看个例子:
把<img> 节点替换成Text节点,Text的内容是 <img> 的alt属性的值。
<p>The <img src="img/cat.png" alt="Cat"> in the
<img src="img/hat.png" alt="Hat">.</p>
<p><button οnclick="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>
用 document.createTextNode() 创建Text节点。
注意,getElementsByTagName()、getElementsByClassName() 和 childNodes 等得到的节点列表是 live 的,也就是说,当DOM结构发生变化之后,通过这些方法的node列表(上面的var images)也会随之变化。
所以,上面代码遍历images时,要从后往前遍历。
可以把live的节点列表变成solid(固定的):
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
第十一章也用过类似的方法:把一个 “像” 数组的对象转换成数组,因为它们的数据存储方式相同。
再看一个例子:
<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>
使用 document.createElement(tag) 创建普通的节点。
8. Attributes
8.1. 有些节点的属性,在DOM中有相同名字的属性。
例如: <a id="mylink" href="..."> 中的href。
var mylink = document.getElementById("mylink");
mylink.href="http://blog.csdn.net";
8.2. 有些节点属性在DOM中没有对应的属性
我们自己定义的属性也没有,所以,需要用 getAttribute 和 setAttribute 来访问:
<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-前缀,以避免命名冲突。
8.3. 再看一个比较复杂的例子
查找 <pre> 中代码的关键字,把它们用粗体显示 (<strong>)
<p>Here it is, the identity function:</p>
<pre data-language="javascript">
function id(x) { return x; }
</pre>
<script>
function highlightCode(node, keywords) {
var text = node.textContent;
node.textContent = ""; // Clear the node
var match, pos = 0;
while (match = keywords.exec(text)) { // regex 中有 /g,全局匹配,那么,有多少次匹配,while循环就会执行多少次。
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));
}
var languages = {
javascript: /\b(function|return|var)\b/g
};
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]);
}
}
</script>
上面代码中,attribute的访问很容易理解,反倒是RegExp的使用方法有些费解。仔细看第11行和第17行的注释。
8.4. node的class属性
例如: <p class="big"> ... </p>
在DOM中class对应的属性是className,我们也可以用 getAttribute("class") 、setAttribute("class") 来访问它。
9. Layout
9.1. 类型
block:另起一行
<p>, <h1>, <div> 等
inline:不会另起一行
<a>, <strong>, <span> 等
9.2. size
offsetWith, offsetHeight:带border的宽和高
clientWith, clientHeight: 不带border的宽和高
9.3. 例子
<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>
9.4.
获取一个element的精确位置
getBoundingClientRect( )
9.5. 浏览器的滚动条滚动之后的坐标pageXOffset, pageYOffset
9.6. 重新计算layout
改动DOM内容,读取position和size属性,或则调用getBoundingClientRect( ) 都需要重新计算layout,如果操作过于频繁(例如:在循环中),会导致性能很差,网页响应慢。
看个例子:
<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>
10. Styling
10.1. style 属性:
<p><a href=".">Normal link</a></p>
<p><a href="." style="color: green">Green link</a></p>
10.2. display 类型:
display:block; 自己占一行
display:inline; 自己不单独占一行
display:none; 隐藏 (不改变DOM结构)
例如:
This text is displayed <strong>inline</strong>,
<strong style="display: block">as a block</strong>, and
<strong style="display: none">not at all</strong>.
10.3.
修改style
<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>
中间带连接线的style,有两种访问方式:
style["font-family"]
style.fontFamily
11. Cascading Styles
11.1. cascading
多处设置的style合并到一起(三种地方:.css 文件, <head> 中的style段,elements的style属性)
element的style属性中的设置,优先级最高11.2. selector
tag
p {
font-size: 16px;
}
class
.subtle {
color: gray;
}
id
#header {
background: blue;
}
匹配的越精确,优先级越高。下面的p.a.b#main 就比上面的 p 优先级高。
/* p elements, with classes a and b, and id main */
p.a.b#main {
font-size: 20px;
}
p > a { ... }
<p> 的第一级 <a> 类子节点
p a { ... }
<p> 下面所有级别的 <a> 类节点
这些是最常用的selector,还有一些更复杂的,不再一一列出。
12. Query Selectors
上面讲了用 tag、class 和 id 获取elements,也可以通过selector获取elements。
querySelectorAll(sel),
获取所有符合selector的elements,document.querySelectorAll(), element.querySelectorAll()
querySelector(sel)
返回第一个符合条件的element
这两个方法返回的elements不是live的,不会随着DOM的变化而变化
例:
<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>
13. Positioning and Animating
style的position属性有三种类型:
static
默认。从上到下,从左到右,在浏览器中流式布局。
relative
top和left 相对于它的默认 (position:static时) 的位置
absolute
top和left 相对于父节点的坐标。父节点的position不能是static。如果父节点是static,则向上层找父节点。如果找不到符合条件的父节点,则相对于document。
例子: 动态修改positin: relative 的top和left
<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>
一只猫沿着椭圆轨迹平移。
requestAnimationFrame(func) 这个函数告诉browser,下次刷新屏幕时,执行参数中的函数。浏览器大约每秒刷新60次屏幕。
当然,我们也可以使用 setTimeout() 或 setInterval() 来定时移动,但,requestAnimationFrame() 可以获得更平滑的效果。
注意,坐标后面一定要加上单位(px, em等),否则,设置无效。
14. Exercise: Build a Table
<script>
function buildTable(data) {
var table = document.createElement("table");
var header = document.createElement("tr");
table.appendChild(header);
var keys = Object.keys(data[0]);
keys.forEach(function (name) {
var th = document.createElement("th");
th.textContent = name;
header.appendChild(th);
});
data.forEach(function (row) {
var tr = document.createElement("tr");
table.appendChild(tr);
keys.forEach(function (name) {
var value = row[name];
var td = document.createElement("td");
td.textContent = value;
if (name == "height") {
td.className = "number";
}
tr.appendChild(td);
});
});
return table;
}
document.body.appendChild(buildTable(MOUNTAINS));
</script>
比在控制台打印table简单多了,因为,不再需要考虑布局问题。
15. Exercise: Elements by Tag Name
function byTagName(node, tagName) {
var nodes = [];
if (node.nodeType == document.ELEMENT_NODE) {
if (node.tagName.toLowerCase() == tagName.toLowerCase()) {
nodes.push(node);
}
for (var i = 0; i < node.childNodes.length; i++) {
var eles = byTagName(node.childNodes[i], tagName);
nodes = nodes.concat(eles);
}
}
return nodes;
}