JavaScript 忍者秘籍

Part 1 Warming Up

Chapter 1 JavaScript is everywhere

  • JavaScript 特色

    函数对象 (Functions are first-class objects)
    作用域 (Scopes)
    闭包 (Function closures)
    异步 (Promises)
    生成器 (Generators)
    原型链 (Prototype-based object orientation)
    代理 (Proxies)
    数组 (Advanced array methods)
    字典 (Maps)
    正则表达式 (Regular expressions)
    模块 (Modules)

  • transpilers (transformation + compiling) 新特性兼容工具

    Traceur

    Babel

  • 代码技能点

    Debugging skills
    Testing
    Performance analysis

  • console.time() & console.timeEnd()

    1
    2
    3
    console.time('test');
    let res = [].map(item => item); // 待计时代码,建议循环多次计算代码,以取平均
    console.timeEnd('test');

    输出: test: 0.013916015625ms

Chapter 2 Building the page at runtime

两个阶段: Page building & Event handling

Page building

以 HTML 为蓝本, 构建 DOM 树 (Document Object Model);

当遇到 <script> 标签时, 暂停 DOM 树的构建, 转为编译 JavaScript 代码.

  • HTML 规范

  • DOM 规范

  • Web API

  • window — 全局对象
    document — window 对象下的一个属性, 代表 DOM 树

  • 全局代码直接按行执行, 函数内部代码则在函数被调用时执行

Event handling

  • 事件队列 (Event queue)

    一方面: 用户行为, 服务器响应等事件从尾部加入事件队列.
    另一方面: 浏览器循环查看事件队列的头部, 若有事件, 则取出一个执行 (单线程 single-threaded), 执行完再次查看队列新头部; 若无事件, 则直接再次查看队列头部.

  • 事件绑定 (Event-handler registration)

    1. 将函数绑定至特定属性 (不推荐)
      window.onload = function () {} — 只可绑定一个执行函数, 存在覆盖已有函数的问题

    2. 通过 addEventListener (推荐)
      任何 DOM 对象均可

Part 2 Understanding functions

Chapter 3 First-class functions for the novice: definitions and arguments

JavaScript is a functional language

函数与对象 (Object) 的使用方式一致, 如对函数增加属性:

  1. 增加 id 作为唯一标识,以实现存储唯一的函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    const store = {
    nextId: 1,
    cache: {},
    add: function(fn) {
    if(!fn.id) {
    fn.id = this.nextId++;
    this.cache[fn.id] = fn;
    return true;
    }
    }
    }
    store.add(function() {});

    对于要存入的函数, 判断是否存在 id 属性, 不存在则为新函数, 加 id 并存入 cache; 存在则不支持重复存入.

  2. 增加 result, 存储计算结果, 避免重复计算

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    function isPrime(value) {
    if (!isPrime.answers) isPrime.answers = {}; // 初始化 cache
    if (isPrime.answers[value]) return isPrime.answers[value]; // 已经计算过的结果直接返回
    let prime = value !== 1;
    for (let i = 2; i < value; i++) {
    if (value % i === 0) {
    prime = false;
    break;
    }
    }
    return isPrime.answers[value] = prime;
    }
    console.log(isPrime(5)); // true
    console.log(isPrime.answers[5]); // true

    判断素数的函数, 每次判断都记录结果, 再次请求已计算结果, 则直接从存储中返回, 减少遍历.

进阶参考: Functional Programming in JavaScript

定义函数

  • 函数申明与函数表达式

    区别在于: 函数申明中的函数名是必须的,而表达式中是非必须的.

    • IIFE (immediately invoked function expression, immediate function)
      (function () { ... })() — 可模拟js的模块化
      通过 () 或一元运算符 (- + ! ~) 使 js 解析器将 function () {} 从”函数申明”解析转变为”函数表达式”解析
  • 箭头函数 (ES6)

  • 函数构造式 (new Function)

  • 生成器函数 (Generator ES6)

  • no return -> return undefined

function parameters & function arguments

parameters — 函数定义时的形参
arguments — 函数使用时传入的实参

Rest parameters (ES6)

function name(first, second, ...remainingParams) { ... }
remainingParams 为数组, 记录剩余的传入参数

Default parameters (ES6)

function name(param = 'default')
参数未传, 或传入值为 undefined 时, 取默认值
注意, 当传入值为 null 时, 认定为有值, 不取默认值

  • let test = (() => {'test'})() // test -> undefined
    1
    2
    3
    4
    function test(a, ...b) { ... }
    test()
    // a -> undefined
    // b -> []

Chapter 4 Functions for the journeyman: understanding function invocation

arguments - 调用函数时传入的全部参数

  • 类数组形式存储, 具有 length 属性, 可通过数组序号的方式引用特定参数, 但不具有数组其他方法
    * ES6 rest parameters 是真正的数组

  • 在非严格模式, arguments 与 parameters 关联, 更改 arguments, 对应的 parameters 也会改变, 反之亦然;
    使用 strict mode, 可切断这种关联: "use strict";

this - 函数上下文, 即调用该函数的对象

函数调用的四种形式:

作为一个函数: testF()

在非严格模式, this 指向全局变量 window
在严格模式, this 指向 undefined

作为一个方法: testO.testF()

this 指向函数挂载的对象 testO

作为一个对象构造函数: new testF()

* 不支持箭头函数

当执行构造函数时, 发生了以下步骤:
1.创建一个新的空对象
2.将这个空对象传入构造函数, 此时构造函数中的 this 指向该空对象
3.解析构造函数代码, 在这个空对象上绑定若干属性或方法, 或进行其他操作
4.将处理完毕的对象作为 new 操作符的返回值返回
* 如果构造函数中有 return 语句: return 对象类型, 则 new 操作符返回的对象即为该 return 对象; return 非对象类型, 则返回内容会被无视.

普通函数与构造函数的命名约定:
- 普通函数以动词命名, 并以小写字母开头
- 构造函数以名词命名, 并以大写字母开头

通过函数的 apply 或 call 方法

  • apply & call 的语法
    [Function].apply([Object], [Array])
    - Object: Fuction 的上下文 (this), Array: Fuction 的入参 (arguments)
    [Function].call([Object], ...arguments)
    - Object: Fuction 的上下文 (this)

  • 练习

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    function forEach(list, callback) {
    if (list instanceof Array && callback instanceof Function) {
    for (let n = 0; n < list.length; n++) {
    let result = callback.call(list[n], n);
    if (result === false) break;
    }
    }
    }
    let test = [{value: 'a'}, {value: 'b'}, {value: 'c'}];
    forEach(test, function(index) {
    console.log(index, this === test[index]);
    if (test[index].value === 'b') return false;
    })

修复 this 指向问题的其他方法: bind & 箭头函数

  • 事件回调函数的上下文问题

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <button id="test">Click Me!</button>
    <script>
    function Button() {
    this.clicked = false;
    this.click = function() {
    this.clicked = true;
    console.log(button.clicked); // false
    console.log(this); // <button id="test">Click Me!</button>
    }
    }
    var button = new Button();
    var elem = document.getElementById("test");
    elem.addEventListener("click", button.click);
    </script>

    解释: 事件回调函数的上下文是触发该事件的 DOM 对象, 其实这里只是把 button.click 指向的匿名函数传给了事件监听方法作为回调函数, 因此与 button 对象没有什么关系

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    <!DOCTYPE html>
    <html>
    <head>
    <title>test</title>
    </head>
    <body>
    <button id="test">Click Me!</button>
    <script>
    function Button() {
    this.clicked = false;
    this.click = function() {
    this.clicked = true;
    console.log(button.clicked); // true
    console.log(this); // Button
    }
    }
    var button = new Button();
    var elem = document.getElementById("test");
    elem.addEventListener("click", function() { button.click(); });
    </script>
    </body>
    </html>

    解释: 事件回调函数是一个匿名函数, 它的上下文是触发该事件的 DOM 对象, 而进行的操作是执行 button 实例对象的 click 方法, 因此执行时上下文是 Button -> button (个人理解, button.click 指向的匿名函数的 this 指向 Button 构造函数的上下文, 实例化 Button 后, 一起指向了 button 实例对象), 可以正确改变 button.clicked 属性

