Skip to content

Javascript 红宝书(上)

x

一、什么是 Javascript

核心 ECMAScript

  • Web 浏览器只是 ECMAScript 实现可能存在的一种宿主环境。(还有 Node.js/Flash 等)
  • 宿主环境提供 ECMAScript 的基准实现和与环境自身交互必需的扩展。

DOM

  • DOM 是一个API,用于在 HTML 中使用扩展的XML。
  • 将页面抽象成一组分层的节点。
  • 提供与网页内容交互的方法和接口。

BOM

  • BOM API,提供访问/操作浏览器的方法和接口。
  • HTML 5 规范了 BOM 的特性

二、HTML 与 Javascript

<script> 元素

  • 脚本属性

    • crossorigin

      • crossorigin=“anonymous”: 文件请求无需凭据
      • crossorigin=“use-credentials”: 出站请求包含凭据
    • async 或 defer

  • 行内脚本与外部脚本

    • 行内脚本

      • 从上到下解释,阻塞页面解析
      • 不能出现 </script> 字符串,需要转义
    • 外部脚本

      • 同样阻塞页面解析,阻塞时间包含文件加载时间
    • 推荐使用外部文件

      • 可维护性
      • 缓存
      • 适应未来
  • 标签位置

    • 推荐放在页面尾部,空白页面时间较短
  • 推迟执行 defer

    • 设置 defer 属性,相当于告诉浏览器立即下载,但延迟执行。
    • 浏览器解析到结束标签 </html>之后,才执行。
    • 第一个 defer 脚本会在第二个 defer 之前执行;两者都会在 DOMContentLoaded事件之前执行。
  • 异步执行 async

    • 告诉浏览器,不必等脚本加载和执行完后再加载页面
    • 异步脚本不应该在加载期间修改 DOM
    • 第二个脚本可能先于第一个脚本执行
  • 动态加载

    • document.createElement('script')
    • 为避免影响性能,需要在文档头部显示声明,让浏览器预加载器知道。<link ref="preload" href="gibberish.js">

<noscript> 标签

  • 在不支持JavaScript的浏览器中显示替代内容。

三、语言基础

语法

  • 严格模式

    • ECMAScript 5 增加严格模式(strict mode)
    • ECMAScript 3 中不规范的写法/不安全的活动会抛出异常。
    • "use strict";

变量

  • var

    • 声明 函数作用域
    • var 声明提升(hoist)
    • 不推荐改变变量保存值的类型
  • let

    • 声明 块级作用域
    • 暂时性死区(temporal dead zone):声明前执行,抛出 ReferenceError
    • 全局声明:let 在全局作用域中声明的变量不会成为 window 对象的属性
    • for 循环中的 let 声明:let 出现之前,for 循环定义的迭代变量会渗透到循环体外部。
  • const

    • const 声明可以让浏览器运行时强制保持不变,也可以让静态代码分析工具提前发现不合法的赋值操作。
  • 声明风格及最佳实践

    • 不使用 var
    • const 优先,let 次之

