0%

学习笔记 2020 11 14

学习笔记 2020-11-14

JavaScript 高级程序设计(第4版) 阅读记录

DOM2 和 DOM3

样式

元素尺寸
  1. 滚动尺寸

    滚动尺寸提供了元素内容滚动距离的信息。有些元素,比如 <html> 无须任何代码就可以自动滚动,而其他元素需要使用 CSS 的 overflow 属性令其滚动。滚动尺寸相关属性有 4 个:

    • scrollHeight,没有滚动条出现时,元素内容的总高度。
    • scrollLeft,内容区左侧隐藏的像素数,设置这个属性可以改变元素的滚动位置。
    • scrollTop,内容区顶部隐藏的像素数,设置这个属性可以改变元素的滚动位置。
    • scrollWidth,没有滚动条出现时,元素内容的总宽度。

    image-20201114085706671

    scrollWidth 和 scrollHeight 可以用来确定给定元素内容的实际尺寸。

    scrollWidth 和 scrollHeight 与 clientWidth 和 clientHeight 之间的关系在不需要滚动的文档上是分不清的。如果文档尺寸超过视口尺寸,scrollWidth 和 scrollHeight 表示文档内容的宽高,clientWidth 和 clientHeight 等于视口宽高。

  2. 确定元素尺寸

    浏览器在每个元素上都暴露了 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 教程

任务

  1. 函数会选择最新的内容码?

    函数 sayHi 使用外部变量。当函数运行时,将使用哪个值?

    let name = "John";
    
    function sayHi() {
      alert("Hi, " + name);
    }
    
    name = "Pete";
    
    sayHi(); // 会显示什么:"John" 还是 "Pete"?

    这种情况在浏览器和服务器端开发中都很常见。一个函数可能被计划在创建之后一段时间后才执行,例如在用户行为或网络请求之后。

    因此,问题是:它会接收最新的修改吗?

    会。

  2. 哪些变量可用呢?

    下面的 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”。

  3. Counter 是独立的吗?

    在这儿我们用相同的 makeCounter 函数创建了两个计数器(counters):countercounter2

    它们是独立的吗?第二个 counter 会显示什么?0,12,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 。

  4. 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。

  5. if 内的函数

    看看下面这个代码。最后一行代码的执行结果是什么?

    let phrase = "Hello";
    
    if (true) {
      let user = "John";
    
      function sayHi() {
        alert(`${phrase}, ${user}`);
      }
    }
    
    sayHi();

    报错。

  6. 闭包 sum

    编写一个像 sum(a)(b) = a+b 这样工作的 sum 函数。

    是的,就是这种通过双括号的方式(并不是错误)。

    举个例子:

    sum(1)(2) = 3
    sum(5)(-1) = 4
    function sum(x) {
      return function (y) {
        return x + y;
      };
    }
  7. 变量可见吗

    下面这段代码的结果会是什么?

    let x = 1;
    
    function func() {
      console.log(x); // ?
    
      let x = 2;
    }
    
    func();

    P.S. 这个任务有一个陷阱。解决方案并不明显。

    报错。因为函数内部包含 x 。虽然还未定义,但已经不会去外部寻找 x 。

  8. 通过函数筛选

    我们有一个内建的数组方法 arr.filter(f)。它通过函数 f 过滤元素。如果它返回 true,那么该元素会被返回到结果数组中。

    制造一系列“即用型”过滤器:

    • inBetween(a, b) —— 在 ab 之间或与它们相等(包括)。
    • 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;
      };
    }
  9. 按字段排序

    我们有一组要排序的对象:

    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);
    }
  10. 函数大军

    下列的代码创建了一个 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;
    }