箭头函数

箭头函数的 this 指向函数定义时的上下文

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE html>
<html>
<head>
<title>test</title>
</head>
<body>
<button id="test">Click Me!</button>
<script>
function Button() {
this.clicked = false;
this.click = () => {
this.clicked = true;
console.log(button.clicked); // true
console.log(this); // Button
}
}
var button = new Button();
var elem = document.getElementById("test");
elem.addEventListener("click", button.click);
</script>
</body>
</html>

解释:虽然事件回掉函数仍是 button.click 指向的匿名函数, 但是该函数用箭头函数的方式定义, 因此该函数的上下文被固定为定义时的上下文, 即 Button -> button

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!DOCTYPE html>
<html>
<head>
<title>test</title>
</head>
<body>
<button id="test">Click Me!</button>
<script>
var button = {
clicked: false,
click: () => {
this.clicked = true;
console.log(button.clicked); // false
console.log(this); // window (window.clicked = true)
}
}
var elem = document.getElementById("test");
elem.addEventListener("click", button.click);
</script>
</body>
</html>

解释:定义该箭头函数时的上下文是 window (button 是全局变量, 该对象的定义代码在全局代码 global code 中)

函数的 bind 方法 [Function].bind([Object])

该方法创建一个新的方法, 执行方式与原函数一致, 但是上下文为指定的 object

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!DOCTYPE html>
<html>
<head>
<title>test</title>
</head>
<body>
<button id="test">Click Me!</button>
<script>
var button = {
clicked: false,
click: function() {
this.clicked = true;
console.log(button.clicked); // true
console.log(this); // button
}
}
var elem = document.getElementById("test");
elem.addEventListener("click", button.click.bind(button)); // 否则上下文就是 DOM button
var newClick = button.click.bind(button);
console.log(newClick !== button.click) // true (bind 创造一个新方法的证明)
</script>
</body>
</html>

练习:

1
2
3
4
5
6
7
8
9
10
11
12
13
var test1 = {
key: function() {
return this;
}
};
var test2 = {
key: test1.key
};
var test3 = test2.key;

console.log(test1.key()); // 作为一个方法被调用, test1
console.log(test2.key()); // 作为一个方法被调用, test2
console.log(test3()); // 作为一个函数被调用, window
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Test() {
this.key = () => this; // 被锁死绑定在 Test 上
}

var test1 = new Test(); // Test 被实例化, Test 的 this 指向 test1
var test2 = new Test(); // Test 被实例化, Test 的 this 指向 test2
var test3 = {
key: test1.key
};
var test4 = {
key: test2.key
};

console.log(test1.key()); // test1
console.log(test2.key()); // test2
console.log(test3.key()); // test1
console.log(test4.key()); // test2
1
2
3
4
5
6
7
8
9
10
11
12
13
function Test() {
this.key = function() {
return this;
}.bind(this); // 创建了一个 this 永远指向 Test 的方法
}

var test1 = new Test(); // Test 被实例化, Test 的 this 指向 test1
var test2 = {
key: test1.key
};

console.log(test1.key()); // test1
console.log(test2.key()); // test1

Chapter 5 Functions for the master: closures and scopes

scopes 作用域

  • 函数执行时的 execution contexts (执行上下文)

    执行上下文是 JavaScript 引擎运行时执行在内部的一个机制, 用以追踪函数执行过程, 与函数的上下文 (this) 概念不同.

    执行上下文可分为: global execution context 与 function execution context, 全局执行上下文仅有一个 (一个 JavaScript 程序一个, 一个 web 页面一个), 而每个函数被调用时会生成一个新的函数执行上下文.

    在程序运行时, 通过 execution context stack (执行栈, 上进上出) 去管理当前正在执行的 execution context 以及等待执行中的 execution context.

  • lexical environments (口语化表达即作用域)

    每生成一个执行上下文, 则对应生成一个作用域. 每当调用一个函数, JavaScript 引擎会为这个函数生成一个作用域, 并带有一个内部属性 [[Environment]], 使该函数可以通过该属性提供的引用地址找到当前作用域 (外部作用域). 举例来说:

    1
    2
    3
    4
    5
    let a = 0;
    function test() {
    let b = 1;
    }
    test();

    对于全局作用域, 有 a 变量和 test 函数, 并且 test 函数的作用域内存在一个内部的 [[Environment]] 属性, 该属性记录了全局作用域的引用地址; 另外, test 函数作用域内还有 b 变量.

    代码执行过程中的变量查找流程:
    先在当前环境查找, 若没有找到则根据 [[Environment]] 指向去外部环境查找, 一直到找打或在全局作用域都未找到为止

  • 变量定义的分类

    1.const 定义常量
    对于引用类型, 仅保证了指向的不变性, 但指向对象仍可进行更改 (Object, Array, …)
    作用域情况与 let 一致
    2.var 定义变量
    仅存在全局作用域以及函数作用域两种 (defined in the closest function or global environment)
    3.let 定义变量
    存在全局作用域, 函数作用域以及块级作用域 (defined in the closest environment)

  • 变量定义的流程

    当一个新的语法环境创建时:
    Step1: 遍历代码, 找出声明的函数以及变量
    Step2: 创建后, 再按从上到下的顺序执行代码

    在 Step1 中:
    先创建函数: 包括函数隐含的 arguments 以及明确定义的参数, 重复声明则覆盖前一个
    再声明变量: 变量值为 undefined, 若变量名已经存在, 则不重新创建或覆盖

    * 此处的函数仅指函数声明形式, 不包括函数表达式以及箭头函数 (这两类在 Step1 归类于变量声明, 当进入 Step2 执行到变量定义处, 才被创建)

    1
    2
    3
    4
    5
    console.log(test);  // function
    var test = 3;
    console.log(test); // 3
    function test() {}
    console.log(test); // 3

closures 闭包

当声明或创建函数时, 会将当前有效作用域下的所有变量与函数一起形成组合关系 (闭包), 由此, 变量都会保留在内存中, 以供函数调用时使用, 直到与之绑定关系的函数不再使用, 才被垃圾回收机制拾取销毁.

  • 应用分析1: 模拟私有变量

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    function Test() {
    var value = 0;
    this.getValue = function() {
    return value;
    };
    this.add = function() {
    value++;
    }
    }

    var test1 = new Test();
    test1.add();
    var test2 = new Test();

    console.log(test1.value); // undefined
    console.log(test1.getValue()); // 1
    console.log(test2.getValue()); // 0

    当执行 new 操作符时, 创建 Test 语法环境, 内含 value 变量. 每执行一次 new, 都生成一个新的语法环境. 由于 new 的结果变量未被销毁, 因此两个 Test 语法环境都被保留.
    当执行 add/getValue 方法时, 创建对应方法的语法环境, 改环境内无变量, 但存在 [[Environment]] 指向对应的 Test 语法环境, 从而实现修改或获得 value 变量的值.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    function Test() {
    var value = 0;
    this.getValue = function() {
    return value;
    };
    this.add = function() {
    value++;
    }
    }

    var test1 = new Test();
    var test2 = {};
    test2.getValue = test1.getValue;

    console.log(test1.getValue()); // 0
    console.log(test2.getValue()); // 0

    test1.add();

    console.log(test1.getValue()); // 1
    console.log(test2.getValue()); // 1

    test1 与 test2 共享了一个 getValue 语法环境

  • 应用分析2: 回调

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    <div id="box1">Move box1</div>
    <div id="box2">Move box2</div>
    <script>
    function animateIt(elementId) {
    var elem = document.getElementById(elementId);
    var tick = 0;
    var timer = setInterval(function() {
    if (tick < 100) {
    elem.style.left = elem.style.top = tick + 'px';
    tick++;
    } else {
    clearInterval(timer);
    console.log(tick); // 100
    console.log(elem); // <div id="box1/box2" style="top: 99px; left: 99px;">Move box</div>
    console.log(timer); // 1/2
    }
    }, 10);
    }
    animateIt('box1');
    animateIt('box2');
    </script>

    每当执行一个 animateIt(…), 生成一个新的 animateIt 语法环境, 包含变量: elementId, elem, tick 以及 timer
    每当定时器的时间间隔到期时, 重新唤起调用时生成的 animateIt 语法环境, 执行定时器的回调函数
    animateIt 语法环境会一直保留, 直到 clearInterval

