ES6笔记-《ECMAScript 6 入门》

74 阅读21分钟

Babel

  1. ES6转码器,可将ES6转为ES5代码;
  2. 配置文件:设置转码规则和插件;
  3. 命令行工具,进行命令行转码;
  4. babel-node命令,提供一个支持 ES6 的 REPL 环境(Read-Eval-Print Loop,读入-求值-输出循环,是一种交互式编程环境);
  5. @babel/register使 require加载.js、.jsx、.es和.es6后缀名的文件,就会先用 Babel 进行转码;
  6. Polyfill:解决Babel不转换新API
  7. 浏览器环境:ES6实时转为ES5,对性能有影响,生产环境需要加载已经转码完成的脚本

let

  1. 块级作用域
  2. 不存在变量提升
  3. 暂时性死区(封闭作用域,let声明变量之前该变量不可用)
  4. 相同作用域内,不允许重复声明

块级作用域

  1. 需有大括号才有块级作用域
  2. 其他环境:在块级作用域之中声明函数,在块级作用域之外不可引用
  3. ES6浏览器:函数声明类似于var,即会提升到全局作用域或函数作用域的头部。同时,函数声明还会提升到所在的块级作用域的头部。

const

  1. 声明变量为只读,值不可改
  2. 块级作用域
  3. 不允许变量提升
  4. 暂时性死区
  5. 不可重复声明
  6. 本质:变量指向的那个内存地址所保存的数据不得改动
    • 简单类型:值保存在变量指向的内存地址,等同于常量
    • 复杂类型:内存地址保存的是指向实际数据的指针(地址不可变)
    • Object.freeze方法可将对象冻结,变成不可改

顶层对象属性

  1. 浏览器环境:window对象
  2. Node:global对象
  3. var和function声明的全局变量 = 顶层对象的属性
  4. let、const、class声明的全局变量 ≠ 顶层对象属性

globalThis对象

  1. 任何环境下,都可以从globalThis拿到顶层对象,指向全局环境下的this

数组形式解构赋值

  1. 只要某种数据结构具有 Iterator 接口,都可以采用数组形式的解构赋值(Iterator接口返回的对象包含 next() 方法,每次调用 next() 会返回一个包含 value(当前值)和 done(是否遍历完成)的对象。如Array、String、Map、Set等)

    • Generator函数返回一个遍历器对象,具有Iterator接口
      • Generator函数通过function*声明

      • 通过next()方法逐步执行,遇到yield暂停

      • yield关键字在函数内部标记暂停点,返回{value:..,done:bollean}结果,直到再次调用next()

      • 一般应用场景:惰性求值、异步编程控制、状态机

  2. 允许设置默认值

    • 数组成员为undefined时默认值会生效(因为ES6内部使用严格相等符号===来判断是否有值);
    • 数组成员为null时默认值不会生效(因为null不严格等于undefined);
    • 默认值为表达式时,这个表达式为惰性求值,用到的时候,才会求值;
    • 默认值可以引用解构赋值的其他变量,但该变量必须已经声明

对象的解构赋值

  1. 对象的属性没有次序,变量必须与属性同名;
  2. 内部机制是先找到同名属性,然后再赋给对应的变量;
  3. 可以取到继承的属性;
  4. 默认值生效条件是对象的属性值严格等于undefined

字符串的解构赋值

  1. 字符串被转换成了一个类似数组的对象;
  2. 可以对length属性解构赋值。

数值和布尔值的解构赋值

  1. 数值和布尔值会先转为对象;
  2. undefined和null无法转为对象,无法进行解构赋值。

函数参数的解构赋值

  1. 可以指定默认值

圆括号问题

  1. 只要有可能导致解构的歧义,就不得使用圆括号
  2. 不能使用圆括号的情况:变量声明、函数参数、赋值语句的模式
  3. 可以使用圆括号的情况:赋值语句的非模式部分,如
[(b)] = [3];

({ p: (d) } = {}); // 正确

[(parseInt.prop)] = [3];

解构赋值的用途

  1. 交换变量的值

    let x = 1;
    let y = 2;
    
    [x, y] = [y, x];
    
  2. 从函数返回多个值

    function example() {
      return [1, 2, 3];
    }
    let [a, b, c] = example();
    
  3. 函数参数的定义

    function f([x, y, z]) { ... }
    f([1, 2, 3]);
    
  4. 提取JSON数据

    let jsonData = {
      id: 42,
      status: "OK",
      data: [867, 5309]
    };
    
    let { id, status, data: number } = jsonData;
    
  5. 指定函数参数的默认值

  6. 遍历Map结构,用for...of获取键名和键值时更方便

    const map = new Map();
    map.set('first', 'hello');
    map.set('second', 'world');
    
    for (let [key, value] of map) {
      console.log(key + " is " + value);
    }
    
  7. 输入模块的指定方法