数据类型

  • 简单数据类型

    • Undefined
    • Null
    • Boolean
    • Number

      • 八进制
      077; // 八进制的63
      078; // 无效的八进制,当成78处理
      0o77; // 严格模式下的八进制表示
      
      • 十六进制 (比如 0x1f)
      • 浮点值

        • 浮点值的精确度可高达17位小数。
        • 0.1 + 0.2 = 0.30000000000000004
        • 舍入错误:计算机的二进制实现和位数限制有些数无法有限表示。因为 JS 遵循 IEEE 754 数值规范,除了 javascript,其他相同格式语言也会有此问题。
      • NaN

      console.log(0/0); // NaN
      console.log(1/0); // Infinity
      isNaN(): 表示是否不能转换为数值
      
      • 数值转换:Number() 、parseInt() 和 parseFloat()
    • String

      • 模版字面量标签函数(tag function)
      function simpleTag(strings, ...expressions) {
        console.log(strings);
        console.log(expressions);
      }
      
      simpleTag`${a} + ${b} = ${a+b}`;
      // strings: ["", " + ", " = ", "", raw: Array(4)]
      // expressions: [6, 9, 15]
      
      • 原始字符串(String.raw)
      /* 表示不希望被转义 * /
      console.log(`\u03a3`); // "Σ"
      console.log(String.raw`\u03a3`); // "\u03a3"
      
    • Symbol

      • 不可变唯一实例
      Symbol('foo') === Symbol('foo'); // false
      Symbol.for('foo') === Symbol.for('foo'); // true
      
      • Symbol.asyncIterator

        • 方法,返回值为对象的异步迭代器 AsyncIterator
        • 一个异步可迭代对象必须要有 Symbol.asyncIterator 属性
        • 可用于for await...of循环
        • 示例
        const myAsyncIterable = new Object();
        
        myAsyncIterable[Symbol.asyncIterator] = async function*() {
          yield "hello";
          yield new Promise(r => r("async"));
          yield "iteration!";
        };
        
        // 执行
        (async () => {
          for await (const x of myAsyncIterable) {
            console.log(x);
            // expected output:
            //    "hello"
            //    "async"
            //    "iteration!"
          }
        })();
        
      • Symbol.iterator

        • 方法,返回值为对象的迭代器
        • 可用于for...of循环
        • 示例
        var myIterable = {}
        myIterable[Symbol.iterator] = function* () {
            yield 1;
            yield 2;
            yield 3;
        };
        [...myIterable] // [1, 2, 3]
        
      • Symbol.hasInstance

        • 决定一个构造器对象是否认可一个对象是它的实例。
        • 由 instanceof 操作符使用
        • 示例
        class A {
          static [Symbol.hasInstance](instance) {
            return false;
          }
        }
        
        let a = new A();
        a instanceof A; // false
        
      • Symbol.isConcatSpreadable

        • 决定一个数组的元素是否可以被打平。
        • Array.prototype.concat使用
        • 示例
        let a = [2];
        [1].concat(a); //  [1, 2]
        
        a[Symbol.isConcatSpreadable] = false
        [1].concat(a); //  [1, Array(1)]
        
      • Symbol.match

        • String.prototype.match()方法会调用此函数
      • Symbol.replace

        • 当一个字符串替换所匹配字符串时所调用的方法
        • String.prototype.replace() 方法会调用此方法。
      • Symbol.search

        • 接受用户输入的正则表达式,返回该正则表达式在字符串中匹配到的下标
        • String.prototype.search()
      • Symbol.split

        • 通过 String.prototype.split() 调用
      • Symbol.toStringTag

        • 作为对象的属性键使用,对应的属性值(字符串类型)用来表示该对象的自定义类型标签。
        • Object.prototype.toString() 方法会去读取这个标签并把它包含在自己的返回值里。
        • 代码
        class ValidatorClass {
          get [Symbol.toStringTag]() {
            return "Validator";
          }
        }
        
        Object.prototype.toString.call(new ValidatorClass());
        // "[object Validator]"
        
      • Symbol.toPrimitive

        • 当一个对象转换为对应的原始值时,会调用此函数。
      • Symbol.species

        • 属性,其被构造函数用以创建派生对象,用于允许子类覆盖对象的默认构造函数。
        • 代码
        class MyArray extends Array {
          // 覆盖 species 到父级的 Array 构造函数上
          static get [Symbol.species]() {
            return Array;
          }
        }
        var a = new MyArray(1, 2, 3);
        var mapped = a.map((x) => x * x);
        
        console.log(mapped instanceof MyArray); // false
        console.log(mapped instanceof Array); // true
        
  • 复杂数据类型(Object)

    • Object类型即一种无序名对值的集合
    • constructor
    • hasOwnProperty: 判断非原型属性
    • isPrototypeof
    • propertyIsEnumerable: 属性是否可使用
    • toLocaleString
    • toString: 返回对象的字符串表示
    • valueOf: 返回对象的字符串、数值或布尔值表示。
  • typeof 操作符

    • "undefined"
    • "boolean"
    • "number"
    • "string"
    • "symbol"
    • "object":非函数对象 或 null
    • "function":函数对象

流控制语句

  • 标签语句

    • label: statement
    • 可通过 break 或 continue 引用
  • break 和 continue 语句

    • break 强制执行循环之后下一条语句
    • continue 从循环顶部开始执行
  • with 语句

    • 将代码 作用域 设置为一个 对象

理解函数

四、变量、作用域与内存

原始值与引用值

执行上下文与作用域

  • 执行上下文(作用域)

    • 任何变量都存在于某个执行上下文中(即作用域)。这个上下文决定了变量的生命周期,以及它们可以访问代码的哪些部分。
    • 执行上下文分 全局上下文函数上下文块级上下文
    • 每个上下文都有一个关联的 变量对象(Variable Object),上下文中定义的所有变量和函数都存在这个对象上。
    • 如果上下文是函数,则其 活动对象(Activation Object) 作为变量对象;活动对象最初只有一个定义变量:arguments
    • 如果是全局上下文,其活动对象叫 全局对象(Global Object)
  • 执行流

    • 代码执行流进入函数时,函数的上下文推入 上下文栈。函数执行完后,上下文栈会弹出该函数上下文,将控制权返还给之前的执行上下文。
  • 作用域链

    • 代码执行流每进入一个新上下文,都会创建 变量对象 的一个 作用域链,用于搜索变量和函数。
    • 代码正在执行的上下文的变量对象 始终位于作用域链的最前端
    • 代码执行时在 标识符(即变量名)解析 时,通过沿作用域链,从前往后逐级搜索标识符合名称完成的。直到找到标识符(若找不到通常会报错:ReferenceError: x is not defined)。
    • 内部上下文可以通过作用域链访问外部上下文中的一切,但外部上下文无法访问内部上下文中的任何东西。 上下文之间的连接是线性的、有序的。
  • 作用域链增强

    • 某些语句会导致在作用域链前端,临时添加一个上下文,执行完后被删除。
    • try/catch 语句的 catch 块
    • with 语句
  • 变量声明

    • var 声明:与变量提升
    • const 声明:暗示变量值类型单一且不可变,v8 引擎对此优化,在 javascript 运行时编译器将其替换成实际值
    • 标识符查找:访问局部变量比访问全局变量快,因为不必切换作用域。

垃圾回收

  • 标记清理(mark-and-sweep)

    • 当变量进入上下文时,加上存在于上下文的标记;离开时,也会被加上离开上下文的标记。
    • 回收时,将上下文中或被上下文引用的变量的标记去掉,剩下带标记的都待删除。垃圾回收程序做一次内存清理,销毁带标记的所有值,收回内存。

    x

  • 引用计数(reference counting)

    • 对每个值都记录他被引用的次数。
    • 声明变量并赋引用值时,这个值引用数为 1; 值被赋给另一个变量,计数加 1; 该引用值的变量被其他值覆盖,计数减 1。
    • 循环引用的问题

      • 两个变量相互引用,它们的引用数永远不会变成 0;
      • 变量设置为null,切断变量与其引用值之间联系,便于回收。
  • 性能考量

    • 内存有限时,垃圾回收时可能明显拖慢渲染速度和帧速率。
    • 写代码:无论什么时候开始收集垃圾,都能让它尽快结束工作。
  • 内存优化

    • 通过 const/let 提升性能

      • 以块为作用域,可以更早的让垃圾回收程序介入。
    • 解除引用

      • 优化内存的原则是保证在执行代码时只保存必要的数据。
      • 如果数据不再需要,设置为 null,从而释放其引用。
    • 隐藏类和删除操作

      • 避免「先创建再补充(ready-fire-aim)」式的动态属性赋值,而应在构造函数内一次性声明所有属性。
      • 避免动态删除属性,最好把不用的属性设置为 null。
      • 以上都会导致同一个构造函数的实例,不再共享一个隐藏类。
    • 内存泄漏

      • 意外声明全局变量
      • 定时器回调函数引用的外部变量不释放
      • 闭包使用不当
    • 静态分配与对象池

五、基本引用类型

Date

RegExp

  • let expression = /pattern/flags;
  • 模式:pattern

    • 字符类
    • 限定符
    • 分组
    • 向前查找
    • 反向引用
  • 选项:flags

    • g: 全局模式
    • i: 不区分大小写
    • m: 多行模式
    • y: 粘附模式
    • u: Unicode 模式
    • s: dotAll 模式
  • 实例属性

    • 返回布尔类型:global/ignoreCase/unicode/sticky/multiline
    • source/flags
  • 实例方法

    • exec()

      • 若匹配,返回数组;否则,返回 null。
      • 数组中第一个元素是匹配整个模式的字符串,其他元素是与表达式中捕获组匹配的字符串。
      • 如果模式设置全局标记g,则每次调用 exec() 返回匹配信息,并且指针后移;否则不设置全局,只返回第一个匹配信息。
      • 如果模式设置全局标记y,则每次调用 exec()只会在 lastIndex 的位置上寻找你匹配项。
    • test()

      • 用法与 exec() 区别仅在于返回值为bool值。
  • 构造函数属性

    • input: 最后搜索的字符串($_)
    • lastMatch: 最后匹配的文本($&)
    • lastParen:最后匹配的捕获组($+)
    • leftContext:input 字符串出现在 lastMatch 前面的文本($`)
    • rightContext:input 字符串出现在 lastMatch 后面的文本($')

原始包装类型

  • 概念:Boolean/Number/String 类型,既有其他 引用类型 的特点,也有各自 原始类型 对应的特殊行为。
  • Boolean

    • 实例会重写 valueOf() 方法,返回原始值 true 或 false。
    • new Boolean(false) && true; // true
  • Number

    • 重写 valueOf()方法,返回原始数值;重写 toString()方法,接受基数参数,返回基数形式的字符串;
    • toFixed/toPrecision/toExponential
    • Number.isInteger(1.0)
  • String

    • Javascript 字符

      • str.charCodeAt()
      • String.fromCharCode(65, 666)
      • String.fromCodePoint(0x1F60A):码点是 Unicode 中一个字符完整的标识。16 位或 32位。
    • normalize() 方法

    • 字符串操作方法

      • concat/slice/substr
    • 字符串位置方法

      • indexOf/lastIndexOf
    • 字符串包含方法

      • startsWith/endsWith/includes
    • trim/repeat/padStart/padEnd

    • 模式匹配

      • str.match() 与 reg.exec() 相同
      • str.search() 返回第一个匹配索引位置
      • str.replace()
      var text = 'cat, rat';
      text.replace(/(.at)/g, 'word($1)'); // "word(cat), word(rat)"
      
    • localCompare: 比较字符串,并返回 1/0/-1。

单例内置对象

  • Global:浏览器将 window 对象实现为 Global 对象的代理。
  • Math

六、集合引用类型

Object

数组(Array)

  • Array.from

    • 类数组结构 转换为数组实例
  • Array.of

    • 一组参数 转换为数组实例
  • 检测数组

    • value instanceof Array
    • Array.isArray(value)
  • 迭代器方法

    • Array.prototype.keys(): 返回索引迭代器
    • Array.prototype.values(): 返回元素迭代器
    • Array.prototype.entries(): 返回索引/值对迭代器
    • 示例:for(let [idx, elment] of arr.entries()) {...}
  • 填充数组

    • Array.prototype.fill()
    • Array.prototype.copyWithin()
  • 转换方法

    • valueOf():arr.valueOf() === arr; // true
    • toString():[1, {}, 'a', [2, [3]]].toString(); // "1,[object Object],a,2,3"
    • join():[1, {}, 'a', [2, [3, 4]]].join('#'); // "1#[object Object]#a#2,3,4"
  • 栈和队列

    • push/pop
    • shift/unshift
  • 排序(sort/reverse)

    • sort 默认将元素 String() 后再排序
    • 注意:返回调用它们数组的引用(原数组)。
  • 操作

    • concat():返回新数组
    • slice():返回新数组
    • splice():删除、插入、替换,返回新数组
  • 搜索

    • 严格相等:indexOf/lastIndexOf/includes
    • 断言函数:find/findIndex
  • 迭代和归并

    • every/filter/forEach/map/sort
    • reduce/reduceRight

定型数组(Typed Array)

x

  • ArrayBuffer

    • ArrayBuffer 对象代表存储二进制数据的一段内存,是一个字节数组。
    • ArrayBuffer 是所有 定型数组视图 引用的 基本单位
    • ArrayBuffer 也是一个构造函数,连续的内存区域,参数是内存大小(单位字节)。const buf = new ArrayBuffer(32);
    • ArrayBuffer 一经创建就不能再调整大小。
    • 要读取或写入 ArrayBuffer,必须通过 “视图”
  • Typed Array

    • ArrayBuffer 的一种视图。
    const buf = new ArrayBuffer(12);
    const ins = new Int32Array(buf);
    ins.buffer.byteLength; // 12
    ins.length; // 3
    
    • 定型数组行为:类似普通数组,可使用 every/find/reduce/map/slice/values 等。
    • 合并、复制和修改

      • 无法调整大小。
      • 不支持 pop/push/splice等
      • 向内复制 set()
      • 向外复制()
    • 上溢和下溢

  • DataView

    • 另一种允许读写 ArrayBuffer 的视图。
    • new DataView(ArrayBuffer buffer [, 字节起始位置 [, 长度]]);
    • 支持设置字节序。

      • 什么是字节序?字节序是数值在内存中的存储方式。分为小端字节序(little-endian)和大端字节序(big-endian)两种:比如个人电脑基本都是小端字节序,而互联网标准通常要求数据使用 big-endian 存储。
      • TypedArray 中,字节序会跟随系统的字节序,于是基本都是小端字节序,是不支持自己设置的,于是就会带来一个问题:如果从网络请求来的数据是大端字节序,会导致数据无法解析。
      • 相比之下,DataView 可以支持设置字节序。

Map(顺序与迭代)

  • 与 Object 类型最主要的区别:m[Symbol.iterator] === m.entries; // true

WeakMap

  • API

    • 弱映射,键只能是 Object 类型;否则,抛出 TypeError。
  • 弱键

    • 键,不属于正式引用,不会阻止垃圾回收;
    • 值,只要键存在,就不会被垃圾回收。
  • 不可迭代键

  • 用于存储:私有变量

    • 以实例对象为键,以私有成员的字典为值。
  • 用于存储:DOM 节点元数据

    • WeakMap 实例不会妨碍垃圾回收,适合保存关联元数据。
    • m.set(dom,{ a: 1 }); // 若从 DOM 树删除,dom 仍在内存中。
    • wm.set(dom,{ a: 1 }); // 若删除,垃圾回收程序立即释放其内存

Set

  • 顺序与迭代

    • s[Symbol.iterator] === s.entries; // true
  • 保留插入顺序

  • 定义集合操作

    • union/intersection/difference

WeakSet

  • API

    • 弱集合,值只能是 Object 类型
  • 不可迭代值

  • 用于存储:DOM 节点

    • Set() 存储:若从 DOM 树删除,dom 仍在内存中。
    • WeakSet() 存储:若删除,垃圾回收程序立即释放其内存。

迭代与扩展

七、迭代器与生成器

迭代器

  • 可迭代协议:即实现 Iterable 接口的能力,支持迭代的自我识别能力和创建实现 Iterator 接口的对象的能力。
  • 内置迭代类型:字符串/数组/Map/Set/arguments对象/NodeList
  • 原生语言的迭代器特性

    • for-of 循环
    • 数组解构
    • 扩展符操作
    • Arrary.from()
    • 创建集合
    • 创建映射
    • Promise.all() 接收
    • Promise.race() 接收
    • yield* 操作符
  • 自定义迭代器

提前终止迭代器

  • 迭代器实例的 return() 方法,可选。

生成器

  • function * generatorFn() {}
  • 只要可定义函数的地方,就可以定义生产器。
  • 调用生成器函数会返回一个 生成器对象,实现了 Iterator 接口。
  • 生成器对象 一开始处于 suspened 状态。
  • 生成器函数只有在初次调用 next() 后才开始执行
  • g === g[Symbol.iterator](); // true

yield

  • yield 中断执行

    • 生成器函数在遇到 yield 关键字后,执行会停止,函数作用域的状态会被保留。
    • 停止执行的生成器函数,只能通过生产器对象调用 next() 方法恢复。
  • 实现输入和输出

    • 上一次让生成器函数暂停的 yield 关键字会接收给 next() 方法的第一个值。
    • 第一次 next() 传参不会被使用,因为这次调用只为开始执行生成器。
    • 代码
    function* generatorFn () {
      return yield 'x';
    }
    
    let it = generatorFn();
    
    it.next(); // {value: "x", done: false}
    it.next('y'); // {value: "y", done: true}
    
  • 产生可迭代对象

    • 使用星号增强 yield 行为,让它能迭代一个可迭代对象。
    • 代码
    function* generatorFn () {
      yield* [1,2,3];
    }
    
    for(let x of generatorFn()) {console.log(x);}
    // 1
    // 2
    // 3
    
    • yield * 实际上只是 将一个可迭代对象,序列化为一连串可单独产出的值
  • 使用 yield * 实现递归算法

    • 案例一
    function * nTimes(n) {
      if (n > 0) {
          yield * nTimes(n-1);
          yield n - 1;
      }
    }
    
    for(let x of nTimes(3)) {console.log(x);}
    // 0
    // 1
    // 2
    
    • 案例二:图连通算法
    class Node {
      constructor(id) {
       this.id = id;
          this.neighbors = new Set();
      }
    
      connect(node) {
          if (node !== this) {
              this.neighbors.add(node);
              node.neighbors.add(this);
          }
      }
    }
    
    class RandomGraph {
      constructor(size) {
       this.nodes = new Set();
    
          // 创建节点
          for(let i = 0; i < size; i++) {
              this.nodes.add(new Node(i));
          }
    
          // 随机连接节点
          const threshhold = 1 / size;
          for(const x of this.nodes) {
              for(const y of this.nodes) {
                  if (Math.random() < threshhold) {
                      x.connect(y);
                  }
              }
          }
      }
    
      isConnected() {
          const visitedNodes = new Set();
    
          // 深度优先遍历
          function * traverse(nodes) {
              for(const node of nodes) {
                  if (!visitedNodes.has(node)) {
                      console.log('visiting ', node.id);
                      yield node;
                      yield * traverse(node.neighbors);
                  }
              }
          }
    
          // 取得集合中的第一个节点
          const firstNode = this.nodes[Symbol.iterator]().next().value;
    
          // 使用递归生成器迭代每一个节点
          for(const node of traverse([firstNode])) {
              visitedNodes.add(node);
          }
    
          debugger;
          return visitedNodes.size === this.nodes.size;
      }
    
      print() {
          for(const node of this.nodes) {
              const ids = [...node.neighbors]
                  .map(n => n.id)
                  .join(',');
    
              console.log(`${node.id}: ${ids}`);
          }
      }
    }
    
    // 示例
    const g = new RandomGraph(6);
    
    g.print();
    g.isConnected();
    

生成器作为默认迭代器

class Foo {
  constructor() {
    this.values = [1, 2, 3];
  }

  *[Symbol.iterator]() {
    yield * this.values;
   }
}

for(let x of new Foo()) {console.log(x);}
// 1
// 2
// 3

提前终止生成器

  • 一个实现 Iterator 接口的对象一定有 next() 方法,还有可选的 return() 方法用于提前终止迭代器。
  • 生成器另有一个 throw() 方法。
  • 与迭代器不同,生成器的 return() 方法关闭后,无法恢复。
  • throw():会在暂停时注入错误,若未被处理,则生成器关闭;若处理,则可恢复执行。

八、对象、类与面向对象编程

理解对象

  • 定义:一组属性的无序集合
  • 属性的类型

    • 数据属性:数据属性 包含一个保存数值的位置。4个特性:

      • [[Configurable]]
      • [[Enumberable]]
      • [[Writable]]
      • [[Value]]
    • 访问器属性:访问器属性 不包含数据值,且必须使用 Object.defineProperty() 定义。4个特性:

      • [[Configurable]]
      • [[Enumberable]]
      • [[Get]]
      • [[Set]]
  • 定义多个属性

    • Object.defineProperties()
  • 读取属性的特性

    • Object.getOwnPropertyDescriptor()
    • 新增 Object.getOwnPropertyDescriptors()
  • 对象合并

    • 浅拷贝 Object.assign()
  • 相等判定

    • +0 === -0; // true
    • NaN === NaN; // false
    • Object.is(+0, -0); // false
    • Object.is(NaN, NaN); // true
  • 对象解构

    • let personCopy = {}; ({ name: personCopy.name, age: personCopy.age } = person);

理解创建对象

  • 工厂模式:createPerson()
  • 构造函数模式:new Person()

    • new 操作符的过程:
    • 1、在内存中创建一个对象
    • 2、这个新对象内部的 [[Prototype]] 属性(即 __proto__)被赋值为构造函数的 prototype 属性
    • 3、构造函数内部的 this 被赋值为这个新对象
    • 4、执行构造函数内部代码
    • 5、构造函数返回某个非空对象;或者,返回刚创建的新对象。
  • 原型模式

    • 每个函数都会创建一个 prototype 属性,即原型对象。原型对象的 constructor 属性,指回与之关联的构造函数。
    • 实例对象内部 [[Prototype]] 指针(即 __proto__ 属性)被赋值为构造函数的原型对象。
    • Object.create() 创建新对象,并指定其原型。
    • Object.prototype.__proto__ === null; // true
    • 原型层级

      • person.hasOwnProperty('name'); // 实例属性 返回 true
      • ‘name’ in person; // 不论是实例属性,还是原型属性,都返回 true。
      • for-in: 访问对象中可被枚举的 实例属性和原型属性
  • 对象迭代

    • Object.entries()
    • Object.values()

理解继承

  • 继承的概念

    • 面向对象语言支持两种继承:接口继承和实现继承。
    • ECMAScript 只支持实现继承,通过原型链。
  • 原型链继承

    • SubType.prototype = new SuperType();
  • 盗用构造函数(constructor stealing) 继承

    • Why: 又称 “对象伪装” 或 “经典继承”,解决原型包含引用导致的继承问题。
    • How: 在子类构造函数中调用父类构造函数。
  • 组合继承

    • 也称“伪经典继承”,综合了 原型链盗用构造函数
    • 通过原型链继承原型上的属性和方法;通过盗用构造函数继承实例属性。
  • 原型式继承

    • function object(o) { function F() {} F.prototype = o; return new F(); }
    • Object.create() 将原型式继承的概念规范化。
  • 寄生式继承

    • 思想:寄生构造函数和工厂模式。
    • How: 创建一个实现继承的函数,以某种方式增强对象,返回这个对象。
  • 寄生式组合继承

    • Why: 组合继承存在效率问题,父类构造函数始终会被调用两次。
    • 寄生式组合继承通过 盗用构造函数 继承 属性,通过 混合式原型链 继承 方法
    • 核心逻辑: inheritPrototype
    function inheritPrototype(subType, superType) {
      let prototype = object(superType.prototype);   // 创建对象
      subType.prototype = prototype;    // 赋值对象
      prototype.constructor = subType;   // 增强对象
    } 
    
    // 或
    
    // 实现继承的核心函数
    function inheritPrototype(subType,superType) {
      function F() {};
      // F()的原型指向的是superType
      F.prototype = superType.prototype; 
      // subType的原型指向的是F()
      subType.prototype = new F(); 
      // 重新将构造函数指向自己,修正构造函数
      subType.prototype.constructor = subType; 
    }
    

理解类

  • 构造函数
  • 实例,原型和类成员

    • 支持迭代器与生成器方法
  • 用于继承

    • 继承基础

      • 静态类方法和原型方法都会带到派生类上。
    • 构造函数,HomeObject 和 super()

      • super 只能在派生类构造函数和静态方法中使用。
    • 抽象基类

      • 可供继承,但本身不会被实例化。
      • 通过 new.target 实现
    • 继承内置类型

    • 类混入

      • 混入模式可以在通过在一个表达式中连缀多个元素来实现,此表达式最终会被解析为一个可以继承的类。
      • 定义一组 “可嵌套“ 的函数,函数接收超类为参数,混入类为该参数的子类,返回混入类。
      • 很多JS 框架抛弃混入模式,转向组合模式。符合设计原则:“复合胜过继承”。

九、代理与反射

代理基础

  • 代理是目标对象 (target) 的抽象。
  • 创建空代理

    • 默认情况下,在代理对象的执行的所有操作都会无障碍的传播到目标对象。
  • 定义捕获器 (trap)

    • 捕获器是处理程序对象(handler) 中定义的 “基本操作的拦截器”。
  • 捕获器参数和 Reflect API

    • const proxy = new Proxy(target, Reflect);
  • 可撤销代理

    • const { proxy, revoke } = Proxy.revocable(target, Reflect);
    • 撤销代理之后再调用代理会抛出 TypeError。
  • 实用反射 API

    • Reflect API 与 Object API
    • 状态标记

      • Reflect 方法返回的布尔值称为 “状态标记”。
    • 用一等函数代替操作符

      • Reflect.get(): 可替代对象属性访问操作符。
      • Reflect.set(): 可替代赋值操作符。
      • Reflect.has(): 可替代赋值 in 操作符。
      • Reflect.deleteProperty(): 可替代 delete 操作符。
      • Reflect.construct(): 可替代 new 操作符。
    • 安全地 apply 函数

      • Function.prototype.apply.call(myFunc, thisVal, argList);
      • Reflect.apply(myFunc, thisVal, argList);
  • 代理的问题与不足

    • 代理中的 this
    • 代理与内部槽位

代理捕获器与反射方法

  • get()
  • set()
  • has()
  • defineProperty()
  • getOwnPropertyDescriptor()
  • deleteProperty()
  • ownKeys()
  • getPrototypeOf()
  • setPrototypeOf()
  • isExtensible()
  • preventExtensions()
  • apply()
  • construct()

代理模式

  • 跟踪属性访问
  • 隐藏属性
  • 属性验证
  • 函数与构造函数参数验证
  • 数据绑定与可观察对象

十、函数

箭头函数

  • 箭头函数不能使用 arguments, super 和 new.target。
  • 此外,箭头函数没有 prototype 属性。

函数名

  • 如果是 get 函数,set 函数,或使用bind() 实例化,函数名前面会加上一个前缀。

函数声明与函数表达式

  • Javascript 引擎在任何代码执行前,会先读取函数声明,并在执行上下文中生成函数定义。
  • 函数表达式必须等代码执行到它那一行,才生成函数定义

函数内部

  • arguments

    • 类数组对象
    • arguments.callee 是一个指向 arguments 对象所在函数的指针。
  • this

    • 标准函数中,this 引用的是把函数当成方法调用的上下文对象。
    • 箭头函数中,this 引用的是定义箭头函数的上下文。
  • new.target

    • 引用被调用的构造函数。
  • caller

    • arguments.callee.caller: 引用的是调用当前函数的函数,在全局作用域中为null。

函数属性与方法

  • length 属性

    • 函数定义的命名参数的个数。
  • prototype 属性

  • call()/apply() 方法

    • 以指定的 this 值来调用函数。
  • bind() 方法

    • 创建一个新的函数实例,其 this 值被绑定到指定对象。

尾调用机制

  • ES6 新增的一项内存管理优化机制。
  • 条件

    • 严格模式下执行
    • 外部函数的返回值是对尾调用函数的调用
    • 尾调用函数返回后不需要执行额外的逻辑
    • 尾调用函数不是引用外部函数作用域变量的闭包
  • 此优化在递归场景下效果明显。

闭包

  • 作用域链

    • 在定义函数时,就会为它创建作用域链,预装载全局变量对象,并保存在内部的 [[Scope]] 中。
    • 在调用函数时,会创建相应的执行上下文。然后通过复制函数的 [[Scope]] 来创建其作用域链
    • 接着创建函数的活动对象,推入作用域链首。
  • 闭包生命周期

    • 外部函数执行完,其执行上下文的作用域链会销毁,但它的 活动对象仍存在于内存;直到内部函数被销毁后才被销毁。
  • this 对象

    • 如果内部函数没使用箭头函数,则 this 对象会在运行时绑定到 执行函数的上下文
    • 如果在全局函数中调用,则 this 在非严格模式下等于 window;严格模式下 undefinde。
    • 如果作为对象方法,this 等于这个对象。
  • 内存泄漏

    • 注意切断循环引用

立即调用的函数表达式 (IIFE)

  • 模拟块级作用域
  • 锁定参数,防止变量定义外泄。

十一、期约与异步函数

异步编程

  • 同步行为和异步行为的对立与统一是计算机科学的一个基本概念。

期约 (Promise)

  • Promises/A+ 规范
  • 期约的基础

    • 期约状态机

      • pending/fulfilled/rejected
      • 状态不可逆
      • 状态是私有的
    • 解决值 (value)、拒绝理由 (reason) 及期约用例

    • 通过执行函数 (executor) 控制期约状态

      • 执行函数的指责:初始化期约的异步行为和控制状态的最终转换。
    • Promise.resolve()

      • 幂等性
      • let p = Promise.resolve(1); p === Promise.resolve(p); // true
    • Promise.reject()

      • 没有幂等性
    • 同步/异步执行的二原型

      • try { Promise.reject(new Error('foo')); } catch(e) { console.log(e); } // 未捕获期约抛出的错误
      • 原因: 拒绝期约的错误,没有跑到同步代码中,而是通过浏览器异步消息队列来处理的。因此,try/catch 不能捕获该错误。
  • 期约的实例方法

    • 实现 Thenable 接口
    • Promise.protype.then()

      • 为期约实例添加处理程序的主要方法。参数:onResolved 和 onRejected。
      • onResolved 处理程序的返回值,会通过 Promise.resolve() 包装来形成新的期约。
      • Promise.resolve('foo').then().then(); // Promise {<fulfilled>: "foo"}
      • Promise.resolve(new Error('qux')).then(); // Promise {<fulfilled>: Error: qux
      • Promise.reject(1).then(null, () => 2).then(); // Promise {<fulfilled>: 2}
    • Promise.protype.catch()

      • 为期约实添加拒绝处理程序。参数:onRejected。
      • 等价于:Promise.protype.then(null, onRejected)
      • Promise.reject(1).catch(() => 2).then(); // Promise {<fulfilled>: 2}
    • Promise.protype.finally()

      • 期约转换为解决或拒绝都会执行。参数:onFinally。
      • onFinally 是一个与状态无关的方法。原样后传父期约。
      • Promise.resolve(1).finally(() => 2).then(); // Promise {<fulfilled>: 1}
      • Promise.reject(1).finally(() => 2).then(); // Promise {<rejected>: 1}
    • 非重入期约方法

      • 当期约进入落定状态 (settled)时,与该状态相关的处理程序仅仅会被 排期,而非立即执行。
      • Javascript 运行时,保证 “非重入特性”:即,同步代码一定会在处理程序之前先执行。
    • 邻近处理程序的执行顺序

    • 传递解决值和拒绝理由
    • 拒绝期约与拒绝错误处理

      • 错误实际上是从消息队列异步抛出,所以不会阻止继续执行同步指令。
  • 期约连锁与期约合成

    • 期约连锁

      • 一个期约接一个期约地拼接。
      • 串行化异步任务,解决回调地狱问题。
    • 期约图

      • 期约连锁可以构建有向非循环图的结构。
      • 每个期约为图中的节点,使用实例方法添加的处理程序是有向顶点。
      • 由于期约的处理程序是先添加都消息队列,然后才逐个执行,因此都成了 层序遍历
    • Promise.all() & Promise.race()

    • 期约合成

      • 将多个期约组合为一个期约。
      • 类似函数合成,把多个函数左右处理程序合成一个连续传值的期约连锁。
      • Case 1
      function addTen(x) {
       return Promise.resove(x)
        .then(addTwo)
        .then(addThree)
        .then(addFive);
      }
      
      • Case 2
      function compose(...fns) {
        return x => fns.reduce(
         (promise, fn) => promise.then(fn),
         Promise.resolve(x)
       );
      }
      
      let addTen = compose(addTwo, addThree, addFive);
      
  • 期约扩展

    • 期约取消

      • 期约在处理过程中,程序却不再需要其结果。
      • Kevin Smith 的取消令牌 CancelToken
      class CancelToken {
        constructor(cancelFn) {
          this.promise = new Promise((resolve, reject) => {
      
            cancelFn(() => {
              setTimeout(console.log, 0, 'delay cancelled');
              resolve();
            });
          });
        }
      }
      
      const startButton = document.querySelector('#start');
      const cancelButton = document.querySelector('#cancel');
      
      function cancelableDelayedResolve(delay) {
        setTimeout(console.log, 0, 'start delay');
      
        return new Promise((resolve, reject) => {
          const id = setTimeout(() => {
            setTimeout(console.log, 0, 'delayed resolve');
            resolve();
          }, delay);
      
          const cancelToken = new CancelToken(cancelCallback => {
            cancelButton.addEventListener('click', cancelCallback);
          });
      
          cancelToken.promise.then(() => clearTimeout(id));
        });
      }
      
      startButton.addEventListener('click', () => cancelableDelayedResolve(2000));
      
    • 期约进度追踪

      • TrackablePromise
      class TrackablePromise extends Promise {
        constructor(executor) {
          const notifyHandlers = [];
      
          super((resolve, reject) => {
            return executor(resolve, reject, status => {
              notifyHandlers.map(handler => handler(status));
            });
          });
      
          this.notifyHandlers = notifyHandlers;
        }
      
        notify(handler) {
          this.notifyHandlers.push(handler);
          return this;
        }
      }
      
      /* 测试用例 */
      let p = new TrackablePromise((resolve, reject, notify) => {
        function countdown(x) {
          if(x > 0) {
            notify('x = ' + x);
            setTimeout(() => countdown(x - 1), 1000);
          } else {
            resolve();
          }
        }
      
        countdown(5);
      });
      
      p.notify(x => setTimeout(console.log, 0, x));
      

