学习笔记 2020-11-14
JavaScript 高级程序设计(第4版) 阅读记录
DOM2 和 DOM3
样式
元素尺寸
滚动尺寸
滚动尺寸提供了元素内容滚动距离的信息。有些元素,比如
<html>
无须任何代码就可以自动滚动,而其他元素需要使用 CSS 的 overflow 属性令其滚动。滚动尺寸相关属性有 4 个:- scrollHeight,没有滚动条出现时,元素内容的总高度。
- scrollLeft,内容区左侧隐藏的像素数,设置这个属性可以改变元素的滚动位置。
- scrollTop,内容区顶部隐藏的像素数,设置这个属性可以改变元素的滚动位置。
- scrollWidth,没有滚动条出现时,元素内容的总宽度。
scrollWidth 和 scrollHeight 可以用来确定给定元素内容的实际尺寸。
scrollWidth 和 scrollHeight 与 clientWidth 和 clientHeight 之间的关系在不需要滚动的文档上是分不清的。如果文档尺寸超过视口尺寸,scrollWidth 和 scrollHeight 表示文档内容的宽高,clientWidth 和 clientHeight 等于视口宽高。
确定元素尺寸
浏览器在每个元素上都暴露了 getBoundingClientRect() 方法,返回一个 DOMRect 对象,包含 6 个属性: left、top、right、bottom、height 和 width 。这些属性给出了元素在页面中相对于视口的位置。
遍历
DOM2 Traversal and Range 模块定义了两个类型用于辅助顺序遍历 DOM 结构。这两个类型—— NodeIterator 和 TreeWalker ——从某个起点开始执行对 DOM 结构的深度优先遍历。
NodeIterator
NodeIterator 可以通过 document.createNodeIterator() 方法创建其实例。接收 4 个参数:
- root,作为遍历根节点的节点。
- whatToShow,数值代码,表示应该访问哪些节点。
- filter,NodeFilter 对象或函数,表示是否接收或跳过特定节点。
- entityReferenceExpansion,布尔值,表示是否扩展实体引用。这个参数在 HTML 文档中没有效果,因为实体引用永不扩展。
whatToShow 参数是一个位掩码,通过应用一个或多个过滤器来指定访问哪些节点。这个参数对应的常量是在 NodeFilter 类型中定义的。
- NodeFilter.SHOW_ALL,所有节点。
- NodeFilter.SHOW_ELEMENT,元素节点。
- NodeFilter.SHOW_ATTRIBUTE,属性节点。由于 DOM 的结构,因此实际上用不上。
- NodeFilter.SHOW_TEXT,文本节点。
- NodeFilter.SHOW_CDATA_SECTION,CData 区块节点。不是在 HTML 页面中使用的。
- NodeFilter.SHOW_ENTITY_REFERENCE,实体引用节点。不是在 HTML 页面中使用的。
- NodeFilter.SHOW_ENTITY,实体节点。不是在 HTML 页面中使用的。
- NodeFilter.SHOW_PROCESSING_INSTRUCTION,处理指令节点。不是在 HTML 页面中使用的。
- NodeFilter.SHOW_COMMENT,注释节点。
- NodeFilter.SHOW_DOCUMENT,文档节点。
- NodeFilter.SHOW_DOCUMENT_TYPE,文档类型节点。
- NodeFilter.SHOW_DOCUMENT_FRAGMENT,文档片段节点。不是在 HTML 页面中使用的。
- NodeFilter.SHOW_NOTATION,记号节点。不是在 HTML 页面中使用的。
除了第一个,其余的可以组合使用,按位或来组合多个选项。
filter 参数可以用来指定自定义 NodeFilter 对象,或者一个作为节点过滤器的函数。NodeFilter 对象只有一个方法 acceptNode() ,如果给定节点应该访问就返回 NodeFilter.FILTER_ACCEPT,否则返回 NodeFilter.FILTER_SKIP。因为 NodeFilter 是一个抽象类型,所以不可能创建它的实例。只要创建一个包含 acceptNode() 的对象,然后把它传给createNodeIterator() 就可以了。
// 只接收 p 元素的节点过滤器对象
let filter = {
acceptNode(node) {
return node.tagName.toLowerCase() == 'p'
? NodeFilter.FILTER_ACCEPT
: NodeFilter.FILTER_SKIP;
}
};
// 或是以下形式
let filter = function (node) {
return node.tagName.toLowerCase() == 'p'
? NodeFilter.FILTER_ACCEPT
: NodeFilter.FILTER_SKIP;
};
let iterator = document.createNodeIterator(
root,
NodeFilter.SHOW_ELEMENT,
filter,
false
);
NodeIterator 的两个主要方法是:
- nextNode(),在 DOM 子树中以深度优先方式进前一步。
- previousNode(),后退一步。
TreeWalker
TreeWalker 比 NodeIterator 多了以下方法:
- parentNode()
- firstChild()
- lastChild()
- nextSibling()
- previousSibling()
调用 document.createTreeWalker() 创建 TreeWalker 对象。接收类似的参数。
不同的是,filter 除了可以返回原本的两种值,还可以返回 NodeFilter.FILTER_REJECT。
在这个对象中,NodeFilter.FILTER_SKIP 表示跳过节点,访问子树的下一个节点,而 NodeFilter.FILTER_REJECT 表示跳过该节点以及该节点的整个子树。
TreeWalker 类型也有一个名为 currentNode 的属性,表示遍历过程中上一次返回的节点。
范围
DOM2 Traversal and Range 模块定义了范围接口。
DOM 范围
使用 document.createRange() 方法创建 DOM 范围对象。新建的范围对象与创建它的文档关联的,不能在其他文档中使用。
每个范围都是 Range 类型的实例,拥有相应的属性和方法。
- startContainer,范围起点所在的节点 ( 选区中第一个子节点的父节点 )
- startOffset,范围起点在 startContainer 中的偏移量。
- endContainer,范围终点所在的节点 ( 选区中最后一个子节点的父节点 )
- endOffset,范围起点在 startContainer 中的偏移量。
- commonAncestorContainer,文档中以 startContainer 和 endContainer 为后代的最深的节点。
这些属性会在范围被放到文档中特定位置时获得相应的值。
简单选择
使用 selectNode() 或 selectNodeContents() 方法范围选择文档中某个部分。接收一个节点作为参数。selectNode() 方法选择整个节点,包括其后代节点,而 selectNodeContents() 只选择节点的后代。
<!DOCTYPE html>
<html>
<body>
<p id="p1"><b>Hello</b> world!</p>
</body>
</html>
let range1 = document.createRange(),
range2 = document.createRange(),
p1 = document.getElementById('p1');
range1.selectNode(p1);
range2.selectNodeContents(p1);
console.log(range1);
console.log(range2);
// Range: {
// collapsed: false
// commonAncestorContainer: body
// endContainer: body
// endOffset: 2
// startContainer: body
// startOffset: 1
// __proto__: Range
// }
// Range: {
// collapsed: false
// commonAncestorContainer: p#p1
// endContainer: p#p1
// endOffset: 2
// startContainer: p#p1
// startOffset: 0
// __proto__: Range
// }
还可以在范围上调用方法:
setStartBefore(refNode),把范围的起点设置到 refNode 之前,从而让 refNode 成为选
区的第一个子节点。startContainer 属性被设置为 refNode.parentNode,而 startOffset 属性被设置为 refNode 在其父节点 childNodes 集合中的索引。
setStartAfter(refNode),把范围的起点设置到 refNode 之后,从而将 refNode 排除在选
区之外,让其下一个同胞节点成为选区的第一个子节点。startContainer 属性被设置为
refNode.parentNode,startOffset 属性被设置为 refNode 在其父节点 childNodes 集合
中的索引加 1。
setEndBefore(refNode),把范围的终点设置到 refNode 之前,从而将 refNode 排除在选区之外、让其上一个同胞节点成为选区的最后一个子节点。endContainer 属性被设置为 refNode. parentNode,endOffset 属性被设置为 refNode 在其父节点 childNodes 集合中的索引。
setEndAfter(refNode),把范围的终点设置到 refNode 之后,从而让 refNode 成为选区的
最后一个子节点。endContainer 属性被设置为 refNode.parentNode,endOffset 属性被设置为 refNode 在其父节点 childNodes 集合中的索引加 1。
复杂选择
使用 setStart() 和 setEnd() 方法,接收两个参数:参照节点和偏移量。对 setStart() 来说,参照节点会成为 startContainer,而偏移量会赋值给 startOffset。 对 setEnd() 而言,参照节点会成为 endContainer,而偏移量会赋值给 endOffset。
现代 JavaScript 教程
任务
函数会选择最新的内容码?
函数 sayHi 使用外部变量。当函数运行时,将使用哪个值?
let name = "John"; function sayHi() { alert("Hi, " + name); } name = "Pete"; sayHi(); // 会显示什么:"John" 还是 "Pete"?
这种情况在浏览器和服务器端开发中都很常见。一个函数可能被计划在创建之后一段时间后才执行,例如在用户行为或网络请求之后。
因此,问题是:它会接收最新的修改吗?
会。
哪些变量可用呢?
下面的
makeWorker
函数创建了另一个函数并返回该函数。可以在其他地方调用这个新函数。它是否可以从它被创建的位置或调用位置(或两者)访问外部变量?
function makeWorker() { let name = "Pete"; return function() { alert(name); }; } let name = "John"; // create a function let work = makeWorker(); // call it work(); // 会显示什么?
会显示哪个值?“Pete” 还是 “John”?
“Pete”。
Counter 是独立的吗?
在这儿我们用相同的
makeCounter
函数创建了两个计数器(counters):counter
和counter2
。它们是独立的吗?第二个 counter 会显示什么?
0,1
或2,3
还是其他?function makeCounter() { let count = 0; return function() { return count++; }; } let counter = makeCounter(); let counter2 = makeCounter(); alert( counter() ); // 0 alert( counter() ); // 1 alert( counter2() ); // ? alert( counter2() ); // ?
独立的,第二个会显示 0 1 。
Counter 对象
这里通过构造函数创建了一个 counter 对象。
它能正常工作吗?它会显示什么呢?
function Counter() { let count = 0; this.up = function() { return ++count; }; this.down = function() { return --count; }; } let counter = new Counter(); alert( counter.up() ); // ? alert( counter.up() ); // ? alert( counter.down() ); // ?
能。1 2 1。
if 内的函数
看看下面这个代码。最后一行代码的执行结果是什么?
let phrase = "Hello"; if (true) { let user = "John"; function sayHi() { alert(`${phrase}, ${user}`); } } sayHi();
报错。
闭包 sum
编写一个像
sum(a)(b) = a+b
这样工作的sum
函数。是的,就是这种通过双括号的方式(并不是错误)。
举个例子:
sum(1)(2) = 3 sum(5)(-1) = 4
function sum(x) { return function (y) { return x + y; }; }
变量可见吗
下面这段代码的结果会是什么?
let x = 1; function func() { console.log(x); // ? let x = 2; } func();
P.S. 这个任务有一个陷阱。解决方案并不明显。
报错。因为函数内部包含 x 。虽然还未定义,但已经不会去外部寻找 x 。
通过函数筛选
我们有一个内建的数组方法
arr.filter(f)
。它通过函数f
过滤元素。如果它返回true
,那么该元素会被返回到结果数组中。制造一系列“即用型”过滤器:
inBetween(a, b)
—— 在a
和b
之间或与它们相等(包括)。inArray([...])
—— 包含在给定的数组中。
用法如下所示:
arr.filter(inBetween(3,6))
—— 只挑选范围在 3 到 6 的值。arr.filter(inArray([1,2,3]))
—— 只挑选与[1,2,3]
中的元素匹配的元素。
例如:
/* .. inBetween 和 inArray 的代码 */ let arr = [1, 2, 3, 4, 5, 6, 7]; alert( arr.filter(inBetween(3, 6)) ); // 3,4,5,6 alert( arr.filter(inArray([1, 2, 10])) ); // 1,2
function inBetween(a, b) { return function (item) { return item >= a && item <= b; }; } function inArray(arr) { return function (item) { return arr.indexOf(item) !== -1; }; }
按字段排序
我们有一组要排序的对象:
let users = [ { name: "John", age: 20, surname: "Johnson" }, { name: "Pete", age: 18, surname: "Peterson" }, { name: "Ann", age: 19, surname: "Hathaway" } ];
通常的做法应该是这样的:
// 通过 name (Ann, John, Pete) users.sort((a, b) => a.name > b.name ? 1 : -1); // 通过 age (Pete, Ann, John) users.sort((a, b) => a.age > b.age ? 1 : -1);
我们可以让它更加简洁吗,比如这样?
users.sort(byField('name')); users.sort(byField('age'));
这样我们就只需要写
byField(fieldName)
,而不是写一个函数。编写函数
byField
来实现这个需求。function byField(fieldName) { return (a, b) => (a[fieldName] > b[fieldName] ? 1 : -1); }
函数大军
下列的代码创建了一个
shooters
数组。每个函数都应该输出其编号。但好像出了点问题……
function makeArmy() { let shooters = []; let i = 0; while (i < 10) { let shooter = function() { // 创建一个 shooter 函数, alert( i ); // 应该显示其编号 }; shooters.push(shooter); // 将此 shooter 函数添加到数组中 i++; } // ……返回 shooters 数组 return shooters; } let army = makeArmy(); // ……所有的 shooter 显示的都是 10,而不是它们的编号 0, 1, 2, 3... army[0](); // 编号为 0 的 shooter 显示的是 10 army[1](); // 编号为 1 的 shooter 显示的是 10 army[2](); // 10,其他的也是这样。
为什么所有的 shooter 显示的都是同样的值?
修改代码以使得代码能够按照我们预期的那样工作。
function makeArmy() { let shooters = []; let i = 0; while (i < 10) { let j = i; let shooter = function () { // 创建一个 shooter 函数, console.log(j); // 应该显示其编号 }; shooters.push(shooter); // 将此 shooter 函数添加到数组中 i++; } // ……返回 shooters 数组 return shooters; }