Chapter 6 Functions for the future: generators and promises

generator

普通函数只有一个返回值, 而 generator 函数可以返回一序列的值.

初级语法

  1. 定义 generator 函数

    1
    2
    3
    4
    5
    6
    7
    8
    function* functionName () {
    ...
    yield someValue1; // 类似于普通函数里的 return
    ...
    yield someValue2;
    ...
    return endValue; // 遇到 return 或函数执行完, 则代表 generator 函数执行完毕
    }
  2. 使用 generator 函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    const iteratorObject = functionName();

    const result1 = iteratorObject.next();
    console.log(result1.value); // someValue1
    console.log(result1.done); // false

    const result2 = iteratorObject.next();
    const result3 = iteratorObject.next();

    console.log(result3.value); // undefined
    console.log(result3.done); // true -- generator 函数执行完毕

    * 执行 generator 函数时, 其实并没有真的去执行函数内的代码, 而是返回一个 iterator 对象. 该对象有一个重要的方法: next, 调用该方法可以使 generator 函数从前一个停顿点 (或头部) 继续执行, 直到遇到下一个 yield 申明的停顿点 (或执行完全部代码), 并返回一个新的对象, 以给出结果值与标记是否运行结束 (是否还有结果值可以输出).

  3. 迭代利器 — for-of 语法糖

    1
    for (let item of functionName()) { ...item... }

    等价于

    1
    while (!(item = functionName().next()).done) { ...item.value... }

    * for-of 不会读取到 done = true 时的数值

  4. 从一个 generator 函数, 唤起另一个 generator 函数: yield*

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    funtion* generator1 () {
    yield 'value1';
    yield* generator2();
    yield 'value2';
    }
    function* generator2 () {
    yield 'test';
    }
    for (let item of generator1()) {
    console.log(item);
    }
    // 输出:
    // value1
    // test
    // value2
  5. 应用实例

    1) ID 生成器 (避免申明全局变量)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    function* IdGenerator() {
    let id = 0;
    while (true) {
    yield ++id;
    }
    }
    const idIterator = IdGenerator();
    // get id
    idIterator.next().value;

    2) DOM 遍历
    目标 DOM:

    1
    2
    3
    4
    5
    6
    <div id="target">
    <form>
    <input type="text" />
    </form>
    <p>some information</p>
    </div>

    传统函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function traverseDOM(element, callback) {
    callback(element);
    element = element.firstElementChild;
    while (element) {
    traverseDOM(element, callback);
    element = element.nextElementSibling;
    }
    }
    const target = document.getElementById('target');
    traverseDOM(target, function(element) {
    console.log(element.nodeName);
    })

    generator 函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function* traverseDOM(element) {
    yield element;
    element = element.firstElementChild;
    while (element) {
    yield* traverseDOM(element);
    element = element.nextElementSibling;
    }
    }
    const target = document.getElementById('target');
    for (let element of traverseDOM()) {
    console.log(element.nodeName);
    }

传参模式

  1. 传统传参, 与调用 iterator.next 时的传参

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function* test(param) {
    let test = yield 'init ' + param; // (line 1)
    while (true) {
    test = yield 'continue ' + test; // (line 2)
    }
    }
    const iterator = test(1); // (line 3)
    console.log(iterator.next(0).value); // init 1 (line 4)
    console.log(iterator.next(2).value); // continue 2 (line 5)
    console.log(iterator.next(3).value); // continue 3 (line 6)

    解析:
    line 3 使用第一种传参模式, 传入初始值, 生成 iterator 对象.
    line 4 使 generator 函数开始执行, 到 line 1 处等号右侧 yield 停止, 输出 init 1. 在此过程中, 没有前一个暂停 yield, 因此传参无效.
    line 5 使 generator 函数继 line 1 停止处继续执行, 直至 line 2 处等号右侧 yield 停止, 输出 continue 2. 在此过程中, 因为是从 line 1 的 yield 开始的, next 传入的参数 2 成为 line 1 等号右侧整个 yield 表达式的值, 因此变量 test 被赋值为 2; 运行到 line 2 时, 通过新的 yield 暂停并输出 continue 2.
    line 6 使 generator 函数继 line 2 停止处继续执行, 直至再次返回 line 2 处, 在等号右侧 yield 停止, 输出 continue 3. 在此过程中, next 传入的参数 3 首先成为 line 2 前一次停止节点 yield 的值, 因此对 test 变量重新赋值; 等再次运行到 line 2 时, 通过 yield 暂停并输出 continue 3.

  2. 抛入异常 iterator.throw

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    function* test(param) {
    try {
    let test = yield 'init ' + param;
    while (true) {
    console.log('enter circulation');
    test = yield 'continue ' + test;
    }
    } catch (e) {
    console.log(e);
    }
    }
    const iterator = test(1);
    console.log(iterator.next().value);
    iterator.throw('error');
    // 输出:
    // init 1
    // error

    应用场景: 处理异步请求

generator 运行解析

  • generator 函数的 4 种状态

    1) Suspended start
    generator 函数被创建, 返回 iterator 对象, 但函数内部未运行任何代码
    2) Executing
    当 iterator.next 被调用, generator 函数从头部或前一个暂停 yield 处开始运行
    3) Suspended yield
    当运行遇到 yield, 返回结果对象 ({value: undefined|someValue, done: true|false}), 并暂停运行
    4) Completed
    运行遇到 return 或运行到函数最后

  • generator 函数与普通函数执行上下文的差异

    当普通函数被调用时, 生成执行上下文推入执行栈, 函数执行完毕后, 其上下文被推出执行栈并销毁.

    当 generator 函数被调用时:
    Step 1 生成执行上下文推入执行栈, 此时 generator 函数并不执行, 而是处于 Suspended start 状态;
    Step 2 生成并返回 iterator 对象, 执行上下文推出执行栈, 但由于 iterator 指向该上下文, 该上下文被保留, 而非销毁 (类似于闭包)

    每次调用 iterator.next 时, 被保留的执行上下文都会再次推入执行栈, 并在遇到 yield 或 return 或到达函数底部时推出执行栈; 但由于 iterator 对象始终存在且指向该上下文, 该执行上下文始终不会被销毁.

promise 作为异步执行结果占位符的一个对象

* try catch 无法捕捉的错误: 异步回调函数内的错误. 因为回调函数的执行时机与发起异步的时机不一致, 此时 try catch 已失效.

1
2
3
4
5
6
7
try {
setTimeout(function() {
// occur some errors
}, seconds)
} catch(e) {
...
}

基本语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
console.log('start');
const test = new Promise((resolve, reject) => {
console.log('promise start');
setTimeout(() => {
console.log('run later');
resolve(data); | reject(err);
}, seconds)
})
test.then(data => {
console.log('resolve');
}).catch(err => {
console.log('reject');
});
console.log('end');

// 输出:
// start
// promise start
// end
// run later
// resolve | reject

* 传入 Promise 构造函数的箭头函数被当即执行, 但其中的异步回调内容被放入事件队列中等待. promise 对象的 then 和 catch 方法也始终被放入事件队列中等待执行, 与主程序运行到此处时 promise 的状态无关.
* promise 对象的 then 和 catch 方法返回一个新的 promise, 以保证可以链式调用.

promise 的 3 种状态

  • pending state (unresolved state)
  • fulfilled state (resolved state)
  • rejected state (resolved state)

在异步未获得结果时, promise 处于 pending state; 当 promise 的 resolve 函数被调用, promise 进入 fulfilled state, 可以通过 then 获得异步执行结果; 当 promise 的 reject 函数被调用, 或执行过程中发生错误, promise 进入 rejected state, 可以通过 catch 获得错误信息.