字符串的扩展

  1. uniCode表示法,采用\uxxx形式表示一个字符

  2. 添加了遍历器接口,字符串可以被for...of遍历(可以识别大于0xFFFF的码点)

  3. JSON.stringify()现在可能返回不符合UTF-8的字符串,所以如果遇到0xD8000xDFFF之间的单个码点,或者不存在的配对形式,它会返回转义字符串

  4. 引入模板字符串,用反引号(`)标识,将变量名写在${}

    • 需要使用反引号,则前面要用反斜杠转义

    • 所有模板字符串的空格和换行,都会被保留,不想要这个换行,可以使用trim方法消除

      $('#list').html(`
      <ul>
        <li>first</li>
        <li>second</li>
      </ul>
      `.trim());
      
    • 可以调用函数

    • 大括号中的值不是字符串,将按照一般的规则转为字符串

    • 可以进行嵌套

    • 标签模板:紧跟在一个函数名后面,该函数将被调用来处理这个模板字符串

      // 无变量
      alert`hello`
      // 等同于
      alert(['hello'])
      
      // 有变量
      let a = 5;
      let b = 10;
      tag`Hello ${ a + b } world ${ a * b }`;
      // 等同于
      tag(['Hello ', ' world ', ''], 15, 50);
      
      • 防止用户输入恶意内容

        let sender = '<script>alert("abc")</script>'; // 用户输入的恶意代码
        let message = SaferHTML`<p>${sender} has sent you a message.</p>`;
        
        message
        // <p>&lt;script&gt;alert("abc")&lt;/script&gt; has sent you a message.</p>
        
      • 可以使用标签模板,在 JavaScript 语言之中嵌入其他语言(jsx、java等)

      • 模板处理函数的第一个参数,还有一个raw属性,因为接受的参数,实际上是一个数组。该数组有一个raw属性,保存的是转义后的原字符串

        console.log`123`
        // ["123", raw: Array[1]]
        
  5. 模板字符串的限制

    • 模板字符串默认会将字符串转义,导致无法嵌入其他语言,为了解决这个问题,ES2018 放松了对标签模板里面的字符串转义的限制。如果遇到不合法的字符串转义,就返回undefined,而不是报错,并且从raw属性上面可以得到原始字符串。

字符串的新增方法

  1. String.fromCodePoint():用于从UniCode码点返回对应字符,可以识别码点大于0xFFFF的字符

  2. String.raw():可以作为处理模板字符串的基本方法,它会将所有变量替换,而且对斜杠进行转义,方便下一步作为字符串来使用

  3. codePointAt():能够正确处理 4 个字节储存的字符,返回一个字符的码点

  4. normalize():用来将字符的不同表示方法统一为同样的形式,normalize方法可以接受一个参数来指定normalize的方式

    • NFC,默认参数,表示“标准等价合成”
    • NFD,表示“标准等价分解”
    • NFKC,表示“兼容等价合成”
    • NFKD,表示“兼容等价分解”
  5. 实例方法:includes(), startsWith(), endsWith()

    • includes():返回布尔值,表示是否找到了参数字符串
    • startsWith():返回布尔值,表示参数字符串是否在原字符串的头部
    • endsWith():返回布尔值,表示参数字符串是否在原字符串的尾部
    • 这三个方法都支持第二个参数,表示开始搜索的位置。
  6. repeat()

    • repeat方法返回一个新字符串,表示将原字符串重复n
    • 参数如果是小数,会被取整
    • 参数是负数或者Infinity,会报错
    • 参数是 0 到-1 之间的小数,则等同于 0
    • 参数为NaN等同于 0
    • 参数是字符串,则会先转换成数字
  7. 实例方法:padStart(),padEnd()

    • padStart()用于头部补全,padEnd()用于尾部补全

      'x'.padStart(5, 'ab') // 'ababx'
      'x'.padStart(4, 'ab') // 'abax'
      
      'x'.padEnd(5, 'ab') // 'xabab'
      'x'.padEnd(4, 'ab') // 'xaba'
      
    • 接受两个参数,第一个参数是字符串补全生效的最大长度,第二个参数是用来补全的字符串

    • 如果原字符串的长度,等于或大于最大长度,则字符串补全不生效,返回原字符串

    • 如果用来补全的字符串与原字符串,两者的长度之和超过了最大长度,则会截去超出位数的补全字符串

    • 如果省略第二个参数,默认使用空格补全长度

  8. 实例方法:trimStart(),trimEnd()

    • trimStart()消除字符串头部的空格,trimEnd()消除尾部的空格,返回新字符串,不会修改原始字符串

      const s = '  abc  ';
      
      s.trim() // "abc"
      s.trimStart() // "abc  "
      s.trimEnd() // "  abc"
      
    • 对字符串头部(或尾部)的 tab 键、换行符等不可见的空白符号也有效

  9. 实例方法:matchAll()

    • 返回一个正则表达式在当前字符串的所有匹配

      const str = "2023-01-15, 2024-02-20";
      const regex = /(\d{4})-(\d{2})-(\d{2})/g;
      
      const matches = str.matchAll(regex);
      
      for (const match of matches) {
        console.log("完整匹配:", match[0]); // 如 "2023-01-15"
        console.log("年:", match[1]);      // 如 "2023"
        console.log("月:", match[2]);      // 如 "01"
        console.log("日:", match[3]);      // 如 "15"
        console.log("匹配索引:", match.index); // 如 0
      }
      
  10. 实例方法:replaceAll()

    • 一次性替换所有匹配
    • 返回一个新字符串,不会改变原字符串
    • replaceAll()的第二个参数replacement是一个字符串,表示替换的文本
  11. 实例方法:at()

    • 接受一个整数作为参数,返回参数指定位置的字符,支持负索引
    • 参数位置超出了字符串范围,at()返回undefined
  12. 实例方法:toWellFormed()

    • 将原始字符串里面的单个代理字符对,都替换为U+FFFD,返回一个新的字符串

正则的扩展

  1. RegExp构造函数

    • 第一个参数是一个正则对象,那么可以使用第二个参数指定修饰符。返回的正则表达式会忽略原有的正则表达式的修饰符,只使用新指定的修饰符
  2. 字符串的正则方法

    • match()replace()search()split()4 个方法,在语言内部全部调用RegExp的实例方法,从而做到所有与正则相关的方法,全都定义在RegExp对象上
  3. u修饰符

    • 含义为“Unicode 模式”,用来正确处理四个字节的 UTF-16 编码

      /^\uD83D/u.test('\uD83D\uDC2A') // false  识别为一个字符
      /^\uD83D/.test('\uD83D\uDC2A') // true
      
    • 点字符

      • 含义是除了换行符以外的任意单个字符,点字符不能识别,必须加上u修饰符
    • Unicode字符表示法

      • 使用大括号表示 Unicode 字符,加上u修饰符,才能识别当中的大括号,否则会被解读为量词
    • 量词

      • 使用u修饰符后,所有量词都会正确识别大于0xFFFF的 Unicode 字符
    • 预定义模式

      • 匹配所有非空白字符。只有加了u修饰符,它才能正确匹配码点大于0xFFFF的 Unicode 字符
    • i修饰符

      • 不加u修饰符,就无法识别非规范的K字符
    • 转义

      • 没有u修饰符的情况下,正则中没有定义的转义(如逗号的转义\,)无效,而在u模式会报错
  4. RegExp.prototype.unicode 属性

    • 表示是否设置了u修饰符
  5. y修饰符

    • 全局匹配,后一次匹配都从上一次匹配成功的下一个位置开始
    • 确保匹配必须从剩余的第一个位置开始
    • y修饰符不会忽略非法字符
  6. RegExp.prototype.sticky 属性

    • 表示是否设置了y修饰符
  7. RegExp.prototype.flags 属性

    • 返回正则表达式的修饰符
  8. s 修饰符:dotAll 模式

    • 使得.可以匹配任意单个字符
    • dotAll属性,返回一个布尔值,表示该正则表达式是否处在dotAll模式
  9. 后行断言

    • 与“先行断言”相反,x只有在y后面才匹配
  10. Unicode 属性类

    • Unicode 属性类的标准形式,需要同时指定属性名和属性值
    • 对于某些属性,可以只写属性名,或者只写属性值
  11. v 修饰符:Unicode 属性类的运算

    • 对属性类进行运算,一种是差集运算,另一种是交集运算
  12. 具名组匹配

    • 允许为每一个组匹配指定一个名字,既便于阅读代码,又便于引用
  13. d 修饰符:正则匹配索引

    • exec()match()的返回结果添加indices属性,在该属性上面可以拿到匹配的开始位置和结束位置
  14. String.prototype.matchAll()

    • 一次性取出所有匹配。返回一个遍历器(Iterator),而不是数组

      const string = 'test1test2test3';
      const regex = /t(e)(st(\d?))/g;
      
      for (const match of string.matchAll(regex)) {
        console.log(match);
      }
      // ["test1", "e", "st1", "1", index: 0, input: "test1test2test3"]
      // ["test2", "e", "st2", "2", index: 5, input: "test1test2test3"]
      // ["test3", "e", "st3", "3", index: 10, input: "test1test2test3"]
      

数值的扩展

  1. 二进制和八进制表示法

    • 分别用前缀0b(或0B)和0o(或0O

    • 将数值转为十进制,要使用Number方法

      Number('0b111')  // 7
      Number('0o10')  // 8
      
  2. 数值分隔符

    • 使用下划线(_)作为分隔符

    • 注意点:

      • 不能放在数值的最前面或最后面。

      • 不能两个或两个以上的分隔符连在一起。

      • 小数点的前后不能有分隔符。

      • 科学计数法里面,表示指数的eE前后不能有分隔符

        // 全部报错
        3_.141
        3._141
        1_e12
        1e_12
        123__456
        _1464301
        1464301_
        
  3. Number.isFinite(), Number.isNaN()

    • Number.isFinite()检查一个数值是否为有限的,即不是Infinity
    • Number.isNaN()用来检查一个值是否为NaN
  4. Number.parseInt(), Number.parseFloat()

    • 将全局方法parseInt()parseFloat(),移植到了Number对象上面
  5. Number.isInteger()

    • 用来判断一个数值是否为整数

    • 整数和浮点数采用的是同样的储存方法

      Number.isInteger(25) // true
      Number.isInteger(25.0) // true
      
    • 数值存储为64位双精度格式,如果数值的精度超过这个限度,第54位及后面的位就会被丢弃,这种情况下,Number.isInteger可能会误判

      Number.isInteger(3.0000000000000002) // true
      
    • 一个数值的绝对值小于Number.MIN_VALUE(5E-324),即小于 JavaScript 能够分辨的最小值,会被自动转为 0

  6. Number.EPSILON

    • 表示 1 与大于 1 的最小浮点数之间的差,实际上是 JavaScript 能够表示的最小精度
  7. 安全整数和 Number.isSafeInteger()

    • JavaScript 能够准确表示的整数范围在-2^532^53之间(不含两个端点)
    • Number.isSafeInteger()则是用来判断一个整数是否落在这个范围之内
  8. Math 对象的扩展

    • Math.trunc()
      • 用于去除一个数的小数部分,返回整数部分
      • 对于非数值,Math.trunc内部使用Number方法将其先转为数值
      • 对于空值和无法截取整数的值,返回NaN
    • Math.sign()
      • 用来判断一个数到底是正数、负数、还是零。对于非数值,会先将其转换为数值
      • 参数为正数,返回+1
      • 参数为负数,返回-1
      • 参数为 0,返回0
      • 参数为-0,返回-0;
      • 其他值,返回NaN
    • Math.cbrt()
      • 用于计算一个数的立方根
      • 对于非数值,Math.cbrt()方法内部也是先使用Number()方法将其转为数值
    • Math.clz32()
      • 将参数转为 32 位无符号整数的形式,然后返回这个 32 位值里面有多少个前导 0
      • 对于小数,只考虑整数部分
      • 对于小数,只考虑整数部分
    • Math.imul()
      • 返回两个数以 32 位带符号整数形式相乘的结果,返回的也是一个 32 位的带符号整数
    • Math.fround()
      • 返回一个数的32位单精度浮点数形式
    • Math.hypot()
      • 返回所有参数的平方和的平方根
    • Math.expm1()
    • 回 ex - 1,即Math.exp(x) - 1
    • Math.log1p()
      • 返回1 + x的自然对数,即Math.log(1 + x)。如果x小于-1,返回NaN
    • Math.log10()
      • 以 10 为底的x的对数。如果x小于 0,则返回 NaN
    • Math.log2()
      • 返回以 2 为底的x的对数。如果x小于 0,则返回 NaN
    • 双曲函数方法
      • Math.sinh(x) 返回x的双曲正弦(hyperbolic sine)
      • Math.cosh(x) 返回x的双曲余弦(hyperbolic cosine)
      • Math.tanh(x) 返回x的双曲正切(hyperbolic tangent)
      • Math.asinh(x) 返回x的反双曲正弦(inverse hyperbolic sine)
      • Math.acosh(x) 返回x的反双曲余弦(inverse hyperbolic cosine)
      • Math.atanh(x) 返回x的反双曲正切(inverse hyperbolic tangent)
  9. BigInt 数据类型

    • BigInt 只用来表示整数,没有位数的限制,任何位数的整数都可以精确表示

    • BigInt 类型的数据必须添加后缀n

      1234 // 普通整数
      1234n // BigInt
      
      // BigInt 的运算
      1n + 2n // 3n
      
    • 可以使用各种进制表示,都要加上后缀n

    • BigInt 与普通整数是两种值,它们之间并不相等

    • 可以使用负号(-),但是不能使用正号(+

  10. BigInt 函数

    • 可以用它生成 BigInt 类型的数值

    • 必须有参数,而且参数必须可以正常转为数值

      new BigInt() // TypeError
      BigInt(undefined) //TypeError
      BigInt(null) // TypeError
      BigInt('123n') // SyntaxError
      BigInt('abc') // SyntaxError
      
    • 参数不能是小数

    • 转换规则

      • 可以使用Boolean()Number()String()这三个方法,将 BigInt 可以转为布尔值、数值和字符串类型
      • 转为字符串时后缀n会消失
      • 取反运算符(!)也可以将 BigInt 转为布尔值
    • 数学运算

      • 除法运算/会舍去小数部分,返回一个整数
      • 以下不能用在BigInt:
        • 不带符号的右移位运算符>>>
        • 一元的求正运算符+
      • 不能与普通数值进行混合运算
    • 其他运算

      • 比较运算符(比如>)和相等运算符(==)允许 BigInt 与其他类型的值混合计算

        0n < 1 // true
        0n < true // true
        0n == 0 // true
        0n == false // true
        0n === 0 // false
        
      • BigInt 与字符串混合运算时,会先转为字符串,再进行运算

函数的扩展

  1. 函数参数的默认值

    • 直接写在参数定义的后面

    • 使用参数默认值时,函数不能有同名参数

    • 参数默认值可以与解构赋值的默认值,结合起来使用

      function foo({x, y = 5}) {
        console.log(x, y);
      }
      
      foo({}) // undefined 5
      foo({x: 1}) // 1 5
      foo({x: 1, y: 2}) // 1 2
      foo() // TypeError: Cannot read property 'x' of undefined
      
      // 调用时没提供参数
      function foo({x, y = 5} = {}) {
        console.log(x, y);
      }
      
      foo() // undefined 5
      
    • 参数默认值的位置

      • 非尾部的参数设置默认值,无法只省略该参数,而不省略它后面的参数,除非显式输入undefined

        function f(x = 1, y) {
          return [x, y];
        }
        
        f() // [1, undefined]
        f(2) // [2, undefined]
        f(, 1) // 报错
        f(undefined, 1) // [1, 1]
        
    • 函数的 length 属性

      • 返回没有指定默认值的参数个数

      • 设置默认值的参数不是尾参数,length属性不会计入后面的参数

        (function (a = 0, b, c) {}).length // 0
        (function (a, b = 1, c) {}).length // 1
        
    • 作用域

      • 设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域(context)。等到初始化结束,这个作用域就会消失。
    • 应用

      • 指定某一个参数不得省略,如果省略就抛出一个错误

        function throwIfMissing() {
          throw new Error('Missing parameter');
        }
        
        function foo(mustBeProvided = throwIfMissing()) {
          return mustBeProvided;
        }
        
        foo()
        // Error: Missing parameter
        
      • 将参数默认值设为undefined,表明这个参数是可以省略的

  2. rest 参数

    • 形式为...变量名,用于获取函数的多余参数

    • 该变量将多余的参数放入数组中

    • 注意点

      • rest 参数之后不能再有其他参数

        // 报错
        function f(a, ...b, c) {
          // ...
        }
        
      • 函数的length属性,不包括 rest 参数

        (function(a) {}).length  // 1
        (function(...a) {}).length  // 0
        (function(a, ...b) {}).length  // 1
        
  3. 严格模式

    • 只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式

    • 可以设定全局性的严格模式

    • 可以把函数包在一个无参数的立即执行函数里面

      const doSomething = (function () {
        'use strict';
        return function(value = 42) {
          return value;
        };
      }());
      
  4. name 属性

    • 返回该函数的函数名
    • 将一个匿名函数赋值给一个变量,ES5 的name属性,会返回空字符串,ES6 的name属性会返回实际的函数名。
    • 将一个具名函数赋值给一个变量,则 ES5 和 ES6 的name属性都返回这个具名函数原本的名字。
  5. 箭头函数

    • 大括号被解释为代码块,如果箭头函数直接返回一个对象,必须在对象外面加上括号

    • 注意点

      • 箭头函数没有自己的this对象,内部的this就是定义时上层作用域中的this
      • 不可以当作构造函数,也就是说,不可以对箭头函数使用new命令,否则会抛出一个错误。
      • 不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
      • 不可以使用yield命令,因此箭头函数不能用作 Generator 函数。
    • 不适用场合

      • 定义对象的方法,且该方法内部包括this,此时this会指向全局对象,因为对象不构成单独的作用域,导致箭头函数定义时的作用域就是全局作用域

        globalThis.s = 21;
        
        const obj = {
          s: 42,
          m: () => console.log(this.s)
        };
        
        obj.m() // 21
        
      • 需要动态this的时候,也不应使用箭头函数

        var button = document.getElementById('press');
        button.addEventListener('click', () => {
          this.classList.toggle('on'); // 这里的this指向全局对象而不是被点击的按钮对象
        });
        
    • 嵌套的箭头函数

      const pipeline = (...funcs) =>
        val => funcs.reduce((a, b) => b(a), val); // (5+1)*2
      
      const plus1 = a => a + 1;
      const mult2 = a => a * 2;
      const addThenMult = pipeline(plus1, mult2); // pipeline函数调用返回val=>funcs...
      
      addThenMult(5) // 5作为初始值
      // 12
      
  6. 尾调用优化

    • 尾调用是某个函数的最后一步是调用另一个函数

    • 优化:尾递归

      • 函数尾调用自身
      • 只存在一个调用帧,所以永远不会发生“栈溢出”错误
    • 柯里化

      • 将多参数的函数转换成单参数的形式

        function currying(fn, n) {
          return function (m) {
            return fn.call(this, m, n);
          };
        }
        
        function tailFactorial(n, total) {
          if (n === 1) return total;
          return tailFactorial(n - 1, n * total);
        }
        
        const factorial = currying(tailFactorial, 1);
        
        factorial(5) // 120
        
    • 严格模式

      • 尾调用优化只在严格模式下开启,正常模式是无效的
    • 尾递归优化的实现(不在严格模式下)

      • 蹦床函数

        function trampoline(f) {
          while (f && f instanceof Function) { // 当f调用后返回的是函数就会进入循环
            f = f(); // 调用f返回一个新函数或值
          }
          return f;
        }
        
        function sum(x, y) {
          if (y > 0) {
            return sum.bind(null, x + 1, y - 1); // 创建一个新函数,指向null
          } else {
            return x;
          }
        }
        
        trampoline(sum(1, 100000)) // sum函数的每次执行,都会返回自身的另一个版本
        // 100001
        
      • 真正的尾递归优化

        function tco(f) {
          var value;			// 最终结果
          var active = false;	// 标记是否在循环中
          var accumulated = [];	// 参数队列
        
          return function accumulator() {
            accumulated.push(arguments);	// 收集每次递归调用的参数
            if (!active) {
              active = true;
              while (accumulated.length) {	// 循环处理队列
                value = f.apply(this, accumulated.shift());
                // bind以给定的 this 值和作为数组提供的 arguments 调用该函数
              }
              active = false;
              return value;
            }
          };
        }
        
        var sum = tco(function(x, y) {
          if (y > 0) {
            return sum(x + 1, y - 1)
          }
          else {
            return x
          }
        });
        
        sum(1, 100000)
        // 100001
        
  7. 允许函数的最后一个参数有尾逗号

  8. Function.prototype.toString()

    • 返回一模一样的原始代码,不省略注释和空格
  9. catch 命令的参数省略

    • 允许catch语句省略参数

数组的扩展

  • 扩展运算符

    • 将一个数组转为用逗号分隔的参数序列

    • 后面还可以放置表达式

      const arr = [
        ...(x > 0 ? ['a'] : []),
        'b',
      ];
      
    • 只有函数调用时,扩展运算符才可以放在圆括号中

    • 应用

      • 复制数组

        const a1 = [1, 2];
        // 写法一
        const a2 = [...a1];
        // 写法二
        const [...a2] = a1;
        
      • 合并数组(浅拷贝)

        const arr1 = ['a', 'b'];
        const arr2 = ['c'];
        const arr3 = ['d', 'e'];
        
        // ES5 的合并数组
        arr1.concat(arr2, arr3);
        // [ 'a', 'b', 'c', 'd', 'e' ]
        
        // ES6 的合并数组
        [...arr1, ...arr2, ...arr3]
        // [ 'a', 'b', 'c', 'd', 'e' ]
        
      • 与解构赋值结合

        const [first, ...rest] = [1, 2, 3, 4, 5];
        first // 1
        rest  // [2, 3, 4, 5]
        
      • 字符串:将字符串转为真正的数组

        [...'hello']
        // [ "h", "e", "l", "l", "o" ]
        
      • 实现了 Iterator 接口的对象

        • 任何定义了遍历器(Iterator)接口的对象,都可以用扩展运算符转为真正的数组

          let nodeList = document.querySelectorAll('div');
          let array = [...nodeList];
          
      • Map 和 Set 结构,Generator 函数

        let map = new Map([
          [1, 'one'],
          [2, 'two'],
          [3, 'three'],
        ]);
        
        let arr = [...map.keys()]; // [1, 2, 3]
        
        const go = function*(){
          yield 1;
          yield 2;
          yield 3;
        };
        
        [...go()] // [1, 2, 3]
        
  • Array.from()

    • 将两类对象转为真正的数组

      • 类似数组的对象( 本质是有length属性,如DOM 操作返回的 NodeList 集合、函数内部的arguments对象)

        let arrayLike = {
            '0': 'a',
            '1': 'b',
            '2': 'c',
            length: 3
        };
        
        // ES5 的写法
        var arr1 = [].slice.call(arrayLike); // ['a', 'b', 'c']
        
        // ES6 的写法
        let arr2 = Array.from(arrayLike); // ['a', 'b', 'c']
        
      • 可遍历(iterable)的对象

    • 可以接受第二个参数,对每个元素进行处理,处理后的值放入返回的数组

      Array.from(arrayLike, x => x * x);
      // 等同于
      Array.from(arrayLike).map(x => x * x);
      
    • 可以传入Array.from()的第三个参数,用来绑定this

  • Array.of()

    • 用于将一组值,转换为数组

    • 如果没有参数,就返回一个空数组

      Array.of(3, 11, 8) // [3,11,8]
      Array.of(3) // [3]
      Array.of(3).length // 1
      
  • 实例方法:copyWithin()

    • 当前数组内部,将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组

    • 接受三个参数,这三个参数都应该是数值,如果不是,会自动转为数值

      • target(必需):从该位置开始替换数据。如果为负值,表示倒数。

      • start(可选):从该位置开始读取数据,默认为 0。如果为负值,表示从末尾开始计算。

      • end(可选):到该位置前停止读取数据,默认等于数组长度。如果为负值,表示从末尾开始计算。

        // 将3号位复制到0号位
        [1, 2, 3, 4, 5].copyWithin(0, 3, 4)
        // [4, 2, 3, 4, 5]
        
        // -2相当于3号位,-1相当于4号位
        [1, 2, 3, 4, 5].copyWithin(0, -2, -1)
        // [4, 2, 3, 4, 5]
        
        // 将3号位复制到0号位
        [].copyWithin.call({length: 5, 3: 1}, 0, 3)
        // {0: 1, 3: 1, length: 5}
        
        // 将2号位到数组结束,复制到0号位
        let i32a = new Int32Array([1, 2, 3, 4, 5]);
        i32a.copyWithin(0, 2);
        // Int32Array [3, 4, 5, 4, 5]
        
        // 对于没有部署 TypedArray 的 copyWithin 方法的平台
        // 需要采用下面的写法
        [].copyWithin.call(new Int32Array([1, 2, 3, 4, 5]), 0, 3, 4);
        // Int32Array [4, 2, 3, 4, 5]
        
  • 实例方法:find(),findIndex(),findLast(),findLastIndex()

    • find()

      • 用于找出第一个符合条件的数组成员

      • 如果没有符合条件的成员,则返回undefined

      • 接受三个参数,依次为当前的值、当前的位置和原数组

        [1, 5, 10, 15].find(function(value, index, arr) {
          return value > 9;
        }) // 10
        
    • findIndex()

      • 返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1
    • find()与findIndex()

      • 两个方法都可以接受第二个参数,用来绑定回调函数的this对象。

        function f(v){
          return v > this.age;
        }
        let person = {name: 'John', age: 20};
        [10, 12, 26, 15].find(f, person);    // 26  回调函数中的this对象指向person对象
        
      • 两个方法都可以发现NaN

        [NaN].indexOf(NaN)
        // -1
        
        [NaN].findIndex(y => Object.is(NaN, y)) 
        // findIndex()方法可以借助Object.is()方法做到
        // 0
        
    • findLast()与findLastIndex()

      • 从数组的最后一个成员开始,依次向前检查
  • 实例方法:fill()

    • 使用给定值,填充一个数组

    • 可以接受第二个和第三个参数,用于指定填充的起始位置和结束位置

      ['a', 'b', 'c'].fill(7, 1, 2)
      // ['a', 7, 'c']
      
    • 如果填充的类型为对象,那么被赋值的是同一个内存地址的对象,而不是深拷贝对象

  • 实例方法:entries(),keys() 和 values()

    • 用于遍历数组,都返回一个遍历器对象

    • keys()是对键名的遍历、values()是对键值的遍历,entries()是对键值对的遍历

      for (let index of ['a', 'b'].keys()) {
        console.log(index);
      }
      // 0
      // 1
      
      for (let elem of ['a', 'b'].values()) {
        console.log(elem);
      }
      // 'a'
      // 'b'
      
      for (let [index, elem] of ['a', 'b'].entries()) {
        console.log(index, elem);
      }
      // 0 "a"
      // 1 "b"
      
  • 实例方法:includes()

    • 返回一个布尔值,表示某个数组是否包含给定的值

    • 第二个参数表示搜索的起始位置,默认为0。如果第二个参数为负数,则表示倒数的位置,如果这时它大于数组长度,则会重置为从0开始

      [1, 2, 3].includes(3, 3);  // false
      [1, 2, 3].includes(3, -1); // true
      
    • 缺点

      • 内部使用严格相等运算符(===)进行判断,会导致对NaN的误判
  • 实例方法:flat(),flatMap()

    • flat()

      • 将嵌套的数组“拉平”,变成一维的数组。该方法返回一个新数组,对原数据没有影响

      • 参数为一个整数,表示想要拉平的层数,默认为1。

        [1, [2, [3]]].flat(Infinity)
        // [1, 2, 3]
        
    • flatMap()

      • 对原数组的每个成员执行一个函数,然后对返回值组成的数组执行flat()方法。该方法返回一个新数组,不改变原数组

      • 参数是一个遍历函数,该函数可以接受三个参数,分别是当前数组成员、当前数组成员的位置(从零开始)、原数组。有第二个参数,用来绑定遍历函数里面的this

        arr.flatMap(function callback(currentValue[, index[, array]]) {
          // ...
        }[, thisArg])
        
  • 实例方法:at()

    • 接受一个整数作为参数,返回对应位置的成员,并支持负索引。这个方法不仅可用于数组,也可用于字符串和类型数组

      const arr = [5, 12, 8, 130, 44];
      arr.at(2) // 8
      arr.at(-2) // 130
      
    • 如果参数位置超出了数组范围,at()返回undefined

  • 实例方法:toReversed(),toSorted(),toSpliced(),with()

    • 对数组进行操作时,不改变原数组,而返回一个原数组的拷贝
    • toReversed()对应reverse(),用来颠倒数组成员的位置。
    • toSorted()对应sort(),用来对数组成员排序。
    • toSpliced()对应splice(),用来在指定位置,删除指定数量的成员,并插入新成员。
    • with(index, value)对应splice(index, 1, value),用来将指定位置的成员替换为新的值。
  • 实例方法:group(),groupToMap()

    • group()

      • 以接受三个参数,依次是数组的当前成员、该成员的位置序号、原数组

      • 分组函数返回值应该是字符串,以作为分组后的组名。

      • group()的返回值是一个对象,该对象的键名就是每一组的组名

        const array = [1, 2, 3, 4, 5];
        
        array.group((num, index, array) => {
          return num % 2 === 0 ? 'even': 'odd';
        });
        // { odd: [1, 3, 5], even: [2, 4] }
        
    • groupToMap()

      • 与group()完全一致,唯一的区别是返回值是一个 Map 结构,而不是对象

        const array = [1, 2, 3, 4, 5];
        
        const odd  = { odd: true };
        const even = { even: true };
        array.groupToMap((num, index, array) => {
          return num % 2 === 0 ? even: odd;
        });
        //  Map { {odd: true}: [1, 3, 5], {even: true}: [2, 4] }
        
  • 数组的空位

    • 空位不是undefined,空位是没有任何值

对象的扩展

  1. 方法的 name 属性

    • 方法的name属性返回函数名(即方法名)
    • 对象的方法使用了gettersetter,则name属性在该方法的属性的描述对象的getset属性上面
  2. 属性的遍历

    • for...in
    • Object.keys(obj)
    • Object.getOwnPropertyNames(obj)
    • Object.getOwnPropertySymbols(obj)
    • Reflect.ownKeys(obj)
  3. super 关键字

    • 用来指向当前对象的原型对象

    • 表示原型对象时,只能用在对象的方法之中

      const proto = {
        foo: 'hello'
      };
      
      const obj = {
        foo: 'world',
        find() {
          return super.foo;
        }
      };
      
      Object.setPrototypeOf(obj, proto);
      obj.find() // "hello"
      
    • 不可在属性、箭头函数或普通函数中使用

  4. 解构赋值

    let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
    x // 1
    y // 2
    z // { a: 3, b: 4 }
    
    • 要求等号右边是一个对象,不能是undefined``或null`

    • 解构赋值必须是最后一个参数,否则会报错

    • 不能复制继承自原型对象的属性

  5. 扩展运算符

    • 取出参数对象的所有可遍历属性,拷贝到当前对象之中

      let z = { a: 3, b: 4 };
      let n = { ...z };
      n // { a: 3, b: 4 }
      
  6. 完整克隆对象(包含原型链的克隆方式)

    const clone = Object.assign(
      Object.create(Object.getPrototypeOf(obj)), 
      obj
    );
    

对象的新增方法

  1. Object.is()

    • 比较两个值是否严格相等,与严格比较运算符(===)的行为基本一致

    • 不同之处只有两个:一是+0不等于-0,二是NaN等于自身

      +0 === -0 //true
      NaN === NaN // false
      
      Object.is(+0, -0) // false
      Object.is(NaN, NaN) // true
      
  2. Object.assign()

    • 合并对象(浅拷贝),将源对象的可枚举属性复制到目标对象

      const target = { a: 1, b: 1 };
      
      const source1 = { b: 2, c: 2 };
      const source2 = { c: 3 };
      
      Object.assign(target, source1, source2);
      target // {a:1, b:2, c:3}
      
    • 如果该参数不是对象,则会先转成对象,然后返回

    • undefinednull无法转成对象,不能作为参数,但是如果不在首参数,就不会报错

    • 限制:只拷贝源对象的自身属性(不拷贝继承属性),也不拷贝不可枚举的属性

    • 遇到同名属性,Object.assign()的处理方法是替换

    • 用处

      • 为对象添加属性

        class Point {
          constructor(x, y) {
            Object.assign(this, {x, y});
          }
        }
        
      • 为对象添加方法

        Object.assign(SomeClass.prototype, {
          someMethod(arg1, arg2) {
            ···
          },
          anotherMethod() {
            ···
          }
        });
        
        // 等同于下面的写法
        SomeClass.prototype.someMethod = function (arg1, arg2) {
          ···
        };
        SomeClass.prototype.anotherMethod = function () {
          ···
        };
        
      • 合并多个对象

        const merge =
          (...sources) => Object.assign({}, ...sources);
        
      • 为属性指定默认值

        const DEFAULTS = {
          logLevel: 0,
          outputFormat: 'html'
        };
        
        function processContent(options) {
          options = Object.assign({}, DEFAULTS, options); // options是用户提供的参数
          console.log(options);
          // ...
        }
        
  3. Object.getOwnPropertyDescriptors()

    • 返回指定对象所有自身属性(非继承属性)的描述对象

    • 可以拷贝get属性和set属性

      const source = { set foo(v) { console.log(v); } };
      const target = {};
      Object.defineProperties(target, Object.getOwnPropertyDescriptors(source));
      
  4. Object.setPrototypeOf(),Object.getPrototypeOf()

    • Object.setPrototypeOf():设置一个对象的原型对象,返回参数对象本身
    • Object.getPrototypeOf():用于读取一个对象的原型对象
  5. Object.keys(),Object.values(),Object.entries()

    • Object.keys():返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历属性的键名
    • Object.values():返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历属性的键值
    • Object.entries():返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历属性的键值对数组。
  6. Object.fromEntries():用于将一个键值对数组转为对象

  7. Object.hasOwn()

    • 判断是否为自身的属性

    • 接受两个参数,第一个是所要判断的对象,第二个是属性名

      const foo = Object.create({ a: 123 });
      foo.b = 456;
      
      Object.hasOwn(foo, 'a') // false
      Object.hasOwn(foo, 'b') // true
      

运算符的扩展

  1. 指数运算符 **

    • 右结合:多个指数运算符连用时,是从最右边开始计算的
  2. 链判断运算符 ?.

    • 直接在链式调用的时候判断,左侧的对象是否为nullundefined。如果是的,就不再往下运算,而是返回undefined

      const firstName = message?.body?.user?.firstName || 'default';
      const fooValue = myForm.querySelector('input[name=foo]')?.value
      
    • 对于没有实现的方法

      if (myForm.checkValidity?.() === false) {
        // 表单校验失败
        return;
      }
      
    • 写法

      • obj?.prop // 对象属性是否存在

      • obj?.[expr] // 同上

      • func?.(...args) // 函数或对象方法是否存在

      • 常见形式

        a?.b
        // 等同于
        a == null ? undefined : a.b
        
    • 注意点

      • 如果属性链有圆括号,链判断运算符对圆括号外部没有影响,只对圆括号内部有影响。

      • 以下写法报错

        // 构造函数
        new a?.()
        new a?.b()
        
        // 链判断运算符的右侧有模板字符串
        a?.`{b}`
        a?.b`{c}`
        
        // 链判断运算符的左侧是 super
        super?.()
        super?.foo
        
        // 链运算符用于赋值运算符左侧
        a?.b = c
        
  3. Null 判断运算符 ??

    • 只有运算符左侧的值为nullundefined时,才会返回右侧的值

    • 与?.配合使用

      const animationDuration = response.settings?.animationDuration ?? 300;
      
    • 判断函数参数是否赋值

      function Component(props) {
        const enable = props.enabled ?? true;
        // …
      }
      
    • 如果多个逻辑运算符一起使用,必须用括号表明优先级,否则会报错

  4. 逻辑赋值运算符 ||=&&=??=

    • 先进行逻辑运算,然后根据运算结果,再视情况进行赋值运算

      // 老的写法
      user.id = user.id || 1;
      
      // 新的写法
      user.id ||= 1;
      
  5. #!命令:放在脚本的第一行,用来指定脚本的执行器

Symbol

  1. 概述

    • 类似于字符串的数据类型,保证每个属性的名字都是独一无二

    • 通过Symbol()函数生成

    • 参数是一个对象,就会调用该对象的toString()方法,将其转为字符串,然后才生成一个 Symbol 值

      const obj = {
        toString() {
          return 'abc';
        }
      };
      const sym = Symbol(obj);
      sym // Symbol(abc)
      
    • 相同参数的Symbol函数的返回值是不相等的

      // 没有参数的情况
      let s1 = Symbol();
      let s2 = Symbol();
      
      s1 === s2 // false
      
      // 有参数的情况
      let s1 = Symbol('foo');
      let s2 = Symbol('foo');
      
      s1 === s2 // false
      
    • 不能与其他类型的值进行运算

  2. Symbol.prototype.description:直接返回 Symbol 值的描述

  3. 作为属性名的 Symbol

    • 作为对象属性名时,不能用点运算符

      const mySymbol = Symbol();
      const a = {};
      
      a.mySymbol = 'Hello!';
      a[mySymbol] // undefined
      a['mySymbol'] // "Hello!"	// 不再代表Symbol 值
      
    • 在对象的内部,使用 Symbol 值定义属性时,Symbol 值必须放在方括号之中(不用方括号则不再代表Symbol 值)

      let s = Symbol();
      
      let obj = {
        [s]: function (arg) { ... }
      };
      
      obj[s](123);
      
  4. 实例:消除魔术字符串

    • 魔术字符串:在代码之中多次出现、与代码形成强耦合的某一个具体的字符串或者数值

    • 常用方法:写成变量

      const shapeType = {
        triangle: 'Triangle'
      };
      
      function getArea(shape, options) {
        let area = 0;
        switch (shape) {
          case shapeType.triangle:
            area = .5 * options.width * options.height;
            break;
        }
        return area;
      }
      
      getArea(shapeType.triangle, { width: 100, height: 100 });
      
  5. 属性名的遍历

    • Object.getOwnPropertySymbols()方法:

      • 可以获取指定对象的所有 Symbol 属性名

      • 返回一个数组,成员是当前对象的所有用作属性名的 Symbol 值

        const obj = {};
        let a = Symbol('a');
        let b = Symbol('b');
        
        obj[a] = 'Hello';
        obj[b] = 'World';
        
        const objectSymbols = Object.getOwnPropertySymbols(obj);
        
        objectSymbols
        // [Symbol(a), Symbol(b)]
        
    • Reflect.ownKeys()

      • 返回所有类型的键名,包括常规键名和 Symbol 键名。

        let obj = {
          [Symbol('my_key')]: 1,
          enum: 2,
          nonEnum: 3
        };
        
        Reflect.ownKeys(obj)
        //  ["enum", "nonEnum", Symbol(my_key)]
        
  6. Symbol.for(),Symbol.keyFor()

    • Symbol.for()

      • 接受一个字符串作为参数,然后搜索有没有以该参数作为名称的 Symbol 值。如果有,就返回这个 Symbol 值,否则就新建一个以该字符串为名称的 Symbol 值,并将其注册到全局。

        let s1 = Symbol.for('foo');
        let s2 = Symbol.for('foo');
        
        s1 === s2 // true
        
    • Symbol.keyFor()

      • 返回一个已登记的 Symbol 类型值的key

        let s1 = Symbol.for("foo");
        Symbol.keyFor(s1) // "foo"
        
        let s2 = Symbol("foo");
        Symbol.keyFor(s2) // undefined
        
  7. 实例:模块的 Singleton 模式

    • Singleton 模式指的是调用一个类,任何时候返回的都是同一个实例——把实例放到顶层对象global

    • 为防止其他文件进行修改,键名使用Symbol方法生成,那么外部将无法引用这个值,就无法改写

      // mod.js
      const FOO_KEY = Symbol.for('foo');
      
      function A() {
        this.foo = 'hello';
      }
      
      if (!global[FOO_KEY]) {
        global[FOO_KEY] = new A();
      }
      
      module.exports = global[FOO_KEY];
      
  8. 内置的 Symbol 值

Set 和 Map 数据结构

  1. Set

    • 类似于数组,但是成员的值都是唯一的,没有重复的值

    • 用处

      • 去除数组重复成员

        // 去除数组的重复成员
        [...new Set(array)]
        
      • 去除字符串里面的重复字符

        [...new Set('ababbc')].join('')
        // "abc"
        
    • 加入值的时候,不会发生类型转换

    • 两个对象总是不相等的

    • 配合Array.from()去除数组重复成员

      function dedupe(array) {
        return Array.from(new Set(array));
      }
      
      dedupe([1, 1, 2, 3]) // [1, 2, 3]
      
    • Set实例的属性

      • Set.prototype.constructor:构造函数,默认就是Set函数。
      • Set.prototype.size:返回Set实例的成员总数。
    • Set实例的方法

      • Set.prototype.add(value):添加某个值,返回 Set 结构本身。

      • Set.prototype.delete(value):删除某个值,返回一个布尔值,表示删除是否成功。

      • Set.prototype.has(value):返回一个布尔值,表示该值是否为Set的成员。

      • Set.prototype.clear():清除所有成员,没有返回值。

        s.add(1).add(2).add(2);
        // 注意2被加入了两次
        
        s.size // 2
        
        s.has(1) // true
        s.has(2) // true
        s.has(3) // false
        
        s.delete(2) // true
        s.has(2) // false
        
    • 遍历操作

      • Set.prototype.keys():返回键名的遍历器

      • Set.prototype.values():返回键值的遍历器

      • Set.prototype.entries():返回键值对的遍历器

      • Set.prototype.forEach():使用回调函数遍历每个成员

        let set = new Set(['red', 'green', 'blue']);
        
        for (let item of set.keys()) {
          console.log(item);
        }
        // red
        // green
        // blue
        
        for (let item of set.values()) {
          console.log(item);
        }
        // red
        // green
        // blue
        
        for (let item of set.entries()) {
          console.log(item);
        }
        // ["red", "red"]
        // ["green", "green"]
        // ["blue", "blue"]
        
        let set = new Set(['red', 'green', 'blue']);
        for (let x of set) {
          console.log(x);
        }
        // red
        // green
        // blue
        
        let set = new Set([1, 4, 9]);
        set.forEach((value, key) => console.log(key + ' : ' + value))
        // 1 : 1
        // 4 : 4
        // 9 : 9
        
  2. WeakSet

    • 与 Set 有两个区别
      • 成员只能是对象和 Symbol 值
      • 对象都是弱引用,即如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存
  3. Map

    • 类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键

    • 任何具有 Iterator 接口、且每个成员都是一个双元素的数组的数据结构都可以当作Map构造函数的参数

    • 读取一个未知的键,则返回undefined

    • Map实例的属性与方法

      • size:返回 Map 结构的成员总数
      • Map.prototype.set(key, value):设置键名key对应的键值为value,然后返回整个 Map 结构。如果key已经有值,则键值会被更新,否则就新生成该键
      • Map.prototype.get(key):读取key对应的键值,如果找不到key,返回undefined
      • Map.prototype.has(key):返回一个布尔值,表示某个键是否在当前 Map 对象之中
      • Map.prototype.delete(key):删除某个键,返回true。如果删除失败,返回false
      • Map.prototype.clear():清除所有成员,没有返回值
    • 遍历方法

      • Map.prototype.keys():返回键名的遍历器。
      • Map.prototype.values():返回键值的遍历器。
      • Map.prototype.entries():返回所有成员的遍历器。
      • Map.prototype.forEach():遍历 Map 的所有成员。
      • 可以接受第二个参数,用来绑定this
    • 与其他数据结构的相互转换

      • Map转数组

        const myMap = new Map()
          .set(true, 7)
          .set({foo: 3}, ['abc']);
        [...myMap]
        // [ [ true, 7 ], [ { foo: 3 }, [ 'abc' ] ] ]
        
      • 数组转Map

        new Map([
          [true, 7],
          [{foo: 3}, ['abc']]
        ])
        // Map {
        //   true => 7,
        //   Object {foo: 3} => ['abc']
        // }
        
      • Map转对象

        function strMapToObj(strMap) {
          let obj = Object.create(null);
          for (let [k,v] of strMap) {
            obj[k] = v;
          }
          return obj;
        }
        
        const myMap = new Map()
          .set('yes', true)
          .set('no', false);
        strMapToObj(myMap)
        // { yes: true, no: false }
        
      • 对象转Map

        let obj = {"a":1, "b":2};
        let map = new Map(Object.entries(obj));
        
      • Map转JSON

        • Map 的键名都是字符串,可以转为对象 JSON

          function strMapToJson(strMap) {
            return JSON.stringify(strMapToObj(strMap));
          }
          
          let myMap = new Map().set('yes', true).set('no', false);
          strMapToJson(myMap)
          // '{"yes":true,"no":false}'
          
        • Map 的键名有非字符串,可以转为数组 JSON

      • JSON转Map

        function jsonToStrMap(jsonStr) {
          return objToStrMap(JSON.parse(jsonStr));
        }
        
        jsonToStrMap('{"yes": true, "no": false}')
        // Map {'yes' => true, 'no' => false}
        
  4. WeakMap

    • Map的区别有两点

      • 只接受对象(null除外)和 Symbol 值作为键名,不接受其他类型的值作为键名
      • WeakMap的键名所指向的对象,不计入垃圾回收机制
    • 用途

      • DOM 节点作为键名

        let myWeakmap = new WeakMap();
        
        myWeakmap.set(
          document.getElementById('logo'),
          {timesClicked: 0})
        ;
        
        document.getElementById('logo').addEventListener('click', function() {
          let logoData = myWeakmap.get(document.getElementById('logo'));
          logoData.timesClicked++;
        }, false);
        
      • 部署私有属性

        const _counter = new WeakMap();
        const _action = new WeakMap();
        
        class Countdown {
          constructor(counter, action) {
            _counter.set(this, counter);
            _action.set(this, action);
          }
          dec() {
            let counter = _counter.get(this);
            if (counter < 1) return;
            counter--;
            _counter.set(this, counter);
            if (counter === 0) {
              _action.get(this)();
            }
          }
        }
        
        const c = new Countdown(2, () => console.log('DONE'));
        
        c.dec()
        c.dec()
        // DONE
        
  5. WeakRef

    • 直接创建对象的弱引用

      let target = {};
      let wr = new WeakRef(target);
      // 垃圾回收机制不会计入wr这个引用,也就是说,wr的引用不会妨碍原始对象target被垃圾回收机制清除
      
  6. FinalizationRegistry

    • 用来指定目标对象被垃圾回收机制清除以后,所要执行的回调函数

      const registry = new FinalizationRegistry(heldValue => {
        // ....
      });
      
      registry.register(theObject, "some value");	// 用来注册所要观察的目标对象
      

Proxy

  1. 概述

    • 在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,可以对外界的访问进行过滤和改写

    • ES6 原生提供 Proxy 构造函数

      var proxy = new Proxy(target, handler);
      // target参数表示所要拦截的目标对象,handler参数也是一个对象,用来定制拦截行为
      
    • 小技巧

      • 将 Proxy 对象,设置到object.proxy属性,从而可以在object对象上调用

        var object = { proxy: new Proxy(target, handler) };
        
    • 支持的拦截操作

      • get(target, propKey, receiver):拦截对象属性的读取,比如proxy.fooproxy['foo']
      • set(target, propKey, value, receiver):拦截对象属性的设置,比如proxy.foo = vproxy['foo'] = v,返回一个布尔值。
      • has(target, propKey):拦截propKey in proxy的操作,返回一个布尔值。
      • deleteProperty(target, propKey):拦截delete proxy[propKey]的操作,返回一个布尔值。
      • ownKeys(target):拦截Object.getOwnPropertyNames(proxy)Object.getOwnPropertySymbols(proxy)Object.keys(proxy)for...in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。
      • getOwnPropertyDescriptor(target, propKey):拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。
      • defineProperty(target, propKey, propDesc):拦截Object.defineProperty(proxy, propKey, propDesc)Object.defineProperties(proxy, propDescs),返回一个布尔值。
      • preventExtensions(target):拦截Object.preventExtensions(proxy),返回一个布尔值。
      • getPrototypeOf(target):拦截Object.getPrototypeOf(proxy),返回一个对象。
      • isExtensible(target):拦截Object.isExtensible(proxy),返回一个布尔值。
      • setPrototypeOf(target, proto):拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。
      • apply(target, object, args):拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)proxy.call(object, ...args)proxy.apply(...)
      • construct(target, args):拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(...args)
  2. Proxy 实例的方法

    • get()

      • 接受三个参数,依次为目标对象、属性名、 proxy 实例本身(可选)
    • set()

      • 接受四个参数,依次为目标对象、属性名、属性值、 Proxy 实例本身(可选)
    • apply()

      • 拦截函数的调用、callapply操作

      • 接受三个参数,分别是目标对象、目标对象的上下文对象(this)和目标对象的参数数组

        var target = function () { return 'I am the target'; };
        var handler = {
          apply: function () {
            return 'I am the proxy';
          }
        };
        
        var p = new Proxy(target, handler);
        
        p()
        // "I am the proxy"
        
    • has()

      • 用来拦截HasProperty操作,即判断对象是否具有某个属性时,这个方法会生效,对for...in循环不生效

      • 接受两个参数,分别是目标对象、需查询的属性名

        // 隐藏某些属性
        var handler = {
          has (target, key) {
            if (key[0] === '_') {
              return false;
            }
            return key in target;
          }
        };
        var target = { _prop: 'foo', prop: 'foo' };
        var proxy = new Proxy(target, handler);
        '_prop' in proxy // false
        
    • construct()

      • 用于拦截new命令

      • 接受三个参数,目标对象、构造函数的参数数组、创造实例对象时new命令作用的构造函数、

      • 方法中的this指向的是handler

        const p = new Proxy(function () {}, {
          construct: function(target, args) {
            console.log('called: ' + args.join(', '));
            return { value: args[0] * 10 };
          }
        });
        
        (new p(1)).value
        // "called: 1"
        // 10
        
    • deleteProperty()

      • 用于拦截delete操作,如果这个方法抛出错误或者返回false,当前属性就无法被delete命令删除

        var handler = {
          deleteProperty (target, key) {
            invariant(key, 'delete');
            delete target[key];
            return true;
          }
        };
        
    • defineProperty()

      • 拦截Object.defineProperty()操作

        var handler = {
          defineProperty (target, key, descriptor) {
            return false;
          }
        };
        var target = {};
        var proxy = new Proxy(target, handler);
        proxy.foo = 'bar' // 不会生效
        
    • getOwnPropertyDescriptor()

      • 拦截Object.getOwnPropertyDescriptor(),返回一个属性描述对象或者undefined
    • getPrototypeOf()

      • 拦截获取对象原型,拦截以下操作
        • Object.prototype.__proto__
        • Object.prototype.isPrototypeOf()
        • Object.getPrototypeOf()
        • Reflect.getPrototypeOf()
        • instanceof
    • isExtensible()

      • 拦截Object.isExtensible()操作
    • ownKeys()

      • 拦截对象自身属性的读取操作,拦截以下操作
        • Object.getOwnPropertyNames()
        • Object.getOwnPropertySymbols()
        • Object.keys()
        • for...in循环
    • preventExtensions()

      • 拦截Object.preventExtensions()。该方法必须返回一个布尔值,否则会被自动转为布尔值

      • 限制

        • 只有目标对象不可扩展时(即Object.isExtensible(proxy)false),proxy.preventExtensions才能返回true,否则会报错

        • 解决方法(在proxy.preventExtensions()方法里面,调用一次Object.preventExtensions()

          var proxy = new Proxy({}, {
            preventExtensions: function(target) {
              console.log('called');
              Object.preventExtensions(target);
              return true;
            }
          });
          
          Object.preventExtensions(proxy)
          // "called"
          // Proxy {}
          
    • setPrototypeOf()

      • 拦截Object.setPrototypeOf()方法
  3. Proxy.revocable()

    • 返回一个可取消的 Proxy 实例
    • 执行revoke函数之后,再访问Proxy实例,就会抛出一个错误
    • 使用场景
      • 目标对象不允许直接访问,必须通过代理访问,一旦访问结束,就收回代理权,不允许再次访问
  4. this 问题

    • 在 Proxy 代理的情况下,目标对象内部的this关键字会指向 Proxy 代理

      const target = {
        m: function () {
          console.log(this === proxy);
        }
      };
      const handler = {};
      
      const proxy = new Proxy(target, handler);
      
      target.m() // false
      proxy.m()  // true
      
    • Proxy 拦截函数内部的this,指向的是handler对象

  5. 实例:Web 服务的客户端

    function createWebService(baseUrl) {
      return new Proxy({}, {
        get(target, propKey, receiver) {
          return () => httpGet(baseUrl + '/' + propKey);
        }
      });
    }
    

Reflect

  1. 概述

    • 设计目的

      • Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上,现阶段,某些方法同时在ObjectReflect对象上部署,未来的新方法将只部署在Reflect对象上

      • 修改某些Object方法的返回结果,让其变得更合理

        // 老写法
        try {
          Object.defineProperty(target, property, attributes);
          // success
        } catch (e) {
          // failure
        }
        
        // 新写法
        if (Reflect.defineProperty(target, property, attributes)) {
          // success
        } else {
          // failure
        }
        
      • Object操作都变成函数行为

        // 老写法
        'assign' in Object // true
        
        // 新写法
        Reflect.has(Object, 'assign') // true
        
      • Reflect对象的方法与Proxy对象的方法一一对应

  2. 静态方法

    • Reflect.apply(target, thisArg, args)

      • 用于绑定this对象后执行给定函数

        const ages = [11, 33, 12, 54, 18, 96];
        
        // 旧写法
        const youngest = Math.min.apply(Math, ages);
        const oldest = Math.max.apply(Math, ages);
        const type = Object.prototype.toString.call(youngest);
        
        // 新写法
        const youngest = Reflect.apply(Math.min, Math, ages);
        const oldest = Reflect.apply(Math.max, Math, ages);
        const type = Reflect.apply(Object.prototype.toString, youngest, []);
        
    • Reflect.construct(target, args)

      • 调用构造函数的方法

        function Greeting(name) {
          this.name = name;
        }
        
        // new 的写法
        const instance = new Greeting('张三');
        
        // Reflect.construct 的写法
        const instance = Reflect.construct(Greeting, ['张三']);
        
    • Reflect.get(target, name, receiver)

      • 查找并返回target对象的name属性,如果没有该属性,则返回undefined

        var myObject = {
          foo: 1,
          bar: 2,
          get baz() {
            return this.foo + this.bar;
          },
        }
        
        Reflect.get(myObject, 'foo') // 1
        Reflect.get(myObject, 'bar') // 2
        Reflect.get(myObject, 'baz') // 3
        
    • Reflect.set(target, name, value, receiver)

      • 设置target对象的name属性等于value

        var myObject = {
          foo: 1,
          set bar(value) {
            return this.foo = value;
          },
        }
        
        myObject.foo // 1
        
        Reflect.set(myObject, 'foo', 2);
        myObject.foo // 2
        
    • Reflect.defineProperty(target, name, desc)

      • 用来为对象定义属性

        function MyDate() {
          /*…*/
        }
        
        // 旧写法
        Object.defineProperty(MyDate, 'now', {
          value: () => Date.now()
        });
        
        // 新写法
        Reflect.defineProperty(MyDate, 'now', {
          value: () => Date.now()
        });
        
    • Reflect.deleteProperty(target, name)

      • 用于删除对象的属性

      • 如果删除成功,或者被删除的属性不存在,返回true;删除失败,被删除的属性依然存在,返回false

        const myObj = { foo: 'bar' };
        
        // 旧写法
        delete myObj.foo;
        
        // 新写法
        Reflect.deleteProperty(myObj, 'foo');
        
    • Reflect.has(target, name)

      • 对应name in obj里面的in运算符

        var myObject = {
          foo: 1,
        };
        
        // 旧写法
        'foo' in myObject // true
        
        // 新写法
        Reflect.has(myObject, 'foo') // true
        
    • Reflect.ownKeys(target)

      • 用于返回对象的所有属性

        var myObject = {
          foo: 1,
          bar: 2,
          [Symbol.for('baz')]: 3,
          [Symbol.for('bing')]: 4,
        };
        
        // 旧写法
        Object.getOwnPropertyNames(myObject)
        // ['foo', 'bar']
        
        Object.getOwnPropertySymbols(myObject)
        //[Symbol(baz), Symbol(bing)]
        
        // 新写法
        Reflect.ownKeys(myObject)
        // ['foo', 'bar', Symbol(baz), Symbol(bing)]
        
    • Reflect.isExtensible(target)

      • 返回一个布尔值,表示当前对象是否可扩展

        const myObject = {};
        
        // 旧写法
        Object.isExtensible(myObject) // true
        
        // 新写法
        Reflect.isExtensible(myObject) // true
        
    • Reflect.preventExtensions(target)

      • 用于让一个对象变为不可扩展。它返回一个布尔值,表示是否操作成功

        var myObject = {};
        
        // 旧写法
        Object.preventExtensions(myObject) // Object {}
        
        // 新写法
        Reflect.preventExtensions(myObject) // true
        
    • Reflect.getOwnPropertyDescriptor(target, name)

      • 用于得到指定属性的描述对象

        var myObject = {};
        Object.defineProperty(myObject, 'hidden', {
          value: true,
          enumerable: false,
        });
        
        // 旧写法
        var theDescriptor = Object.getOwnPropertyDescriptor(myObject, 'hidden');
        
        // 新写法
        var theDescriptor = Reflect.getOwnPropertyDescriptor(myObject, 'hidden');
        
    • Reflect.getPrototypeOf(target)

      • 用于读取对象的__proto__属性

        const myObj = new FancyThing();
        
        // 旧写法
        Object.getPrototypeOf(myObj) === FancyThing.prototype;
        
        // 新写法
        Reflect.getPrototypeOf(myObj) === FancyThing.prototype;
        
    • Reflect.setPrototypeOf(target, prototype)

      • 设置目标对象的原型,它返回一个布尔值,表示是否设置成功。

        const myObj = {};
        
        // 旧写法
        Object.setPrototypeOf(myObj, Array.prototype);
        
        // 新写法
        Reflect.setPrototypeOf(myObj, Array.prototype);
        
        myObj.length // 0
        
  3. 实例:使用 Proxy 实现观察者模式

    • 函数自动观察数据对象,一旦对象有变化,函数就会自动执行

      const queuedObservers = new Set();
      
      const observe = fn => queuedObservers.add(fn);
      const observable = obj => new Proxy(obj, {set});
      
      function set(target, key, value, receiver) {
        const result = Reflect.set(target, key, value, receiver);
        queuedObservers.forEach(observer => observer());
        return result;
      }
      

Promise对象

  1. Promise 的含义

    • 异步编程的解决方案
    • 特点
      • 对象状态不受外界影响,只有异步操作的结果可以决定当前的状态:pending(准备中)、fulfilled(已成功)、rejected(已失败)
      • 一旦状态改变,就不会再变,任何时候都可以得到这个结果
  2. 基本用法

    • 接受一个函数作为参数,该函数的两个参数分别是resolvereject
    • resolve函数:将Promise对象的状态从“未完成”变为“成功”
    • reject函数:将Promise对象的状态从“未完成”变为“失败”
    • then方法分别指定resolved状态和rejected状态的回调函数
      • 接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为resolved时调用,第二个回调函数是Promise对象的状态变为rejected时调用。这两个函数都是可选的
    • Promise 新建后就会立即执行
  3. Promise.prototype.then()

    getJSON("/post/1.json").then(
      post => getJSON(post.commentURL)
    ).then(
      comments => console.log("resolved: ", comments),
      err => console.log("rejected: ", err)
    );
    
  4. Promise.prototype.catch()

    • 用于指定发生错误时的回调函数

      promise
        .then(function(data) { //cb
          // success
        })
        .catch(function(err) {
          // error
        });
      
  5. Promise.prototype.finally()

    • 不管 Promise 对象最后状态如何,都会执行的操作

      server.listen(port)
        .then(function () {
          // ...
        })
        .finally(server.stop);
      
  6. Promise.all()

    • 用于将多个 Promise 实例,包装成一个新的 Promise 实例

      const p = Promise.all([p1, p2, p3]);
      
    • 接受一个数组作为参数,p1p2p3都是 Promise 实例,如果不是,就会先调用Promise.resolve方法,将参数转为 Promise 实例(可以不是数组,但必须具有 Iterator 接口)

    • 只有p1p2p3的状态都变成fulfilledp的状态才会变成fulfilled,有一个被rejectedp的状态就变成rejected

    • 没有自己的catch方法,才会调用Promise.all()catch方法

      const p1 = new Promise((resolve, reject) => {
        resolve('hello');
      })
      .then(result => result)
      .catch(e => e);
      
      const p2 = new Promise((resolve, reject) => {
        throw new Error('报错了');
      })
      .then(result => result)
      .catch(e => e);
      
      Promise.all([p1, p2])
      .then(result => console.log(result))
      .catch(e => console.log(e));
      // ["hello", Error: 报错了]
      
  7. Promise.race()

    • 将多个 Promise 实例,包装成一个新的 Promise 实例
    • 只要p1p2p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数
  8. Promise.allSettled()

    • 用来确定一组异步操作是否都结束了(不管成功或失败)

    • 接受一个数组作为参数,数组的每个成员都是一个 Promise 对象,并返回一个新的 Promise 对象

    • 只有等到参数数组的所有 Promise 对象都发生状态变更,返回的 Promise 对象才会发生状态变更

      const resolved = Promise.resolve(42);
      const rejected = Promise.reject(-1);
      
      const allSettledPromise = Promise.allSettled([resolved, rejected]);
      
      allSettledPromise.then(function (results) {
        console.log(results);
      });
      // [
      //    { status: 'fulfilled', value: 42 },
      //    { status: 'rejected', reason: -1 }
      // ]
      
      // 过滤出成功的请求
      const successfulPromises = results.filter(p => p.status === 'fulfilled');
      
      // 过滤出失败的请求,并输出原因
      const errors = results
        .filter(p => p.status === 'rejected')
        .map(p => p.reason);
      
  9. Promise.any()

    • 接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例返回
    • 只要参数实例有一个变成fulfilled状态,实例就会变成fulfilled状态;所有参数实例都变成rejected状态,就会变成rejected状态
  10. Promise.resolve()

    • 将现有对象转为 Promise 对象

      Promise.resolve('foo')
      // 等价于
      new Promise(resolve => resolve('foo'))
      
  11. Promise.reject()

    • 返回一个新的 Promise 实例,该实例的状态为rejected
  12. 应用

    • 加载图片

      const preloadImage = function (path) {
        return new Promise(function (resolve, reject) {
          const image = new Image();
          image.onload  = resolve;
          image.onerror = reject;
          image.src = path;
        });
      };
      
    • Generator函数与Promise的结合

      function getFoo () {
        return new Promise(function (resolve, reject){
          resolve('foo');
        });
      }
      
      const g = function* () {
        try {
          const foo = yield getFoo();
          console.log(foo);
        } catch (e) {
          console.log(e);
        }
      };
      
      function run (generator) {
        const it = generator();
      
        function go(result) {
          if (result.done) return result.value;
      
          return result.value.then(function (value) {
            return go(it.next(value));
          }, function (error) {
            return go(it.throw(error));
          });
        }
      
        go(it.next());
      }
      
      run(g);
      
  13. Promise.try()

    • 所有操作提供了统一的处理机制(同步函数与异步操作)

      const f = () => console.log('now');
      Promise.try(f);
      console.log('next');
      // now
      // next
      

Iterator和for...of循环

  1. Iterator的概念

    • 主要为数组、对象、MapSet
    • 遍历过程
      • 创建一个指针对象,指向当前数据结构的起始位置
      • 第一次调用指针对象的next方法,指向数据结构的第一个成员
      • 不断调用指针对象的next方法,直到它指向数据结构的结束位置
      • next方法返回一个包含valuedone两个属性的对象。其中,value属性是当前成员的值,done属性是一个布尔值,表示遍历是否结束
  2. 默认 Iterator 接口

    • 为所有数据结构,提供了一种统一的访问机制

    • 一个数据结构只要具有Symbol.iterator属性,就可以认为是“可遍历的”

    • Symbol.iterator属性本身是一个函数,执行这个函数,就会返回一个遍历器

      let arr = ['a', 'b', 'c'];
      let iter = arr[Symbol.iterator]();
      
      iter.next() // { value: 'a', done: false }
      iter.next() // { value: 'b', done: false }
      iter.next() // { value: 'c', done: false }
      iter.next() // { value: undefined, done: true }
      
    • 为对象部署Interator接口

      let obj = {
        data: [ 'hello', 'world' ],
        [Symbol.iterator]() {
          const self = this;
          let index = 0;
          return {
            next() {
              if (index < self.data.length) {
                return {
                  value: self.data[index++],
                  done: false
                };
              }
              return { value: undefined, done: true };
            }
          };
        }
      };
      
    • 对于类似数组的对象(存在数值键名和length属性)部署 Iterator 接口

      let iterable = {
        0: 'a',
        1: 'b',
        2: 'c',
        length: 3,
        [Symbol.iterator]: Array.prototype[Symbol.iterator]
      };
      for (let item of iterable) {
        console.log(item); // 'a', 'b', 'c'
      }
      
  3. 字符串的 Iterator 接口

    var someString = "hi";
    typeof someString[Symbol.iterator]
    // "function"
    
    var iterator = someString[Symbol.iterator]();
    
    iterator.next()  // { value: "h", done: false }
    iterator.next()  // { value: "i", done: false }
    iterator.next()  // { value: undefined, done: true }
    
  4. Iterator 接口与 Generator 函数

    // Symbol.iterator()方法的最简单实现
    
    var someString = "hi";
    typeof someString[Symbol.iterator]
    // "function"
    
    var iterator = someString[Symbol.iterator]();
    
    iterator.next()  // { value: "h", done: false }
    iterator.next()  // { value: "i", done: false }
    iterator.next()  // { value: undefined, done: true }
    
  5. 遍历器对象的 return(),throw()

    • return()方法必须返回一个对象

      function readLinesSync(file) {
        return {
          [Symbol.iterator]() {
            return {
              next() {
                return { done: false };
              },
              return() {
                file.close();
                return { done: true };
              }
            };
          },
        };
      }
      
  6. for...of 循环

    • for...of循环内部调用的是数据结构的Symbol.iterator方法

    • 对象

      • for...of与Object.keys配合使用

        for (var key of Object.keys(someObject)) {
          console.log(key + ': ' + someObject[key]);
        }
        
      • 使用generator函数重新包装

        const obj = { a: 1, b: 2, c: 3 }
        
        function* entries(obj) {
          for (let key of Object.keys(obj)) {
            yield [key, obj[key]];
          }
        }
        
        for (let [key, value] of entries(obj)) {
          console.log(key, '->', value);
        }
        // a -> 1
        // b -> 2
        // c -> 3
        

Generator函数

  1. 简介

    • 一个状态机,封装了多个内部状态

    • 特征

      • function*函数名{}
      • 内部使用yield表达式
    • 分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行

    • next方法的运行逻辑

      • 遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。
      • 下一次调用next方法时,再继续往下执行,直到遇到下一个yield表达式。
      • 如果没有再遇到新的yield表达式,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值。
      • 如果该函数没有return语句,则返回的对象的value属性值为undefined
    • yield表达式如果用在另一个表达式之中,必须放在圆括号里面

      function* demo() {
        console.log('Hello' + yield); // SyntaxError
        console.log('Hello' + yield 123); // SyntaxError
      
        console.log('Hello' + (yield)); // OK
        console.log('Hello' + (yield 123)); // OK
      }
      
  2. next 方法的参数

    • 可以带一个参数,该参数就会被当作上一个yield表达式的返回值

      function* foo(x) {
        var y = 2 * (yield (x + 1));
        var z = yield (y / 3);
        return (x + y + z);
      }
      
      var a = foo(5);
      a.next() // Object{value:6, done:false}
      a.next() // Object{value:NaN, done:false}
      a.next() // Object{value:NaN, done:true}
      
      var b = foo(5);
      b.next() // { value:6, done:false }
      b.next(12) // { value:8, done:false }
      b.next(13) // { value:42, done:true }
      
    • 想要第一次调用就输入值可以在Generator函数外面再包一层

      function wrapper(generatorFunction) {
        return function (...args) {
          let generatorObject = generatorFunction(...args);
          generatorObject.next();
          return generatorObject;
        };
      }
      
      const wrapped = wrapper(function* () {
        console.log(`First input: ${yield}`);
        return 'DONE';
      });
      
      wrapped().next('hello!')
      // First input: hello!
      
  3. for...of 循环

    function* numbers () {
      yield 1
      yield 2
      return 3
      yield 4
    }
    
    // 扩展运算符
    [...numbers()] // [1, 2]
    
    // Array.from 方法
    Array.from(numbers()) // [1, 2]
    
    // 解构赋值
    let [x, y] = numbers();
    x // 1
    y // 2
    
    // for...of 循环
    for (let n of numbers()) {
      console.log(n)
    }
    // 1
    // 2
    
  4. Generator.prototype.throw()

    • 如果 Generator 函数内部没有部署try...catch代码块,那么throw方法抛出的错误,将被外部try...catch代码块捕获
    • throw方法被内部捕获以后,会附带执行到下一条yield表达式,这种情况下等同于执行一次next方法
  5. Generator.prototype.return()

    • 可以返回给定的值,并且终结遍历 Generator 函数

    • return()方法调用时,不提供参数,则返回值的value属性为undefined

      
      function* gen() {
        yield 1;
        yield 2;
        yield 3;
      }
      
      var g = gen();
      
      g.next()        // { value: 1, done: false }
      g.return('foo') // { value: "foo", done: true }
      g.next()        // { value: undefined, done: true }
      
  6. yield* 表达式

    • 用来在一个 Generator 函数里面执行另一个 Generator 函数

      function* inner() {
        yield 'hello!';
      }
      
      function* outer1() {
        yield 'open';
        yield inner();
        yield 'close';
      }
      
      var gen = outer1()
      gen.next().value // "open"
      gen.next().value // 返回一个遍历器对象
      gen.next().value // "close"
      
      function* outer2() {
        yield 'open'
        yield* inner()
        yield 'close'
      }
      
      var gen = outer2()
      gen.next().value // "open"
      gen.next().value // "hello!"
      gen.next().value // "close"
      
  7. 作为对象属性的 Generator 函数

    • 简写

      let obj = {
        * myGeneratorMethod() {
          ···
        }
      };
      
  8. Generator 函数的this

    • gen返回的总是遍历器对象,而不是this对象,使用call方法可以绑定 Generator 函数内部的this
    function* gen() {
      this.a = 1;
      yield this.b = 2;
      yield this.c = 3;
    }
    
    function F() {
      return gen.call(gen.prototype);
    }
    
    var f = new F();
    
    f.next();  // Object {value: 2, done: false}
    f.next();  // Object {value: 3, done: false}
    f.next();  // Object {value: undefined, done: true}
    
    f.a // 1
    f.b // 2
    f.c // 3
    
  9. 含义

    • Generator 函数被称为“半协程”,意思是只有 Generator 函数的调用者,才能将程序的执行权还给 Generator 函数。如果将 Generator 函数当作协程,完全可以将多个需要互相协作的任务写成 Generator 函数,它们之间使用yield表达式交换控制权。
  10. 应用

    • 异步操作的同步化表达

      function* loadUI() {
        showLoadingScreen();
        yield loadUIDataAsynchronously();
        hideLoadingScreen();
      }
      var loader = loadUI();
      // 加载UI
      loader.next()
      
      // 卸载UI
      loader.next()
      
    • 控制流管理

      let steps = [step1Func, step2Func, step3Func];
      
      function* iterateSteps(steps){
        for (var i=0; i< steps.length; i++){
          var step = steps[i];
          yield step();
        }
      }
      
    • 部署Interator接口

      function* iterEntries(obj) {
        let keys = Object.keys(obj);
        for (let i=0; i < keys.length; i++) {
          let key = keys[i];
          yield [key, obj[key]];
        }
      }
      
      let myObj = { foo: 3, bar: 7 };
      
      for (let [key, value] of iterEntries(myObj)) {
        console.log(key, value);
      }
      
      // foo 3
      // bar 7
      

Generator函数的异步应用

  1. 传统方法

    • 回调函数
    • 事件监听
    • 发布/订阅
    • Promise 对象
  2. Generator 函数

    • 异步任务的封装

      var fetch = require('node-fetch');
      
      function* gen(){
        var url = 'https://api.github.com/users/github';
        var result = yield fetch(url);
        console.log(result.bio);
      }
      
      var g = gen();
      var result = g.next();
      
      result.value.then(function(data){
        return data.json();
      }).then(function(data){
        g.next(data);
      });
      
  3. Thunk 函数(可以自动执行 Generator 函数)

    • 任何函数,只要参数有回调函数,就能写成 Thunk 函数的形式。下面是一个简单的 Thunk 函数转换器。

      const Thunk = function(fn) {
        return function (...args) {
          return function (callback) {
            return fn.call(this, ...args, callback);
          }
        };
      };
      
    • 基于 Thunk 函数的 Generator 执行器

      function run(fn) {
        var gen = fn();
      
        function next(err, data) {
          var result = gen.next(data);
          if (result.done) return;
          result.value(next);
        }
      
        next();
      }
      
      function* g() {
        // ...
      }
      
      run(g);
      
  4. co 模块

    • 用于 Generator 函数的自动执行,co函数返回一个Promise对象,因此可以用then方法添加回调函数

      var co = require('co');
      co(gen);
      co(gen).then(function (){
        console.log('Generator 函数执行完成');
      });
      

async函数

  1. 含义

    • Generator函数的语法糖

      const gen = function* () {
        const f1 = yield readFile('/etc/fstab');
        const f2 = yield readFile('/etc/shells');
        console.log(f1.toString());
        console.log(f2.toString());
      };
      
      // 可以写成
      const asyncReadFile = async function () {
        const f1 = await readFile('/etc/fstab');
        const f2 = await readFile('/etc/shells');
        console.log(f1.toString());
        console.log(f2.toString());
      };
      
    • 改进

      • 内置执行器,可直接调用函数
      • 返回值是Promise
  2. 基本用法

    // 函数声明
    async function foo() {}
    
    // 函数表达式
    const foo = async function () {};
    
    // 对象的方法
    let obj = { async foo() {} };
    obj.foo().then(...)
    
    // Class 的方法
    class Storage {
      constructor() {
        this.cachePromise = caches.open('avatars');
      }
    
      async getAvatar(name) {
        const cache = await this.cachePromise;
        return cache.match(`/avatars/${name}.jpg`);
      }
    }
    
    const storage = new Storage();
    storage.getAvatar('jake').then(…);
    
    // 箭头函数
    const foo = async () => {};
    
  3. 语法

    • 函数内部return语句返回的值,会成为then方法回调函数的参数

      async function f() {
        return 'hello world';
      }
      
      f().then(v => console.log(v))
      // "hello world"
      
    • 错误处理

      async function main() {
        try {
          const val1 = await firstStep();
          const val2 = await secondStep(val1);
          const val3 = await thirdStep(val1, val2);
      
          console.log('Final: ', val3);
        }
        catch (err) {
          console.error(err);
        }
      }
      
    • 注意点

      • 最好把await命令放在try...catch代码块中

      • 多个await命令后面的异步操作,如果不存在继发关系,最好让它们同时触发

        let foo = await getFoo();
        let bar = await getBar();
        let [foo, bar] = await Promise.all([getFoo(), getBar()]);
        
      • async 函数可以保留运行堆栈

        const a = async () => {
          await b();
          c();
        };
        // b()运行的时候,a()是暂停执行,上下文环境都保存着。一旦b()或c()报错,错误堆栈将包括a()
        
  4. async 函数的实现原理:Generator函数和自动执行器包装在一个函数里

  5. 实例:按顺序完成异步操作

    async function logInOrder(urls) {
      // 并发读取远程URL
      const textPromises = urls.map(async url => {
        const response = await fetch(url);
        return response.text();
      });
    
      // 按次序输出
      for (const textPromise of textPromises) {
        console.log(await textPromise);
      }
    }
    
  6. 顶层 await

    • 允许在模块的顶层独立使用await命令,主要目的是使用await解决模块异步加载的问题

      // import() 方法加载
      const strings = await import(`/i18n/${navigator.language}`);
      
      // 数据库操作
      const connection = await dbConnector();
      
      // 依赖回滚
      let jQuery;
      try {
        jQuery = await import('https://cdn-a.com/jQuery');
      } catch {
        jQuery = await import('https://cdn-b.com/jQuery');
      }
      

Class的基本语法

  1. 类的由来

    • 类的方法都定义在prototype对象上面,所以类的新方法可以添加在prototype对象上面。Object.assign()方法可以很方便地一次向类添加多个方法

      class Point {
        constructor(){
          // ...
        }
      }
      
      Object.assign(Point.prototype, {
        toString(){},
        toValue(){}
      });
      
  2. constructor() 方法

    • 一个类必须有constructor()方法,如果没有显式定义,一个空的constructor()方法会被默认添加
  3. 类的实例

    • 类的属性和方法,除非显式定义在其本身(即定义在this对象上),否则都是定义在原型上(即定义在class上)

      class Point {
        constructor(x, y) {
          this.x = x;
          this.y = y;
        }
      
        toString() {
          return '(' + this.x + ', ' + this.y + ')';
        }
      }
      
      var point = new Point(2, 3);
      
      point.toString() // (2, 3)
      
      point.hasOwnProperty('x') // true
      point.hasOwnProperty('y') // true
      point.hasOwnProperty('toString') // false
      point.__proto__.hasOwnProperty('toString') // true
      
  4. 实例属性的新写法

    • 实例属性现在除了可以定义在constructor()方法里面的this上面,也可以定义在类内部的最顶层

      class IncreasingCounter {
        _count = 0;
        get value() {
          console.log('Getting the current value!');
          return this._count;
        }
        increment() {
          this._count++;
        }
      }
      
  5. 取值函数(getter)和存值函数(setter)

    • 对某个属性设置存值函数和取值函数,拦截该属性的存取行为

    • 存值函数和取值函数是设置在属性的 Descriptor 对象上的

      class CustomHTMLElement {
        constructor(element) {
          this.element = element;
        }
      
        get html() {
          return this.element.innerHTML;
        }
      
        set html(value) {
          this.element.innerHTML = value;
        }
      }
      
      var descriptor = Object.getOwnPropertyDescriptor(
        CustomHTMLElement.prototype, "html"
      );
      
      "get" in descriptor  // true
      "set" in descriptor  // true
      
  6. 属性表达式

    • 类的属性名,可以采用表达式

      let methodName = 'getArea';
      
      class Square {
        constructor(length) {
          // ...
        }
      
        [methodName]() {
          // ...
        }
      }
      
  7. Class 表达式

    • 类也可以使用表达式的形式定义

      const MyClass = class Me {
        getClassName() {
          return Me.name;
        }
      };
      
      // 注意点:这个类的名字是Me,但是Me只在 Class 的内部可用,指代当前类。在 Class 外部,这个类只能用MyClass引用
      let inst = new MyClass();
      inst.getClassName() // Me
      Me.name // ReferenceError: Me is not defined
      
      // 如果类的内部没用到的话,可以省略Me
      const MyClass = class { /* ... */ };
      
    • 立即执行的 Class

      let person = new class {
        constructor(name) {
          this.name = name;
        }
      
        sayName() {
          console.log(this.name);
        }
      }('张三');
      
      person.sayName(); // "张三"
      
  8. 静态方法

    • 在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用

    • 静态方法包含this关键字,这个this指的是类,而不是实例

    • 父类的静态方法,可以被子类继承

      class Foo {
        static classMethod() {
          return 'hello';
        }
      }
      
      class Bar extends Foo {
      }
      
      Bar.classMethod() // 'hello'
      
    • 静态方法可以从super对象上调用的

      class Foo {
        static classMethod() {
          return 'hello';
        }
      }
      
      class Bar extends Foo {
        static classMethod() {
          return super.classMethod() + ', too';
        }
      }
      
      Bar.classMethod() // "hello, too"
      
  9. 静态属性

    • 在实例属性的前面,加上static关键字

      class MyClass {
        static myStaticProp = 42;
      
        constructor() {
          console.log(MyClass.myStaticProp); // 42
        }
      }
      
  10. 私有方法和私有属性

    • 方法一:在命名上加以区别

      class Widget {
      
        // 公有方法
        foo (baz) {
          this._bar(baz);
        }
      
        // 私有方法
        _bar(baz) {
          return this.snaf = baz;
        }
      
        // ...
      }
      
    • 方法二:将私有方法移出类

      lass Widget {
        foo (baz) {
          bar.call(this, baz);
        }
      
        // ...
      }
      
      function bar(baz) {
        return this.snaf = baz;
      }
      
    • 方法三:利用Symbol值得唯一性

      const bar = Symbol('bar');
      const snaf = Symbol('snaf');
      
      export default class myClass{
      
        // 公有方法
        foo(baz) {
          this[bar](baz);
        }
      
        // 私有方法
        [bar](baz) {
          return this[snaf] = baz;
        }
      
        // ...
      };
      
      // Reflect.ownKeys()依然可以拿到
      const inst = new myClass();
      
      Reflect.ownKeys(myClass.prototype)
      // [ 'constructor', 'foo', Symbol(bar) ]
      
    • 私有属性是在属性名之前使用#表示

      class IncreasingCounter {
        #count = 0;
        get value() {
          console.log('Getting the current value!');
          return this.#count;
        }
        increment() {
          this.#count++;
        }
      }
      
      const counter = new IncreasingCounter();
      counter.#count // 报错
      counter.#count = 42 // 报错
      
    • 私有属性的属性名必须包括#,如果不带#,会被当作另一个属性

    • 私有属性可以设置 getter 和 setter 方法

      class Counter {
        #xValue = 0;
      
        constructor() {
          console.log(this.#x);
        }
      
        get #x() { return this.#xValue; }
        set #x(value) {
          this.#xValue = value;
        }
      }
      
    • 私有属性和私有方法前面,也可以加上static关键字,表示这是一个静态的私有属性或私有方法

      class FakeMath {
        static PI = 22 / 7;
        static #totallyRandomNumber = 4;
      
        static #computeRandomNumber() {
          return FakeMath.#totallyRandomNumber;
        }
      
        static random() {
          console.log('I heard you like random numbers…')
          return FakeMath.#computeRandomNumber();
        }
      }
      
      FakeMath.PI // 3.142857142857143
      FakeMath.random()
      // I heard you like random numbers…
      // 4
      FakeMath.#totallyRandomNumber // 报错
      FakeMath.#computeRandomNumber() // 报错
      
    • 使用in运算符判断私有属性

      class C {
        #brand;
      
        static isC(obj) {
          if (#brand in obj) {
            // 私有属性 #brand 存在
            return true;
          } else {
            // 私有属性 #foo 不存在
            return false;
          }
        }
      }
      
      // 可以与this配合使用
      class A {
        #foo = 0;
        m() {
          console.log(#foo in this); // true
        }
      }
      
  11. 静态块

    • 允许在类的内部设置一个代码块,在类生成时运行且只运行一次,主要作用是对静态属性进行初始化

    • 每个类可以有多个静态块,每个静态块中只能访问之前声明的静态属性

    • 静态块的内部不能有return语句

    • 内部可以使用类名或this,指代当前类

    • 可以将私有属性与类的外部代码分享

      let getX;
      
      export class C {
        #x = 1;
        static {
          getX = obj => obj.#x;
        }
      }
      
      console.log(getX(new C())); // 1
      
  12. 类的注意点

    • 类和模块的内部,默认就是严格模式

    • 类不存在变量提升

    • 类中的Generator方法

      class Foo {
        constructor(...args) {
          this.args = args;
        }
        * [Symbol.iterator]() {
          for (let arg of this.args) {
            yield arg;
          }
        }
      }
      
      for (let x of new Foo('hello', 'world')) {
        console.log(x);
      }
      // hello
      // world
      
    • this的指向

      • 方法内部如果含有this,它默认指向类的实例

      • 如果将这个方法提取出来单独使用,this会指向该方法运行时所在的环境(由于 class 内部是严格模式,所以 this 实际指向的是undefined

        class Logger {
          printName(name = 'there') {
            this.print(`Hello ${name}`);
          }
        
          print(text) {
            console.log(text);
          }
        }
        
        const logger = new Logger();
        const { printName } = logger;
        printName(); // TypeError: Cannot read property 'print' of undefined
        
      • 解决方法一:在构造方法中绑定this

        class Logger {
          constructor() {
            this.printName = this.printName.bind(this);
          }
        
          // ...
        }
        
      • 解决方法二:使用箭头函数

        class Obj {
          constructor() {
            this.getThis = () => this;
          }
        }
        
        const myObj = new Obj();
        myObj.getThis() === myObj // true
        
      • 解决方法三:使用Proxy

        function selfish (target) {
          const cache = new WeakMap();
          const handler = {
            get (target, key) {
              const value = Reflect.get(target, key);
              if (typeof value !== 'function') {
                return value;
              }
              if (!cache.has(value)) {
                cache.set(value, value.bind(target));
              }
              return cache.get(value);
            }
          };
          const proxy = new Proxy(target, handler);
          return proxy;
        }
        
        const logger = selfish(new Logger());
        
  13. new.target 属性

    • 该属性一般用在构造函数之中,返回new命令作用于的那个构造函数

    • 如果构造函数不是通过new命令或Reflect.construct()调用的,new.target会返回undefined

    • Class 内部调用new.target,返回当前 Class

    • 子类继承父类时,new.target会返回子类

    • 可以写出不能独立使用、必须继承后才能使用的类

      class Shape {
        constructor() {
          if (new.target === Shape) {
            throw new Error('本类不能实例化');
          }
        }
      }
      
      class Rectangle extends Shape {
        constructor(length, width) {
          super();
          // ...
        }
      }
      
      var x = new Shape();  // 报错
      var y = new Rectangle(3, 4);  // 正确
      

Class的继承

  1. 简介

    • 通过extends关键字实现继承
    • 子类必须在constructor()方法中调用super(),这一步会生成一个继承父类的this对象,没有这一步就无法继承父类
  2. 私有属性和私有方法的继承

    • 类无法继承父类的私有属性和私有方法
    • 如果父类定义了私有属性的读写方法,子类就可以通过这些方法,读写私有属性
  3. 静态属性和静态方法的继承

    • 静态属性是通过浅拷贝实现继承的
    • 如果父类的静态属性的值是一个对象,子类修改这个对象的属性值,会影响到父类
  4. Object.getPrototypeOf()

    • 用来从子类上获取父类,可以用来判断一个类是否继承了另一个类。

      class Point { /*...*/ }
      
      class ColorPoint extends Point { /*...*/ }
      
      Object.getPrototypeOf(ColorPoint) === Point
      // true
      
  5. super 关键字

    • 情况一

      • super作为函数调用时,代表父类的构造函数,返回的是子类的this(即子类的实例对象)

      • 如果存在同名属性,此时拿到的是父类的属性

        class A {
          name = 'A';
          constructor() {
            console.log('My name is ' + this.name);
          }
        }
        
        class B extends A {
          name = 'B';
        }
        
        const b = new B(); // My name is A
        
    • 情况二

      • super作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类

      • 注意点:当super指向父类的原型对象,定义在父类实例上的方法或属性,是无法通过super调用的

        class A {
          constructor() {
            this.p = 2;
          }
        }
        
        class B extends A {
          get m() {
            return super.p;
          }
        }
        
        let b = new B();
        b.m // undefined
        
      • 如果通过super对某个属性赋值,这时super就是this,赋值的属性会变成子类实例的属性

        class A {
          constructor() {
            this.x = 1;
          }
        }
        
        class B extends A {
          constructor() {
            super();
            this.x = 2;
            super.x = 3;
            console.log(super.x); // undefined 读的是A.prototype.x
            console.log(this.x); // 3
          }
        }
        
        let b = new B();
        
  6. 类的 prototype 属性和__proto__属性

    • 子类的__proto__属性,表示构造函数的继承,总是指向父类

    • 子类prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype属性

      class A {
      }
      
      class B extends A {
      }
      
      B.__proto__ === A // true
      B.prototype.__proto__ === A.prototype // true
      
  7. 原生构造函数的继承

    • 例子

      class MyArray extends Array {
        constructor(...args) {
          super(...args);
        }
      }
      
      var arr = new MyArray();
      arr[0] = 12;
      arr.length // 1
      
      arr.length = 0;
      arr[0] // undefined
      
    • 注意点:ES6 改变了Object构造函数的行为,一旦发现Object方法不是通过new Object()这种形式调用,ES6 规定Object构造函数会忽略参数

      class NewObj extends Object{
        constructor(){
          super(...arguments);
        }
      }
      var o = new NewObj({attr: true});
      o.attr === true  // false
      
  8. Mixin 模式的实现

    • 多个对象合成一个新的对象,新对象具有各个组成成员的接口

      • 简单实现

        const a = {
          a: 'a'
        };
        const b = {
          b: 'b'
        };
        const c = {...a, ...b}; // {a: 'a', b: 'b'}
        
      • 完备实现

        function mix(...mixins) {
          class Mix {
            constructor() {
              for (let mixin of mixins) {
                copyProperties(this, new mixin()); // 拷贝实例属性
              }
            }
          }
        
          for (let mixin of mixins) {
            copyProperties(Mix, mixin); // 拷贝静态属性
            copyProperties(Mix.prototype, mixin.prototype); // 拷贝原型属性
          }
        
          return Mix;
        }
        
        function copyProperties(target, source) {
          for (let key of Reflect.ownKeys(source)) {
            if ( key !== 'constructor'
              && key !== 'prototype'
              && key !== 'name'
            ) {
              let desc = Object.getOwnPropertyDescriptor(source, key);
              Object.defineProperty(target, key, desc);
            }
          }
        }
        
        // 使用时只要继承这个类
        class DistributedEdit extends mix(Loggable, Serializable) {
          // ...
        }
        

Module语法

  1. 概述

    • ES6 模块是编译时加载
  2. 自动采用严格模式

  3. export 命令

    • export命令用于规定模块的对外接口

    • 写法一

      // profile.js
      export var firstName = 'Michael';
      export var lastName = 'Jackson';
      export var year = 1958;
      
    • 写法二

      // profile.js
      var firstName = 'Michael';
      var lastName = 'Jackson';
      var year = 1958;
      
      export { firstName, lastName, year };
      
    • 可以使用as关键字重命名

      function v1() { ... }
      function v2() { ... }
      
      export {
        v1 as streamV1,
        v2 as streamV2,
        v2 as streamLatestVersion
      };
      
  4. import 命令

    • 用于输入其他模块提供的功能

    • 可以使用as关键字将输入的变量重命名

      import { lastName as surname } from './profile.js';
      
    • import命令输入的变量都是只读的,因为它的本质是输入接口

    • import命令具有提升效果,会提升到整个模块的头部,首先执行

    • import是静态执行,所以不能使用表达式和变量

  5. export default 命令

    • 为模块指定默认输出

    • import命令可以为该匿名函数指定任意名字

    • 默认输出与正常输出对比

      // 第一组
      export default function crc32() { // 输出
        // ...
      }
      
      import crc32 from 'crc32'; // 输入
      
      // 第二组
      export function crc32() { // 输出
        // ...
      };
      
      import {crc32} from 'crc32'; // 输入
      
  6. export 与 import 的复合写法

    • 先输入后输出同一个模块,import语句可以与export语句写在一起

      export { foo, bar } from 'my_module';
      
      // 可以简单理解为
      import { foo, bar } from 'my_module';
      export { foo, bar };
      
    • 模块的接口改名和整体输出

      // 接口改名
      export { foo as myFoo } from 'my_module';
      
      // 整体输出
      export * from 'my_module';
      
      export * as ns from "mod";
      
      // 等同于
      import * as ns from "mod";
      export {ns};
      
    • 默认接口

      export { default } from 'foo';
      
  7. 跨模块常量

    • 建一个专门的constants目录,将各种常量写在不同的文件里面

      // constants/db.js
      export const db = {
        url: 'http://my.couchdbserver.local:5984',
        admin_username: 'admin',
        admin_password: 'admin password'
      };
      
      // constants/user.js
      export const users = ['root', 'admin', 'staff', 'ceo', 'chief', 'moderator'];
      
    • 将这些文件输出的常量,合并在index.js里面

      // constants/index.js
      export {db} from './db';
      export {users} from './users';
      
    • 使用的时候,直接加载index.js

      // script.js
      import {db, users} from './constants/index';
      
  8. import()

    • 引入import()函数,支持动态加载模块

    • import()返回一个 Promise 对象

      async function renderWidget() {
        const container = document.getElementById('widget');
        if (container !== null) {
          // 等同于
          // import("./widget").then(widget => {
          //   widget.render(container);
          // });
          const widget = await import('./widget.js');
          widget.render(container);
        }
      }
      
      renderWidget();
      
    • 适用场景

      • 按需加载

        button.addEventListener('click', event => {
          import('./dialogBox.js')
          .then(dialogBox => {
            dialogBox.open();
          })
          .catch(error => {
            /* Error handling */
          })
        });
        
      • 条件加载

        if (condition) {
          import('moduleA').then(...);
        } else {
          import('moduleB').then(...);
        }
        
      • 动态的模块路径

        import(f())
        .then(...);
        
    • 注意点(import()加载模块成功以后,这个模块会作为一个对象)

      • 可以使用对象解构赋值的语法,获取输出接口

        import('./myModule.js')
        .then(({export1, export2}) => {
          // ...·
        });
        
      • 如果模块有default输出接口,可以用参数直接获得

        import('./myModule.js')
        .then(myModule => {
          console.log(myModule.default);
        });
        
        // 可以适用具名输入的形式
        import('./myModule.js')
        .then(({default: theDefault}) => {
          console.log(theDefault);
        });
        
      • 可以同时加载多个模块

        Promise.all([
          import('./module1.js'),
          import('./module2.js'),
          import('./module3.js'),
        ])
        .then(([module1, module2, module3]) => {
           ···
        });
        
      • 可以用在async函数之中

        async function main() {
          const myModule = await import('./myModule.js');
          const {export1, export2} = await import('./myModule.js');
          const [module1, module2, module3] =
            await Promise.all([
              import('./module1.js'),
              import('./module2.js'),
              import('./module3.js'),
            ]);
        }
        main();
        
  9. import.meta

    • 返回当前模块的元信息

    • import.meta.url

      • 返回当前模块的 URL 路径

        new URL('data.txt', import.meta.url)// https://foo.com/main.js/data.txt
        
    • import.meta.scriptElement

      • 返回加载模块的那个<script>元素,相当于document.currentScript属性

        // HTML 代码为
        // <script type="module" src="my-module.js" data-foo="abc"></script>
        
        // my-module.js 内部执行下面的代码
        import.meta.scriptElement.dataset.foo
        // "abc"
        
    • import.meta.filename:当前模块文件的绝对路径。

    • import.meta.dirname:当前模块文件的目录的绝对路径。

Module的加载实现

  1. 浏览器加载

    • 传统方法:通过

      • 脚本异步加载

        <script src="path/to/myModule.js" defer></script>
        <script src="path/to/myModule.js" async></script>
        
      • defer是“渲染完再执行”,async是“下载完就执行”,如果有多个defer脚本,会按照它们在页面出现的顺序加载,而多个async脚本是不能保证加载顺序的

    • 记载规则

      • 浏览器加载 ES6 模块,也使用<script>标签,但是要加入type="module"属性。
      <script type="module" src="./foo.js"></script>
      
  2. ES6 模块与 CommonJS 模块的差异

    • CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用
      • CommonJS 模块一旦输出一个值,模块内部的变化就影响不到这个值
      • ES6 模块的JS 引擎对脚本静态分析的时候,遇到模块加载命令import,会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,原始值变了,import加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。
    • CommonJS 模块是运行时加载,ES6 模块是编译时输出接口
      • 因为 CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。
    • CommonJS 模块的require()是同步加载模块,ES6 模块的import命令是异步加载,有一个独立的模块依赖的解析阶段。
  3. Node.js 的模块加载方法

    • .mjs文件总是以 ES6 模块加载,.cjs文件总是以 CommonJS 模块加载,.js文件的加载取决于package.json里面type字段的设置(ES6:"type": "module"

    • package.json指定模块的入口文件的字段

      • main字段

      • exports字段(优先级高于main字段)

        • 子目录别名

          // ./node_modules/es-module-package/package.json
          {
            "exports": {
              // 指定src/features.js别名为features
              "./features/": "./src/features/"
            }
          }
          
          import feature from 'es-module-package/features/x.js';
          // 加载 ./node_modules/es-module-package/src/features/x.js
          
        • main的别名

          {
            "main": "./main-legacy.cjs",// 兼容旧版
            "exports": {
              ".": "./main-modern.cjs"
            }
          }
          
        • 条件加载

          // 为 ES6 模块和 CommonJS 指定不同的入口
          {
            "type": "module",
            "exports": {
              ".": {
                "require": "./main.cjs",
                "default": "./main.js"
              }
            }
          }
          
    • CommonJS模块加载ES6模块

      (async () => {
        await import('./my-app.mjs');
      })();
      
    • ES6模块加载CommonJS

      • 可以加载 CommonJS 模块,但是只能整体加载,不能只加载单一的输出项
    • 内部变量

      • ES6 模块之中,顶层的this指向undefined;CommonJS 模块的顶层this指向当前模块
      • 以下顶层变量在ES6模块之中不存在
        • arguments
        • require
        • module
        • exports
        • __filename
        • __dirname
  4. 循环加载(a脚本的执行依赖b脚本,而b脚本的执行又依赖a脚本)

    • CommonJS模块的循环加载

      • 一旦出现某个模块被"循环加载",就只输出已经执行的部分,还未执行的部分不会输出
    • ES6模块的循环加载

      • 利用函数的变量提升

        // a.mjs
        import {bar} from './b';
        console.log('a.mjs');
        console.log(bar());
        function foo() { return 'foo' }
        export {foo};
        
        // b.mjs
        import {foo} from './a';
        console.log('b.mjs');
        console.log(foo());
        function bar() { return 'bar' }
        export {bar};