异步函数 (async/await)

  • 异步函数

    • async
    • await

      • await 关键字会暂停异步函数后面的代码,让出 Javascript 运行时的执行线程,等待期约解决。
      • 此行为与生成器的函数中的 yield 关键字一样。
      • await 关键字期待一个实现 thenable 接口的对象,但常规的值也可以。

        • 等待一个常规值

          • await 'foo';
        • 等待一个实现 thenable 接口的非期约对象

          • const thenable = { then(callback) { callback('bar'); } }; await thenable;
        • 等待一个期约

          • await Promise.resolve('baz');
    • await 的限制

  • 停止和恢复执行

    • async/await 中真正起作用的是 await。async 只是一个标志符。
    • Javascript runtime 在遇到 await 关键字时,会记录在哪里暂停执行。等到 await 右边的值可用了,Javascript runtime 会向消息队列中推送一个任务,这个任务会恢复异步函数的执行。
    • 运行时工作 Case
    async function foo() {
      console.log(2);
      await null;
      console.log(4);
    }
    
    console.log(1);
    foo();
    console.log(3);
    
    // 1
    // 2
    // 3
    // 4
    
    • 运行时工作 Case 分析

      • 1) 打印 1;
      • 2) 调用异步函数 foo();
      • 3) (在 foo() 中) 打印 2;
      • 4) (在 foo() 中) await 关键字暂停执行,为立即可用的值 null 向消息队列中添加一个任务;
      • 5) foo() 退出
      • 6) 打印 3;
      • 7) 同步线程代码执行完毕;
      • 8) Javascript 从消息队列中取出任务,恢复异步函数执行;
      • 9) (在 foo() 中) 恢复执行,await 取出 null 值;
      • 10) (在 foo() 中) 打印 4;
      • 11) 在 foo() 返回。
  • 异步函数策略

    • 实现 sleep()
    • 利用平行执行

      • 期约之间没有依赖关系时,先一次性初始化所有期约,然后再分别等待它们的结果。
    • 串行执行期约

      • 使用 async/await 期约连锁会变得简单。
    • 栈追踪与内存管理

      • 期约与异步函数功能大多重叠,但在内存中的表示则差别很大。
      • 异步函数的栈追踪信息能更准确的反映当前的调用栈。在抛错生成栈追踪信息时,性能更优。