promise 一旦进入 resolved state, 就不会再变更状态, 即无法在 fulfilled state 与 rejected state 间转换.

链式异步 & 平行异步

  • 链式异步

    1
    2
    // getJSON 返回的是一个 promise 对象
    getJSON(url1).then(result => getJSON(result)).then(result => getJSON(result)).catch(err => {...})

    * 任意一处 getJSON 发生错误, 则被最后的 catch 捕捉.

  • 平行异步

    1
    2
    3
    4
    Promise.all([getJSON(test1), getJSON(test2), getJSON(test3)]).then(result => {
    const result1 = result[0], result2 = result[1], result3 = result[2];
    ...
    }).catch(err => { ... })

    * 当所有 promise 都成功回调, 触发 then; 任一回调失败, 则触发 catch.

    1
    Promise.race([getJSON(test1), getJSON(test2), getJSON(test3)]).then(result => {...}).catch(err => {...})

    * 当任一 promise 回调成功或失败, 则对应触发 then 或 catch.

generator 与 promise 的结合

1
2
3
4
5
6
7
8
async funtion() {
try {
const result = await getJSON(url);
console.log(result);
} catch(e) {
console.log(e);
}
}

内在机理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function async(generator) {
var iterator = generator();

function handle(iteratorResult) {
if (iteratorResult.done) return;
const iteratorValue = iteratorResult.value;
if (iteratorValue instanceof Promise) {
iteratorValue.then(res => handle(iterator.next(res)))
.catch(err => iterator.throw(err));
}
}

try {
handle(iterator.next());
} catch (e) {
iterator.throw(e);
}
}

async(funtion* () {
try {
const result1 = yield getJSON(url);
const result2 = yield getJSON(result1.url);
} catch (e) {
console.log(e);
}
})

Part 3 Digging into objects and fortifying your code

Chapter 7 Object orientation with prototypes

每一个对象类型都先天有一个 [[prototype]] 隐藏属性, 该属性是一个对象类型

  • Object.setPrototypeOf(obj1, ob2)
    将 obj2 作为 obj1 的 [[prototype]] 隐藏属性, 此时 obj1 继承了 obj2 的属性.

实例 prototype 与原型 prototype

每一个函数在声明时也都会产生一个隐藏属性: 原型 prototype, 该对象有一个先天的 constructor 属性, 指向构造函数本身;
当这个函数作为构造函数被执行时, 实例对象的实例 prototype 会指向该构造函数的原型 prototype.

1
2
3
4
5
6
7
8
funtion Test() {
this.state = false;
this.showState = () => !this.state; // this 指向实例, 此处直接定义了实例的方法
}
Test.prototype.showState = () => this.state; // 此处定义了原型 prototype 方法

const test = new Test(); // test 继承了 Test 的原型 prototype
console.log(test.showState()); // true, 由于实例已存在该方法, 因此不会通过 [[prototype]] 隐藏属性进一步查询

通过 this 绑定的方法, 每构建一个实例, 则生成一个专属的方法; 而通过原型 prototype 定义的方法, 多个实例, 通过其 [[prototype]] 隐藏属性共享一个.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
funtion Test() {
this.state = true;
this.code = 0;
}
const test1 = new Test();
Test.prototype.showState = () => this.state;
console.log(test1.showState()); // true

Test.prototype = {
showCode: () => this.code,
}
const test2 = new Test();
console.log(test1.showState()); // true
console.log(test2.showState()); // function is not existed
console.log(test1.showCode()); // function is not existed
console.log(test2.showCode()); // 0

实例的 [[prototype]] 隐藏属性指向实例化那一刻构造函数 [[prototype]] 隐藏属性指向的对象, 后续对该对象进行的修改, 都能反映到实例上; 但是如果改变构造函数 [[prototype]] 隐藏属性的指向, 则该修改前生成的实例仍读取原本的对象, 修改后生成的实例则读取现在指向的对象.

constructor 属性

用于类型判断:

1
2
3
4
5
6
7
8
9
10
function Test() {}
const test1 = new Test();
const test2 = new test1.constructor(); // 当 Test 不在作用域内, 可通过该方式实例化

console.log(typeof test1); // Object
console.log(typeof test2); // Object
console.log(test1.constructor); // Test
console.log(test2.constructor); // Test
console.log(test1 instanceof Test); // true
console.log(test2 instanceof Test); // true

Inheritance 继承

property descriptor

  • configurable 是否可被修改以及删除

  • enumerable 是否在 for-in 循环中可被读取到

  • value 属性的值, 默认值为 undefined

  • writable 是否可被修改

  • get (getter function)
    当读取该属性时调用

  • set (setter function)
    当赋值该属性时调用

Object.defineProperty

1
2
3
4
Object.defineProperty(object, "property", {
configurable: true,
value: 'test',
})

继承实现

  1. Dancer 拥有了 Person 的 dancer 方法, 但仅是简单拷贝, 并不是继承

    1
    2
    3
    4
    5
    6
    7
    8
    9
    function Person() {}
    Person.prototype.dance = function() {};

    function Dancer() {}
    Dancer.prototype = { dance: Person.prototype.dance };

    const dancer = new Dancer();
    console.log(dancer instanceof Dancer); // true
    console.log(dancer instanceof Person); // false
  2. 实现继承的方案: subClass.prototype = new SuperClass()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function Person() {}
    Person.prototype.dance = function() {};

    function Dancer() {}
    Dancer.prototype = new Person();

    const dancer = new Dancer();
    console.log(dancer instanceof Dancer); // true
    console.log(dancer instanceof Person); // true
    console.log(dancer.constructor); // Person

    但是存在一个弊端: 丢失了 Dancer 原本的 prototype

    解决:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    function Person() {}
    Person.prototype.dance = function() {};

    function Dancer() {}
    Dancer.prototype = new Person();

    Object.defineProperty(Dancer.prototype, "constructor", {
    enumerable: false,
    value: Dancer,
    writable: true,
    })

    const dancer = new Dancer();
    console.log(dancer instanceof Dancer); // true
    console.log(dancer instanceof Person); // true
    console.log(dancer.constructor); // Dancer
  3. 不推荐的继承方案

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    function Person() {}
    Person.prototype.dance = function() {};

    function Dancer() {}
    Dancer.prototype = Person.prototype;

    const person = new Person();
    const dancer = new Dancer();

    console.log(person.test); // undefined
    console.log(dancer.test); // undefined

    Dancer.prototype.test = "test";

    console.log(person.test); // test
    console.log(dancer.test); // test

    由于 Dancer.prototype 与 Person.prototype 指向了同一个对象, 因此修改 Dancer.prototype 也会影响 Person.prototype, 从而产生问题.

A instanceof B

判断 B 当前的 prototype 在不在 A 的原型链上

1
2
3
4
5
6
7
8
function Test() {}
const test = new Test();

console.log(test instanceof Test); // true

Test.prototype = {};

console.log(test instanceof Test); // false

class 语法糖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Person {
constructor(name) {
this.name = name;
}
walk() {}
}

class Dancer extends Person {
constructor(name, level) {
super(name);
this.level = level;
}
dance() {}
static compare(dancer1, dancer2) { // 类的静态属性, 实例上不存在
return dancer1.level - dancer2.level;
}
}

const dancer1 = new Dancer('A', 1);
const dancer2 = new Dancer('B', 2);

dancer1.dance();
dancer1.walk();
dancer1.compare; // undefined!
Dancer.compare(dancer1, dancer2);

pre-es6:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
funtion Person(name) {
this.name = name;
}
Person.prototype.walk = function() {};

funtion Dancer(name, level) {
this.level = level;
Dancer.prototype = new Person(name); // 这是我自己想象的实现可能性
}
Object.defineProperty(Dancer.prototype, "constructor", {
enumerable: false,
value: Dancer,
writable: true,
})
Dancer.compare = (dancer1, dancer2) => dancer1.level - dancer2.level;

Chapter 8 Controlling access to objects

getters and setters

  1. 在对象常量中定义 (ES5)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    const obj = {
    list: ['a', 'b', 'c'],
    get firstChild() {
    return this.list[0];
    }
    set firstChild(value) {
    this.list[0] = value;
    }
    };

    console.log(obj.firstChild); // a
    obj.firstChild = 'change';
    console.log(obj.firstChild); // change
    console.log(obj.list[0]); // change
  2. 构造类中定义 (ES6)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    class Test {
    constructor() {
    this.list = ['a', 'b', 'c'];
    }
    get firstChild() {
    return this.list[0];
    }
    set firstChild(value) {
    this.list[0] = value;
    }
    }

    const test = new Test();
    console.log(test.firstChild); // a
    test.firstChild = 'change';
    console.log(test.firstChild); // change
    console.log(test.list[0]); // change

* 如果只定义 getter 不定义 setter, 在进行赋值时: 非严格模式, 会无视赋值请求; 严格模式, 会抛出错误.

  1. Object.defineProperty (可以真正实现私有属性)
    1, 2 方法其实没有实现 list 变量的私有化, 我们仍然可以从外部通过闭包原理直接读写
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    function Test() {
    let _val = 0;
    Object.defineProperty(this, 'val', {
    get: () => _val,
    set: val => { _val = val; }
    });
    }

    const test = new Test();
    console.log(test._val); // undefined
    console.log(test.val); // 0
    test.val = 1;
    console.log(test.val); // 1

应用举例: 校验传入值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Test() {
let _val = 0;
Object.defineProperty(this, 'val', {
get: () => _val,
set: val => {
if (!Number.isInteger(val)) {
throw new TypeError("should be an Int")
}
_val = val;
}
});
}
const test = new Test();
try {
test.val = 'test'
} catch(e) {
console.log(e.name); // TypeError
console.log(e.message); // should be an Int
}

proxy 代理

可拦截处理的操作类型:

  • get/set: 读写属性
  • apply: 调用方法
  • construct: new 一个对象
  • enumerate: for-in遍历
  • getPrototypeOf/setPrototypeOf: 读写 prototype 属性
  • 更多类型详情

不可被拦截的操作符: 等于 (==/===), instanceof, typeof

应用:

  1. 读写对象属性时输出 log 信息

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function makeLog(target) {
    return new Proxy(target, {
    get: (obj, key) => {
    // do some logs
    return obj[key]
    },
    set: (obj, key, value) => {
    // do some logs
    obj[key] = value;
    }
    })
    }
  2. 监控函数的运行时长

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function getRuningTime(func) {
    return new Proxy(func, {
    apply: (target, thisArg, args) => {
    console.time('function');
    const res = target.apply(thisArg, args);
    console.timeEnd('function');
    return res;
    }
    })
    }
  3. 自动填充对象属性

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    function Folder() {
    return new Proxy({}, {
    get: (target, property) => {
    if (!(property in target)) {
    target[property] = new Folder();
    }
    return target[property];
    }
    })
    }

    const root = new Folder();
    root.child.value = 'test'; // 并不会出现报错

    * 虽然没有报错, 但是输出 root 和 root.child 并不是简单的对象, 因此实用性我还是质疑的

  4. 使数组支持负数索引 (从尾部往头部读取)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function createNegativeArrayProxy(array) {
    return new Proxy(array, {
    get: (target, index) => {
    index = +index; // 转换成数字
    return target[index < 0 ? target.length + index : index];
    },
    set: (target, index, value) => {
    index = +index;
    return target[index < 0 ? target.length + index : index] = value;
    }
    })
    }

缺陷: 消耗性能, 开启后程序运行变慢!

* 如果代理只定义了 get 没有定义 set, 当进行 set 操作时, 则无拦截代理, 直接执行原生的 set 操作. (与前面的 getter, setter 情况不同)

Chapter 9 Dealing with collections

Array

  • 数组的创建方法:
    [] (推荐)
    new Array()

  • JavaScript Array 其本质上是 Object
    证据: 当索引超过当前数组长度, 并不会导致报错, 仅返回 undefined.

  • 数组的独特属性: length
    该属性可以被手动修改, 当修改值大于当前值, 数组容量会被扩大; 当修改值小于当前值, 数组容量会被压缩, 超出 length 的数据会丢失.

数组的操作方法

  • 实时改变数组长度的增删元素方法:
    1. push: 向数组尾部增加一个元素, 不影响数组其他项 (推荐)
    2. pop: 从数组尾部取出一个元素, 不影响数组其他项 (推荐)
    3. unshift: 向数组头部增加一个元素, 更改数组各项的索引
    4. shift: 从数组头部取出一个元素, 更改数组各项的索引

  • 不改变数组长度的删除元素方法: delete arr[i]
    仅清空 i 项的数值, 仅使 arr[i] 成为 undefined, 数组总长度不变

  • splice vs slice
    Array.splice(index, number, ...restParams) 该方法会改变原数组
    - index 开始截取的元素索引
    - number 截取掉的元素个数, 不传则认为从 index 开始截取到数组最后; 传 0 则不截取, 新元素塞在 index 元素前
    - restParams 从截取处塞入的新元素
    - 方法返回一个数组, 存储了被截走的元素

    Array.slice(indexStart, indexEnd) 该方法不会改变原数组
    - indexStart 开始取的元素索引, 不传则为取整个数组, 是一种复制数组的方式
    - indexEnd 结束索引, 不包括该元素, 不传则认为从 indexStart 开始取到数组最后
    - 方法返回一个数组, 存储取到的元素

  • 遍历数组
    1. for(let i = 0; i < arr.length; i++) { ... } (不推荐)
    2. arr.forEach(item => { .. })

    最传统的遍历方法

    3. newArr = arr.map((item, index) => { return ... })

    该方法首先创建了一个新数组, 然后对原数组进行遍历, 根据回调函数的 return 结果, 更新这个新数组, 并在遍历完成后返回; 如果遍历至某一项时没有 return, 则新数组对应的该项为 undefined.
  • 根据条件查找
    1. arr.every(item => {... return true || false; })

    测试是否数组每一项都符合条件. 当任一一项 return 为 false 时, 则停止遍历, 返回 false; 否则遍历结束, 返回 true.

    2. arr.some(item => {... return true || false; })

    测试数组是否至少有一项符合条件. 当任一一项 return 为 true 时, 则停止遍历, 返回 true; 否则遍历结束, 返回 false.

    3. arr.find(item => {... return true || false; }) (ES6)

    返回第一项 return 为 true 的数组元素, 若无则返回 undefined.

    4. arr.filter(item => {... return true || false; })

    返回 return 为 true 的所有数组元素 (数组形式), 若无则返回空数组.

    5. arr.indexOf(item) & arr.lastIndexOf(item)

    寻找特定项的索引, 如未找到, 则返回 -1.
    indexOf: 第一个匹配的索引; lastIndexOf: 最后一个匹配的索引.

    6. arr.findIndex(item => {... return true || false; }))

    返回第一项 return 为 true 的数组元素索引, 若无则返回 -1.
  • 排序 arr.sort((a, b) => a - b)
    回调函数返回的值 < 0, 代表 a 应该在 b 之前
    回调函数返回的值 = 0, 代表 a 与 b 等价, 不用移动两者的位置
    回调函数返回的值 > 0, 代表 a 应该在 b 之后

  • 将数组聚合成一个值 arr.reduce((aggregated, item, index) => { ... }, initialValue)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    [1, 2, 3].reduce((aggregated, item, index) => {
    console.log(aggregated, item, index);
    });
    // 输出:
    // 1 2 1
    // undefined 3 2

    [1, 2, 3].reduce((aggregated, item, index) => {
    console.log(aggregated, item, index);
    }, 0);
    // 输出:
    // 0 1 0
    // undefined 2 1
    // undefined 3 2

    const result = [1, 2, 3].reduce((aggregated, item, index) => {
    console.log(aggregated, item, index);
    return aggregated + item;
    }, 0);
    console.log(result);
    // 输出:
    // 0 1 0
    // 1 2 1
    // 3 3 2
    // 6

    aggregated: 聚合值
    遍历数组前被置为 initialValue, 若无 initialValue 则把第一项设为初始值, 从第二项开始遍历; 遍历开始后, 聚合值为前一次遍历的 return 值

    reduce 去重应用:

    1
    2
    3
    [1, 2, 2, 4, null, null].reduce((accumulator, current) => {
    return accumulator.includes(current) ? accumulator : accumulator.concat(current);
    }, []);

Map (ES6)

Object 用作 maps/dictionaries 的弊端:
1. 当属性未明确定义, 有访问到原型链上隐藏属性的风险, 如: constructor
2. 只能以 string 作为 key, 如果创建时不是 string 类型, 会调用 toString 方法转换成 string 类型

基础操作

map 支持各种类型作为 key:

1
2
3
4
5
6
7
8
9
10
11
12
13
const map = new Map();  // 初始化
const obj1 = { value: 1 };
const obj2 = { value: 2 };

map.set(obj1, { name: 'obj1' }); // 以 obj1 为 key, 创建键值对
map.set(obj2, { name: 'obj2' }); // 以 obj2 为 key, 创建键值对

console.log(map.get(obj1)); // { name: 'obj1' } --- 获取某个 key 对应的值
console.log(map.size); // 2 --- map 中存储的键值对数量
console.log(map.has(obj1)); // true --- 判断 map 中是否含有某个 key

map.delete(obj1); // 删除某个 key
map.clear(); // 清除 map 中的全部键值对

遍历 for…of

当遍历时, 顺序以 set 的先后顺序为准, 而对于普通 object, 遍历顺序则没有保障.
* for…of 是一个强大的遍历方法, chapter 6 提到可以遍历 generator.

1
2
3
4
5
6
7
8
9
10
11
12
13
for (let item of map) {
console.log(item[0]); // key
console.log(item[1]); // value
}

for (let key of map.keys()) {
console.log(key); // key
console.log(map.get(key)); // value
}

for (let value of map.values()) {
console.log(value); // value
}

Set (ES6)

存储一系列不重复的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const set = new Set([1, 3, 2, 1, 1]);  // 初始化, 可支持传入数组作为 set 的初始值

console.log(set.size); // 3
console.log(set.has(3)); // true

set.add(2);
console.log(set.size); // 3 --- 只有不存在的值可以被插入
set.add(4);
console.log(set.size); // 4

for (let item of set) {
console.log(item); // 1, 3, 2, 4 --- 根据插入顺序遍历
}

set.delete(2);
set.clear();

两个 set 的并集与交集:

1
2
3
4
5
6
const set1 = new Set([1, 2, 3, 4, 5]);
const set2 = new Set([1, 3, 5, 7, 9]);
// 并集
const set3 = new Set([...set1, ...set2]); // 1, 2, 3, 4, 5, 7, 9
// 交集
const set4 = new Set([...set1].filter(item => set2.has(item))); // 1, 3, 5

* 通过...运算符将 set 转变为数组

Chapter 10 Wrangling regular expressions

正则的创建

  1. 正则常量 (首选)
    const pattern = /.../g

  2. 正则对象 (需要动态创建正则时使用)
    const pattern = new RegExp([string], 'i')

  3. 修饰符

    修饰符 含义
    g 全局匹配
    i 不区分大小写匹配
    m 多行匹配s
    s .匹配符包含换行符
  4. 注意点
    1. /\d/ <–> new RegExp('\\d')
    2. 用变量预先保存正则表达式, 从而减少后续复用产生的编译次数

正则语法

  1. 匹配精确指定的字符
    /test/

  2. 匹配指定范围的字符
    /[abc]/ — 匹配一个字符, 可以是 a, b, c 中的任一
    /[^abc]/ — 匹配任一个不是 a, b, c 的字符
    /[a-m]/ — 匹配 a 至 m 中的任一字符

  3. 转义符 \
    对于一些有特殊用法的字符, 有时候又需要匹配其原义, 这时可以用转义字符去实现, 如: . [ $ ^

  4. 头尾限定符
    /^test/ — 以 test 打头
    /test$/ — 以 test 结尾

  5. 重复字符匹配

    符号 含义 等价于
    ? 匹配一个或零个 {0, 1}
    + 匹配一个或多个 {1,}
    * 匹配零个或多个 {0,}

    重复字符匹配默认是贪心匹配模式(尽可能的取更多项), 可通过追加 ? 字符变为非贪心模式(一旦满足匹配则不再多取)
    例如对于 ‘aaa’ 字符串, /a+/匹配 ‘aaa’, /a+?/匹配 ‘a’

  6. 预定义匹配字符

    符号 含义 等价于
    \t 水平制表符 Tab
    \b 回退键 Backspace
    \v 垂直制表符
    \f 换页
    \r 换行符
    \n 换行符
    \cA : \cZ 控制字符
    \u000 : \uFFF 十六进制字符
    \x00 : \xFF ASCII字符
    . 匹配任意非空白字符 \S
    \d 匹配任意数字 [0-9]
    \D 匹配任意非数字 [^0-9]
    \w 匹配任意字母, 数字, 下划线 [A-Za-z0-9_]
    \W 匹配任意非字母, 数字, 下划线的字符 [^A-Za-z0-9_]
    \s 匹配任意空白字符
    \S 匹配任意非空白字符
    \b 匹配单词边界
    \B 匹配非单词边界

    * 换行符属于空白字符, 一般不能被 \S. 匹配到, 但是通过修饰符 s 可使 . 匹配到换行符
    * 匹配 Unicode 字符 (全集): [\u0080-\uFFFF]

  7. “或”匹配符
    /a|b/ 匹配 a 或 b
    /(ab)|(cd)/ 匹配 ab 或 cd

  8. 向后引用

    括号内匹配的内容, 可以在后面用 \1, \2 等再次匹配, 例如:
    /([ab])e\1/ 可以匹配 aea, beb
    /[ab]e[ab]/ 可以匹配 aea, aeb, bea, beb
    实用场景: 匹配 html 元素及其内容 /<(\w+)([^>]*)>(.*)<\/\1>/

  9. 括号在正则中的作用
    括号在正则中默认既有对匹配符的分组作用, 又有捕获作用.

    捕获作用:
    a) 向后引用, 用 \1, \2 的形式引用前面括号捕获的内容, 例如:

    1
    /<(\w+)>.*<\/\1>/.test('<span>test</span>');  // true

    b) 用 $1, $2 获取括号捕获到的内容, 例如:

    1
    2
    'fontFamily'.replace(/([A-Z])/g, '-$1').toLowerCase();  // font-family
    RegExp.$1; // F

    c) match 方法的结果

    但有时候不需要捕获作用, 希望括号仅作为分组使用, 则可使用 (?:pattern) 的形式:
    /<(\w+)(?:[^>]*)>(.*)<\/\1>/ — 只捕获元素的标签名与元素内容, 不捕获标签属性信息

一些匹配方法

  1. match

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    const html = '<div class="class"><span>text</span></div>';
    const tag1 = /<(\/?)(\w+)([^>]*)>/;
    const tag2 = /<(\/?)(\w+)([^>]*)>/g;

    const result1 = html.match(tag1);
    // 匹配第一个符合的项 <div class="class">
    // 匹配项整体 result1[0] = '<div class="class">'
    // 第一个括号捕获值 result1[1] = ''
    // 第二个括号捕获值 result1[2] = 'div'
    // 第三个括号捕获值 result1[3] = ' class="class"'

    const result2 = html.match(tag2);
    // 匹配所有符合的项
    // result1[0] = '<div class="class">'
    // result1[1] = '<span>'
    // result1[2] = '</span>'
    // result1[3] = '</div>'

    * 无匹配则返回 null

  2. exec
    通过多次调用, 实现对一个字符串的多次匹配查找, 每次执行, 顺序返回一个匹配结果.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    const html = '<div class="class">text</div>';
    const tag = /<(\/?)(\w+)([^>]*)>/g;
    let match;

    match = tag.exec(html);
    // 匹配第一个符合的项 <div class="class">
    // 匹配项整体 match[0] = '<div class="class">'
    // 第一个括号捕获值 match[1] = ''
    // 第二个括号捕获值 match[2] = 'div'
    // 第三个括号捕获值 match[3] = ' class="class"'

    match = tag.exec(html);
    // 匹配第二个符合的项 </div>
    // 匹配项整体 match[0] = '</div>'
    // 第一个括号捕获值 match[1] = '/'
    // 第二个括号捕获值 match[2] = 'div'
    // 第三个括号捕获值 match[3] = ''

    match = tag.exec(html);
    // 无更多匹配项: null
  3. replace
    replace 的第一个参数支持 string 与 RegExp, 第二个参数支持 string 与 function.
    对于 function 参数, 可接收到如下入参:
    - 匹配到的整个字符串
    - 匹配中的捕获, 多个捕获则按顺序获得多个入参
    - 匹配字符串在原字符串的索引
    - 被匹配的原字符串
    function 的返回值则为匹配字符串的替换值.

    1
    2
    3
    4
    'border-bottom-width'.replace(/-(\w)/g, function (all, letter) {
    return letter.toUpperCase();
    });
    // borderBottomWidth
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    function compress(source) {
    const keys = {};
    source.replace(/([^&=]+)=([^&]*)/g, (full, key, value) => {
    keys[key] = (keys[key] ? keys[key] + ',' : '') + value;
    return '';
    })
    const result = [];
    for (let key in keys) {
    result.push(key + '=' + keys[key]);
    }
    return result.join('&');
    }
    console.log(compress('foo=1&foo=2&blah=a&blah=b&foo=3'));
    // foo=1,2,3&blah=a,b

Chapter 11 Code modularization techniques

模块的两个基础需求: 1. 隐藏内部细节; 2. 有对外的接口

通过对象, 闭包, 自执行函数 (IIFE) 实现模块化

通过自执行函数形成一个封闭的模块, 该函数返回一个对象, 函数内部变量通过闭包实现保存.

扩充模块功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 原模块
const MouseCounterModule = function() {
// 点击计数
let numClicks = 0;
const handleClick = () => {
console.log(++numClicks);
}
return {
countClicks: () => {
document.addEventListener('click', handleClick);
}
}
}();
// 模块扩展
(function(module){
// 新功能: 滚动计数
let numScrolls = 0;
const handleScroll = () => {
console.log(++numScrolls);
}
// 扩展到原模块上
module.countScrolls = () => {
document.addEventListener('scroll', handleScroll)
}
})(MouseCounterModule);

* 缺陷: 点击计数与滚动计数的两个功能分别存在于两个闭包中, 互相不能沟通访问内部变量.

AMD 与 CommonJS

区别:
AMD 是依赖于浏览器的模块化方案, 依赖模块异步加载;
CommonJS 主要支持服务端 (node.js), 在客户端需要依赖工具编译, 才能被浏览器识别, 且依赖模块是同步加载.
* CommonJS 编译工具: Browserify, RequireJS

  1. AMD

    1
    2
    3
    define("module ID", ["depend module1 ID", "depend module2 ID", ...], (depend module1 arguments, depend module2 arguments, ...) => {
    // module job
    })

    先异步加载依赖的模块, 待依赖项加载并执行完毕, 再执行本模块, 每个模块依次为一个入参.

  2. CommonJS
    通过文件来分割模块, 通过重写 module.exports 定义模块输出, 通过 require 引入模块

    1
    2
    3
    4
    5
    6
    7
    // module.js
    ... // module job
    module.exports = { ... }; // module interface

    // main.js
    const test = require('module.js'); // import module.js
    ... // use module

    对于引入的模块, 是同步加载资源的, 在服务端即读取文件, 在客户端则需要请求文件, 从而导致阻塞.

* 扩展: Universal Module Definition (UMD) 使一段代码既支持 AMD 也支持 CommonJS.

ES6 modules

特点: 1. 以文件划分模块; 2. 异步加载依赖.

核心语法:

  1. export 定义模块输出

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // 命名输出的方式1: 单独输出 (不可重命名)
    export const test1 = 'test1';
    export function test2() { ... };

    // 命名输出的方式2: 文末统一输出 (可以重命名)
    export { test1 as testConstant, test2 as testFunction }; // 通过 as 可以对输出内容进行重命名

    // 默认输出 (可与上述输出并存)
    export default class Test {
    constructor(name) {
    this.name = name;
    }
    }
  2. import 模块引入

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // 引入某几个命名输出
    import { test1 as testConstant, test2 as testFuntion } from 'module.js';

    // 整个模块内容的引入
    import * as testModule from 'module.js'; // 这里其实就包含了通过 as 重命名的操作, 以及 as 是必须的!
    testModule.test1;
    testModule.test2();

    // 默认输出的引入
    import Name from 'module.js'; // 引入名可随意定义, 不需要保持一致

    // 默认输出与命名输出的一同引入
    import Name, { test1 } from 'module.js';

* ES6 的语法还未被浏览器全支持, 因此需要依靠工具编译.
一些推荐工具: Traceur, Babel, TypeScript

Part 4 Browser reconnaissance

Chapter 12 Working the DOM

  • 将不支持自闭合的标签转换成标准格式

    1
    2
    3
    4
    5
    6
    7
    8
    // 支持自闭合的标签
    const tags = /^(area|base|br|col|embed|hr|img|input|keygen|link|menuitem|meta|param|source|track|wbr)$/i;
    // 将非闭合标签标准化的方法
    const convert = html => html.replace(/(<(\w+)[^>]*)\/>/g, (all, front, tag) => {
    return tags.test(tag) ? all : front + '></' + tag + '>';
    });

    convert('<div class="test"/>'); // <div class="test"></div>
  • DOM 的一些方法属性
    ownerDocument 属性: 一般 DOM 元素的该属性值为 document.
    createDocumentFragment() 方法: 创建一个空 div, 该 div 不在最终的 DOM 树中出现, 但是可以执行插入与克隆等 DOM 操作.
    cloneNode(boolean) 方法: 克隆 DOM, 参数指示是否深度拷贝, 即是否克隆子元素.

  • HTML 元素属性与 DOM 对象属性
    部分原生 HTML 元素属性 (如 id) 与其 DOM 对象属性是关联的, 既可以用 setAttribute 与 getAttribute 进行设置与读取, 也可以用 js 操作对象属性的方式进行设置与读取. 但是需要注意: 两者是关联的关系, 而非用同一个值!
    对于自定义的 HTML 元素属性以及部分原生 HTML 元素属性 (如 class), 只能用 setAttribute 与 getAttribute 进行设置与读取, js 对象属性的操作方式并不支持.
    HTML5 建议用 data- 前缀的方式去自定义 HTML 元素属性, 以和原生属性区分.

    1
    <div id="idTest" class="classTest" data-custom="customTest"></div>
    1
    2
    3
    4
    5
    6
    7
    8
    const test = document.getElementById('idTest');
    console.log(test.getAttribute('id')); // idTest
    console.log(test.id); // idTest
    console.log(test.getAttribute('class')); // classTest
    console.log(test.class); // undefined
    console.log(test.getAttribute('data-custom')); // customTest
    console.log(test['data-custom']); // undefined
    console.log(test.dataCustom); // undefined
  • HTML 元素样式
    行内样式优先级甚至高于 css 样式表中的 !important 优先符.
    行内样式可被 getAttribute 或 DOM 对象的 style 属性获取, 而 css 样式表中的样式则不能.
    可用以下方式获取元素计算样式 (浏览器样式, css 样式与行内样式等的综合计算结果):

    1
    2
    3
    4
    <style>
    div { font-size: 20px; }
    </style>
    <div id="Test" style="color: red;"></div>
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    const fetchComputedStyle = (element, property) => {
    const computedStyles = getComputedStyle(element); // 获取某个元素的计算样式
    if (!computedStyles) return undefined;
    property = property.replace(/([A-Z])/g, '-$1').toLowerCase(); // 将驼峰命名转换成标准的 css 样式名
    return computedStyles.getPropertyValue(property); // 入参需为标准的 css 样式名, 否则查询不到
    };
    const test = document.getElementById('Test');
    console.log(fetchComputedStyle(test, 'color')); // rgb(255, 0, 0) -- 浏览器一般会将各形式颜色统一转成 rgb 形式
    console.log(fetchComputedStyle(test, 'font-size')); // 20px
    console.log(fetchComputedStyle(test, 'fontSize')); // 20px
  • DOM 对象的 offsetHeight 与 offsetWidth 属性
    获取元素的尺寸 (包含元素的 padding).
    当元素 display: none 时, 该元素不在渲染树里, 此时的元素不具有大小, 这两个属性的值都为 0.

  • 布局抖动 layout thrashing
    某些属性方法会强制要求浏览器更新布局, 以保证在最新的布局下获得正确的值. 如果在获取这些属性或者调用方法之间穿插进行 DOM 修改, 则会导致浏览器不断的强制更新布局, 不利于性能优化.
    一些强制浏览器更新布局的方法属性 (来源):

    Interface Property name
    Element clientHeight, clientWidth, clientLeft, clientTop;
    offsetHeight, offsetWidth, offsetLeft, offsetTop, offsetParent;
    getBoundingClientRect, getClientRects;
    innerText, outerText;
    focus;
    scrollByLines, scrollByPages, scrollIntoView, scrollIntoViewIfNeeded;
    scrollHeight, scrollWidth, scrollLeft, scrollTop
    MouseEvent layerX, layerY, offsetX, offsetY
    Window getComputedStyle, scrollBy, scrollTo, scroll, scrollY
    Frame, Document, Image height, width
    一些优化布局抖动的工具: FastDom, React 虚拟 DOM

Chapter 13 Surviving events

事件循环 event loop

浏览器响应的事件可分为: 宏任务 (macrotask) 与 微任务 (microtask), 对应存储于 宏队列 (macrotask queue) 与 微队列 (microtask queue). 宏任务主要有: 鼠标操作, 键盘操作, 网络响应, HTML构建等; 微任务主要有: DOM 变化, Promise 等.

事件循环步骤:
Step1 浏览器检查宏队列内是否有任务, 若有任务, 则取出一项执行; (对于宏队列任务, 最多执行一项)
Step2 浏览器检查微队列内是否有任务, 若有任务, 则依次取出执行, 直到微队列为空; (对于微队列任务, 全部执行)
Step3 确认以上步骤执行完成后是否需要更新 DOM, 若需要, 则更新;
Step4 完成一次循环, 返回 Step1.

事件处理的特征:
1. 一次只执行一个任务;
2. 该任务不会被别的任务打断, 除非浏览器判断运行时间过长或占用内存过大.

浏览器的刷新频率是每秒 60 帧 (60 fps), 约 16 ms 更新一次 DOM. 若 16ms 内微任务没有执行完成, 浏览器则跳过此次更新, UI 不会更新. 因此, 若任务执行时间过长, 则会导致页面长时间无变化, 造成无响应的状态.

* 宏队列与微队列只是一个分类概念, 浏览器具体实现时, 可能会创建更多的队列以存储不同类型的事件, 从而在取出宏任务时, 可以有优先级的区分.

* 事件的捕获与任务的添加不在事件循环内, 是及时响应添加到对应队列里的.

定时器 timer

与事件循环一样, 定时器并不是 javaScript 定义的方法, 而是浏览器或 Node.js 提供的 API:

1
2
3
4
5
6
// 延时
timerId = setTimeout(callbackFunction, delayTime);
clearTimeout(timerId);
// 循环
timerId = setInterval(callbackFunction, intervalTime);
clearInterval(timerId);

定时器设置的时间只是一个理想值, 并不是准确的代码执行时间. 当设置时间到达时, 对应的回调函数会被推入宏队列中等待事件循环取出执行.
对于 setInterval, 当一次循环时间到达, 只有当前宏队列中没有这个定时器推入的等待执行的回调函数时 (当前正在执行的任务类型不影响), 才会推入一个新的回调, 否则就跳过这次循环. 即, 对于循环定时器, 在宏队列中, 同一时间只能有一个该定时器的回调函数在等待执行.

定时器可用于优化运行时间过长的代码, 防止阻塞页面响应用户行为:

1
<table><tbody></tbody></table>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 运行时间过长的代码
const tbody = document.querySelector('tbody');
for (let i = 0; i < 20000; i++>) {
const tr = document.createElement('tr');
for (let t = 0; t < 6; t++) {
const td = document.createElement('td');
td.appendChild(document.createTextNode(i + ', ' + t));
tr.appendChild(td);
}
tbody.appendChild(tr);
}

// 运用定时器优化
const rowCount = 20000;
const divideInto = 10;
const chunkSize = rowCount / divideInto;
let iteration = 0;

const tbody = document.querySelector('tbody');
const generateRows = () => {
const base = chunkSize * iteration;
for (let i = 0; i < chunkSize; i++) {
const tr = document.createElement('tr');
for (let t = 0; t < 6; t++) {
const td = document.createElement('td');
td.appendChild(document.createTextNode((i + base) + ', ' + t));
tr.appendChild(td);
}
tbody.appendChild(tr);
}
iteration++;
if (iteration < divideInto) {
setTimeout(generateRows, 0);
}
}

setTimeout(generateRows, 0);

事件捕获与事件冒泡

浏览器事件处理的两个阶段:

  1. 事件捕获阶段 (Capturing phase)
    从 DOM 树的顶端追踪到事件触发元素
  2. 事件冒泡阶段 (Bubbling phase)
    从事件触发元素回溯到 DOM 树顶端

应用: 将重复的子元素事件直接委托在祖先元素上统一处理, 通过 event.target 确认是目标子元素触发 (event delegation).

addEventListener

element.addEventListener(event name, callback function, whether capture)

  • element
    绑定监听的元素, 如: document, 具体的某个 DOM 元素.
  • event name
    监听的事件类型, 如: click, scroll, …
  • callback function
    事件触发时执行的函数.
    当函数非箭头函数时, 函数体内的 this 指向注册事件监听的元素 (即 element), 而非触发事件的元素.
  • whether capture
    布尔值, 默认为 false. 标记回调函数在捕获阶段 (true) 还是冒泡阶段 (false or default) 执行.
1
2
3
4
5
6
7
<html>
<body>
<div id="outerContainer">
<div id="innerContainer"></div>
</div>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
const outerContainer = document.getElementById('outerContainer');
const innerContainer = document.getElementById('innerContainer');

innerContainer.addEventListener('click', function(event) {
console.log('innerContainer bubble'); // 点击 innerContainer 时第 3 个触发
console.log(this); // innerContainer DOM
console.log(event.target); // innerContainer DOM
});

innerContainer.addEventListener('click', (event) => {
console.log('innerContainer capture'); // 点击 innerContainer 时第 4 个触发
console.log(this); // window
console.log(event.target); // innerContainer DOM
}, true);

outerContainer.addEventListener('click', (event) => {
console.log('outerContainer bubble'); // 点击 innerContainer 时第 5 个触发
console.log(this); // window
console.log(event.target); // innerContainer DOM
});

outerContainer.addEventListener('click', function(event) {
console.log('outerContainer capture'); // 点击 innerContainer 时第 2 个触发
console.log(this); // outerContainer DOM
console.log(event.target); // innerContainer DOM
}, true);

document.addEventListener('click', function(event) {
console.log('document bubble'); // 点击 innerContainer 时第 6 个触发
console.log(this); // document DOM
console.log(event.target); // innerContainer DOM
});

document.addEventListener('click', function(event) {
console.log('document capture'); // 点击 innerContainer 时第 1 个触发
console.log(this); // document DOM
console.log(event.target); // innerContainer DOM
}, true);

* 对于触发事件的元素, 是先冒泡后捕获, 这点表现与前面不一致, 原因未知. (实验浏览器: Chrome 86)

自定义事件

```js
// 自定义事件并触发
funtion triggerEvent(target, eventType, eventDetail) {
const event = new CustomEvent(eventType, {detail: eventDetail});
target.dispatchEvent(event);
}

// 使用示例