ES6学习笔记

ES6语法

阮一峰ES6入门教程学习笔记
主要是梳理知识点,具体实例分析可以看教程。
顺序可能有打乱,我是综合(教程+我认为更重要的章节)的顺序来的。

let & const命令

1. let命令

1.1 基本用法

let声明的变量,只在let命令所在代码块内有效。
适用于for循环计数器。
for循环的循环计数器语句部分和循环体内部是两个作用域。

1.2 不存在变量提升

var存在变量提升,变量在声明前被访问,值为undefined,而let声明前访问会报错。

1.3 暂时性死区(TDZ, Temporal Dead Zone)

如果区块中存在letconst命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,包括使用typeof,都会报错。

1.4 不允许重复声明

在相同作用域内,不能用let重复声明同一个变量;所以不能在函数内部重新声明参数。

2. 块级作用域

2.1 为什么需要块级作用域

内层变量可能会覆盖外层变量
for循环计数器变量会泄露为全局变量

2.2 let为ES6新增块级作用域

  • 块级作用域可以嵌套
  • 内层作用域可以定义外层作用域的同名变量。
    这和上面说的“不能在函数重新声明参数”不同,后者是因为参数是在函数体作用域的,如果再声明,就是在同一作用域内重复声明,是不允许的,而这里是不同层次的作用域,不是重复声明,而是覆盖。
  • IIFE可以使用块级作用域代替

    2.3 关于在块级作用域内声明函数

    考虑到环境导致的行为差异太大,应该避免在块级作用域内声明函数。如果确实需要,也应该写成函数表达式,而不是函数声明语句。且块级作用域必须有大括号。

    2.4 do表达式

    在块级作用域前加上do关键字,可以
    1
    2
    3
    4
    let x = do {
    let t = f();
    t * t + 1;
    };

3. const命令

3.1 基本用法

let相同,除了const用于声明常量,声明时必须初始化,不能只声明不赋值。

3.2 本质

const实际上是保证变量指向的那个内存地址不能改动。如果声明的变量是个对象或数组,可以对该对象进行添加属性等操作,但不能将它指向另一个新的对象。
如果想让对象的属性也不能改变,可以将对象彻底冻结:

1
2
3
4
5
6
7
8
var constantize = (obj) => {
Object.freeze(obj);
Object.keys(obj).forEach( (key, i) => {
if( typeof obj[key] === 'object' ){
Object.freeze(obj[key]);
}
});
};

3.3 ES6声明变量有六种方法:

var, function, let, const, import, class
前两个命令声明的全局变量,依旧是顶层对象(window, global)的属性;let, const, class声明的变量不属于顶层对象的属性。

5. global对象

为了在浏览器,Node,Web Worker中都能获取顶层对象,引入垫片库system.global

1
2
3
4
5
6
// CommonJS的写法
var global = require('system.global')();
// ES6模块的写法
import getGlobal from 'system.global';
const global = getGlobal();

虽然我目前还不知道为什么需要获取全局对象。。

变量的解构赋值

destructuring 模式匹配-解构失败则赋值为undefined

1. 数组解构赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
let [foo, [[bar], baz]] = [1, [[2], 3]];
let [x, , y] = [1, 2, 3];
let [head, ...tail] = [1, 2, 3, 4];
head // 1
tail // [2, 3, 4]
let [x, y, ...z] = ['a'];
x // "a"
y // undefined
z // []
let [x, y, z] = new Set(['a', 'b', 'c']); //Set结构也可以解构成功
//只要是具备Iterator接口的数据结构,比如Generator函数,就可以用数组形式解构赋值
function* fibs() {
let a = 0;
let b = 1;
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
let [first, second, third, fourth, fifth, sixth] = fibs();
sixth // 5

解构赋值可以指定默认值,只有在相应位置的值===undefined时,才会使用默认值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let [x = 1] = [undefined];
x // 1
let [x = 1] = [];
x // 1
let [x = 1] = [null];
x // null
```
如果默认值是表达式,则该表达式为惰性求值,只有在用到的时候才会求值。
默认值可以引用解构赋值的其他变量,但该变量必须已声明。
### 2. 对象解构赋值
- 对象与数组的解构赋值的不同在于:数组有序,根据位置顺序赋值;对象无序,根据同名属性赋值。
- 关于模式和变量:
```javascript
let { foo: baz } = { foo: "aaa", bar: "bbb" };
baz // "aaa"
foo // error: foo is not defined
//foo是匹配的模式,baz才是变量。真正被赋值的是变量baz,而不是模式foo。
//且这种写法(let + :)相当于声明+赋值,所以赋值的变量不能是以前声明过的。

  • 如果赋值语句以大括号开头,必须用圆括号包裹起来,否则大括号放在行首会被解析器当做代码块而不是赋值语句。
  • 对象也可以嵌套解构赋值,但要注意模式不会被赋值,变量才会。
  • 数组是特殊的对象,也可以使用对象解构的方法解构赋值。

3. 字符串解构赋值

字符串会被转换成类数组对象,有length属性

1
2
3
4
5
const [a, b, c, d, e] = 'hello';
a // "h"
b // "e"
let {length : len} = 'hello';
len // 5

4. 数值和布尔值的解构赋值

解构赋值的右边如果不是对象或数组都会先转为对象。undefinednull无法转成对象,解构赋值写在右边会报错。

1
2
3
4
5
let {toString: s} = 123;
s === Number.prototype.toString // true
let {toString: s} = true;
s === Boolean.prototype.toString // true

5. 函数参数的解构赋值

注意以下两种情况的区别:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//为变量x和y指定默认值
function move({x = 0, y = 0} = {}) {
return [x, y];
}
move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, 0]
move({}); // [0, 0]
move(); // [0, 0]
//为函数参数指定默认值
function move({x, y} = { x: 0, y: 0 }) {
return [x, y];
}
move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, undefined]
move({}); // [undefined, undefined]
move(); // [0, 0] ?why

6. 圆括号问题

能不用就不用。能用的情况:赋值语句的非模式部分

7. 解构赋值的用途

  • 交换变量的值
  • 从函数中返回多个值
  • 函数传参对应
  • 提取json数据
  • 函数参数的默认值
  • 遍历Map结构:for...of遍历

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 获取键名
    for (let [key] of map) {
    // ...
    }
    // 获取键值
    for (let [,value] of map) {
    // ...
    }
    // 获取键值对
    for (let [key, value] of map) {
    console.log(key + " is " + value);
    }
  • 输入模块的指定方法
    const { SourceMapConsumer, SourceNode } = require("source-map");

字符串的扩展

1. 字符串的Unicode表示法

\u{xxxx}即使xxxx超过范围(\u0000~\uFFFF),也可以正确解读。以前要用两个双字节(32bit)的形式表示超出范围的字符,否则会解读成两个字符。

2. codePointAt(index) 对应于charAt(index)

  • 该方法定义在字符串的实例对象上
  • 将字符转成码点
  • js内部字符以UTF-16格式储存,每个字符固定为2字节(双字节,16bit)。对于需要两个双字节储存的字符来说,js会认为它们是两个字符:length为误判为2;用charAtcharCodeAt都无法读取整个字符,而是会分别返回前两个字节和后两个字节。
  • ES6解决办法:codePointAt()会正确返回32bit的字符的码点,默认以十进制值表示,如果要十六进制的值,可以用toString转换。
  • 遍历含有32bit字符的字符串时,为避免字符串索引下标问题(如果用下标访问字符,还是要把32bit字符当做占了2个下标来看待),应该用for...of循环,它会正确识别32bit的UTF-16字符:

    1
    2
    3
    4
    5
    6
    var s = '𠮷a';
    for (let ch of s) {
    console.log(ch.codePointAt(0).toString(16));
    }
    // 20bb7
    // 61
  • 利用codePointAt()测试一个字符是16bit还是32bit

    1
    2
    3
    function is32Bit(c) {
    return c.codePointAt(0) > 0xFFFF;
    }

2. String.fromCodePoint(codePoint) 对应于fromCharCode(codePoint)

  • 该方法定义在String上
  • 将码点转化为字符串,可传入多个码点
  • 可以识别大于0xFFFF的字符

3. at(index) 对应于charAt(index)

  • 获取给定位置的字符
  • 可以识别大于0xFFFF的字符

4. normalize()

Unicode正规化

5. includes(), startsWith(), endsWith()

  • 第一个参数为源字符串,第二个参数可选,表示开始搜索的位置
  • 均返回布尔值,表示是否找到/是否在源字符串的开头/末尾

6. repeat()

返回一个新字符串,表示将原字符串重复n次。参数如果是小数,会被取整;如果是字符串,则会先转换成数字。

7. padStart(), padEnd()

  • 字符串长度补全,头部/尾部补全。
  • 原字符串长度大于第一个参数:返回原字符串。
  • 原字符串与补全串长度之和大于第一个参数,截去超出位数的补全字符串。
  • 第二个参数省略则默认为空格
  • padStart()常见用途:为数值补全指定位数;提示字符串格式
    1
    2
    '123456'.padStart(10, '0') // "0000123456"
    '12'.padStart(10, 'YYYY-MM-DD') // "YYYY-MM-12"

8. 模板字符串

  • 用反引号标识,可以包含html标签
  • 可以用${}嵌入变量,表达式运算,访问对象属性,函数调用。
  • 如果大括号内不是字符串,比如对象,会调用其toString()
  • 如果大括号内变量未定义,会报错。
  • 模板字符串可嵌套
  • 引用模板字符串本身

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 写法一:用new Function('param', str)
    let str = 'return ' + '`Hello ${name}!`';
    let func = new Function('name', str);
    func('Jack') // "Hello Jack!"
    // 写法二:用eval.call(null, str)
    let str = '(name) => `Hello ${name}!`';
    let func = eval.call(null, str);
    func('Jack') // "Hello Jack!"
  • 模板编译实例:正则表达式匹配-将template string转换成含echo的js表达式字符串;将转换后的template string放到一个用模板字符串写的函数里,作为编译函数compile(template)的返回值script;调用该编译函数。

  • 标签模板:函数调用的特殊形式。标签-函数,模板-参数。
    若模板字符串里有多个参数,则相当于传参:第一个参数为数组,元素是没有变量替换的部分,后面的参数依次为变量替换后的值。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    //如何将各个参数按照原来的位置合并回去
    var total = 30;
    var msg = passthru `The total is ${total} (${total*1.05} with tax)`;
    function passthru(literals) {
    // console.log("~~~~literals: ~~~~", literals);
    // console.log("~~~~arguments:~~~~", arguments);
    var result = '';
    var i = 0;
    while (i < literals.length) { //3
    // console.log("----literals[", i, "]----", literals[i]);
    result += literals[i++];
    if (i < arguments.length) { //3
    // console.log("====arguments[", i, "]====", arguments[i]);
    result += arguments[i];
    }
    }
    return result;
    }
    console.log(msg); // "The total is 30 (31.5 with tax)"
    //采用rest参数的写法:
    function passthru(literals, ...values) {
    var output = "";
    for (var index = 0; index < values.length; index++) {
    output += literals[index] + values[index];
    }
    output += literals[index]
    return output;
    }

-(敲黑板!)标签模板的重要应用:过滤HTML字符串,将特殊字符如<``>转义,防止用户输入恶意内容,注入不安全代码。

  • React的jsx语法其实就用了模板字符串,定义jsx函数,将DOM字符串转成React对象。
  • 模板处理函数的第一个参数是模板字符串,其实是模板字符串数组,该数组的第一个元素是字符串本身,第二个元素是raw属性,保存转义后的原字符串。
  • String.raw()

数组的扩展

1. Array.from()

  • 将类数组对象(如arguments, DOM操作返回的NodeList)转成真正的数组;
    ES5: [].slice.call(arraylike);
    ES6: Array.from(arraylike);
    扩展运算符...也可以实现此功能:本质上还是调用Iterator接口。
    var arr = [...arguments];
    var arr = [...document.querySelectorAll('div')];
  • 将可遍历对象(部署了Iterator接口的数据结构,如ES6新增数据结构Set和Map)转成真正的数组。
  • 可接收第二个参数:接收一个函数,类似于数组的map方法。
  • 如果map函数用到了this,还可以传入第三个参数用来绑定this
  • 看一个灵活的用法:用第一个参数指定第二个参数的运行次数
    Array.from({ length: 2 }, () => 'jack'); // ['jack', 'jack']

2.Array.of()

  • 这个函数的出现是为了弥补构造函数 Array()的不足:它的行为很统一,总是返回一个由参数作为元素的数组。而构造函数只有在参数不少于2个时才返回数组,参数只有一个时只会指定新数组的长度。
  • 模拟:
    1
    2
    3
    function ArrayOf(){
    return [].slice.call(arguments);
    }

下面介绍的都是数组实例方法,即:用数组实例arr调用,或Array.prototype.调用

3. 数组实例方法copyWithin()

copyWithin(target, start = 0, end = this.length);

  • 在当前数组内部,将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组。数组会被修改。
  • 如果负数,倒数,-1对应最后一个元素。
  • 这里的例子出现了new Int32Array(),js对象的32 位整数值的类型化数组。

4. 数组实例方法find()findIndex()

  • 参数均为回调函数,find()找出第一个符合参数函数条件的数组成员,没有则返回undefinedfindIndex()找出相应的位置,没有则返回-1。
  • 回调函数的参数:当前遍历的value, 当前遍历位置index, 原数组arr。
  • 均可以接受第二个参数,用来绑定回调函数的this对象。
  • 均可以发现NaN,弥补了indexOf()的不足
    1
    2
    [NaN].indexOf(NaN); // -1
    [NaN].findIndex(y => Object.is(NaN, y)); // 0

5. 数组实例方法fill()

  • 适用于填充空数组。非空数组原有元素会被替代。
  • fill(value, start, end),可指定始末位置。

6. 数组实例方法entries(), keys(), values()

  • 用于遍历数组:键值对、键、值。
  • 配合for...ofarr.entries().next()遍历
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    for (let [index, elem] of ['a', 'b'].entries()) {
    console.log(index, elem);
    }
    // 0 "a"
    // 1 "b"
    let letter = ['a', 'b', 'c'];
    let entries = letter.entries();
    console.log(entries.next()); //{ value: [ 0, 'a' ], done: false }
    console.log(entries.next()); //{ value: [ 1, 'b' ], done: false }
    console.log(entries.next()); //{ value: [ 2, 'c' ], done: false }
    console.log(entries.next()); //{ value: undefined, done: true }

7. 数组实例方法includes()

  • 是对ES5数组方法indexOf()的弥补。
  • 检测数组中是否包含目标元素,可以检测出NaN
  • 第二个参数可选,搜索的起始位置。
  • Map和Set数据结构有一个has方法,需要注意与includes区分。

Map结构的has方法,是用来查找键名的,比如Map.prototype.has(key)WeakMap.prototype.has(key)Reflect.has(target, propertyKey)
Set结构的has方法,是用来查找值的,比如Set.prototype.has(value)WeakSet.prototype.has(value)

8. 数组的空位

  • ES5对空位的处理很不统一, ES6明确将空位转化成undefined
  • Array.from()
  • 扩展运算符...
  • copyWithin(), fill(), for...of, entries(), keys(), values(), find(), findIndex()
  • 尽量避免出现空位

函数的扩展

1. 函数的默认值

  • 允许为参数赋默认值,可以是具体值,也可以是表达式-惰性求值
  • 不能在函数体内对参数变量再次声明
  • 与解构赋值结合使用。看这两种写法的区别:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    // 写法一:函数参数的默认值是空对象,但是设置了对象解构赋值的默认值
    function m1({x = 0, y = 0} = {}) {
    return [x, y];
    }
    // 写法二:函数参数的默认值是一个有具体属性的对象,但是没有设置对象解构赋值的默认值
    function m2({x, y} = { x: 0, y: 0 }) {
    return [x, y];
    }
    // 函数没有参数的情况
    m1() // [0, 0]
    m2() // [0, 0]
    // x和y都有值的情况
    m1({x: 3, y: 8}) // [3, 8]
    m2({x: 3, y: 8}) // [3, 8]
    // x有值,y无值的情况
    m1({x: 3}) // [3, 0]
    m2({x: 3}) // [3, undefined]
    // x和y都无值的情况
    m1({}) // [0, 0];
    m2({}) // [undefined, undefined]
  • 定义了 默认值的参数尽量作为尾参数

  • 指定了默认值后函数的length属性将返回没有指定默认值的参数个数
    • length属性是预期传入的参数个数
    • rest参数也不会计入length属性
    • !!!如果设置了默认值的参数不是尾参数,那么length属性也不再计入后面的参数了
  • 作用域
    一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域。等到初始化结束,这个作用域就会消失。这种语法行为,在不设置参数默认值时,是不会出现的。
  • 应用:利用参数默认值,可以指定某一个参数不得省略,如果省略就抛出一个错误。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function throwIfMissing() {
    throw new Error('Missing parameter');
    }
    function foo(mustBeProvided = throwIfMissing()) {
    return mustBeProvided;
    }
    foo()
    // Error: Missing parameter

2. rest参数

  • 代替arguments对象,获取函数的多余参数。rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中。
  • rest参数之后不能再有其他参数。
  • 函数的length属性不包括rest参数

3. 扩展运算符

  • 主要用于函数传参,把数组或类数组对象展开成一系列用逗号隔开的参数序列。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    var foo = function(a, b, c) {
    console.log(a);
    console.log(b);
    console.log(c);
    }
    var arr = [1, 2, 3];
    //传统写法
    foo(arr[0], arr[1], arr[2]);
    //使用扩展运算符
    foo(...arr);
    //1
    //2
    //3
  • 替代数组的apply方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    var arr1 = [0, 1, 2];
    var arr2 = [3, 4, 5];
    // ES5的写法
    Array.prototype.push.apply(arr1, arr2);
    // ES6的写法
    arr1.push(...arr2);
    //ES5写法中,push方法的参数不能是数组,所以只好通过apply方法变通使用push方法。有了扩展运算符,就可以直接将数组传入push方法。
    // ES5的写法
    Math.max.apply(null, arr1);
    // ES6的写法
    Math.max(...arr1);
  • 应用:

    • 合并数组,替代concat方法。
    • 与解构赋值结合,生成新数组,扩展运算符要放在最后一位。
    • 函数返回值为多个,要作为另一个函数的参数时,可用扩展运算符处理返回值然后作为传参。
    • 将字符串转为数组:能正确识别32位Unicode字符。包括长度、reverse反转字符串时的32位Unicode字符问题。

      1
      2
      3
      4
      5
      6
      7
      let str = 'x\uD83D\uDE80y';
      str.split('').reverse().join('')
      // 'y\uDE80\uD83Dx' 这个结果不是我们想要的,中间应该是一个字符整体。
      [...str].reverse().join('')
      // 'y\uD83D\uDE80x'
    • 任何部署了Iterator接口的对象(Map, Set, Generator函数),都可以用扩展运算符转为真正的数组。如果要将没有部署Iterator接口的类数组对象转为真正的数组:用Array.from()方法。

4. 严格模式

只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式。
是为了规避ES5中函数参数部分先于函数体部分执行可能带来的问题:ES5中,若函数体中有规定严格模式而参数执行不符合严格模式,则会先执行参数部分然后才报错。

5. name属性

作为函数的属性,体现函数的名称。若将一个匿名函数赋给一个变量,则ES6中name为该变量名,ES5中name为空字符串。

6. 箭头函数

  • ES6中大括号开头会被解释为代码块,所以如果直接返回一个对象,要在对象外面加上小括号。
  • 函数体内的this对象,指向定义时所在对象,而不是运行时所在对象。
    箭头函数没有自己的this,它引用外层代码块的this。也不能用bind, call, apply改变this的指向。除了thisarguments, super, new.target这三个变量在箭头函数中也不存在,指向外层函数的相应变量。
  • 不能当作构造函数,不能使用new,因为箭头函数没有自己的this
  • 不能使用arguments对象,可以用rest参数代替。
  • 不能使用yield命令,所以不能当作Generator函数。
  • 嵌套的箭头函数
    部署管道机制:前一个函数的输出是后一个函数的输入
    改写lamda演算。

7. 绑定this

双冒号,ES7提案,Babel已支持。左边是对象,右边是函数,将对象绑定在函数上面。

1
2
3
4
5
6
7
foo::bar;
// 等同于
bar.bind(foo);
foo::bar(...arguments);
// 等同于
bar.apply(foo, arguments);

如果双冒号左边为空,右边是一个对象的方法,则等于将该方法绑定在该对象上面。
双冒号返回的是原对象,可以采用链式写法。

8. 尾调用优化 (*好难…)

  • 什么是尾调用:函数的最后一步是执行另一个函数,不能有附加的调用后的其他操作。
    -如何优化:只保留内层函数的调用帧。因为函数嵌套调用会形成调用帧,调用栈,会在内存中保存外层函数的信息及内层函数的位置信息等,如果是尾调用,并且不再需要外层函数的内部变量,则只需要保存内层函数就行了,可以删除外层函数的调用帧。(改写了函数的调用栈)
  • 尾递归:不会发生栈溢出,节省内存。
  • 递归函数的改写:确保最后一步只调用自身:把所有用到的内部变量改写成函数的参数。
    • 在尾递归函数之外,再提供一个正常行驶的函数。
    • curry化
    • 使用ES6的函数参数默认值
  • 递归本质是循环。函数式编程语言没有循环操作的命令,都是用递归实现循环操作,尾递归对这些语言很重要。
  • ES6 一旦使用递归,最好使用尾递归。
  • 尾调用模式只在严格模式下生效。
  • 自己实现尾递归优化:将递归换成循环。
  • 蹦床函数 + 将递归函数改写成每一步返回另一个函数

9. 尾逗号

ES2017允许函数的最后一个参数有逗号,方便添加、修改参数。

对象的扩展

1. 属性的简洁表示法

  • 在对象中直接写变量:属性名就是变量名,属性值就是变量的值;
  • 方法也可以简写
  • 这种写法用于函数的返回值为对象时很方便
  • CommonJS输出变量,属性的赋值器和取值器 都适用这种写法
  • 属性名总是会解析成字符串

2. 属性名表达式

  • 用字面量定义对象时,可以用[表达式]作为属性名、方法名
  • 属性名表达式和简洁表示法不能同时使用,一般用属性名表达式作为属性名之后,要写上属性值
  • 属性名表达式如果是个对象,会自动将对象转为字符串作为属性名,所以如果有多个对象作为属性名,最后只会有最后那个对象转字符串作为属性名,发生覆盖。

3. 方法的name属性

  • 返回方法名
  • 如果对象的方法使用了取值函数(getter)和存值函数(setter),则name属性不是在该方法上面,而是该方法的属性的描述对象的get和set属性上面,返回值是方法名前加上get和set。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    const obj = {
    get foo() {},
    set foo(x) {}
    };
    obj.foo.name
    // TypeError: Cannot read property 'name' of undefined
    const descriptor = Object.getOwnPropertyDescriptor(obj, 'foo');
    descriptor.get.name // "get foo"
    descriptor.set.name // "set foo"
  • bind方法创造的函数,name属性返回bound加上原函数的名字;Function构造函数创造的函数,name属性返回anonymous。

  • 如果对象的方法名是一个 Symbol 值,那么name属性返回的是这个 Symbol 值的描述。

4. Object.is()

  • 与ES5严格相等===行为基本一致,但弥补了+0应该不等于-0NaN应该等于自身。

    1
    2
    3
    4
    5
    +0 === -0 //true
    NaN === NaN // false
    Object.is(+0, -0) // false
    Object.is(NaN, NaN) // true
  • 如何在ES5中手动部署这个方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    Object.defineProperty(Object, 'is', {
    value: function(x, y) {
    if (x === y) {
    // 针对+0 不等于 -0的情况
    return x !== 0 || 1 / x === 1 / y;
    }
    // 针对NaN的情况
    return x !== x && y !== y;
    },
    configurable: true,
    enumerable: false,
    writable: true
    });

5. Object.assign(target, source1, source2)

  • 对象的合并:将源对象(可多个)的所有可枚举属性赋值到目标对象(1个)
  • 不在首参数的字符串会以字符数组的形式拷贝进目标对象。其他类型值如数值、布尔值,都没有任何效果,因为只有字符串的包装对象会产生可枚举属性。

    1
    2
    3
    4
    Object(true) // {[[PrimitiveValue]]: true}
    Object(10) // {[[PrimitiveValue]]: 10}
    Object('abc') // {0: "a", 1: "b", 2: "c", length: 3, [[PrimitiveValue]]: "abc"}
    //PrimitiveValue是包装对象的内部属性,这个值不会被Object.assign拷贝
  • 不拷贝继承属性和不可枚举的属性

  • 是浅拷贝
  • 同名属性会被替换
  • 会把数组视为对象

    1
    2
    Object.assign([1, 2, 3], [4, 5])
    // [4, 5, 3]
  • 用处:

    • 为对象添加属性、方法
    • 克隆对象

      1
      2
      3
      4
      5
      6
      7
      function clone(origin) { //只能克隆origin自身的值,不能克隆其继承值
      return Object.assign({}, origin);
      }
      function clone(origin) { //保持了继承链,连同其继承的值一并克隆
      let originProto = Object.getPrototypeOf(origin);
      return Object.assign(Object.create(originProto), origin);
      }
    • 合并多个对象

    • 为属性指定默认值
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      const DEFAULTS = { //其中的属性最好不要是对象,因为是浅拷贝,可能会被options对象全部覆盖,达不到预期的只改变某几个参数的效果
      logLevel: 0,
      outputFormat: 'html'
      };
      function processContent(options) {
      options = Object.assign({}, DEFAULTS, options); //options是用户提供的参数,会覆盖DEFAULTS中的同名参数。
      console.log(options);
      // ...
      }

6. 属性的可枚举性

  • Object.getOwnPropertyDescriptor

    1
    2
    3
    4
    5
    6
    7
    8
    let obj = { foo: 123 };
    Object.getOwnPropertyDescriptor(obj, 'foo')
    // {
    // value: 123,
    // writable: true,
    // enumerable: true,
    // configurable: true
    // }
  • ES5中只处理可枚举属性的方法:
    for...in 继承的和自身的。toStringlength属性都是不可枚举的所以可以不被遍历到。尽量不用这个来遍历而用下面的。
    Object.keys() 自身的
    JSON.stringify() 自身的

7. 属性的遍历

for…in 自身+继承,可枚举的,无Symbol属性
Object.keys(obj) 同上,除了继承
Object.getOwnPropertyNames(obj) 自身,可枚举+不可枚举,无Symbol属性
Object.getOwnPropertySymbols(obj) 自身的Symbol属性
Reflect.ownKeys(obj) 自身,可枚举+不可枚举,含Symbol

8. 设置/读取原型对象

  • Object.setPrototypeOf(obj, proto);
  • Object.getPrototypeOf()

9.Object.keys()遍历补充

都是遍历自身的,可枚举的,无Symbol属性
Object.keys()
Object.values()
Object.entries()

10. 对象的扩展运算符

  • 解构赋值:
    • 等号右边得是对象
    • 是浅拷贝
    • 只会拷贝对象自身的属性,不会拷贝继承的属性
    • 扩展运算符用于解构赋值要作为最后一个参数,否则报错。
  • 扩展运算符

11. Object.getOwnPropertyDescriptor

  • 配合Object.create() 实现浅拷贝
    1
    2
    const clone = Object.create(Object.getPrototypeOf(obj),
    Object.getOwnPropertyDescriptors(obj));

12. Null传导运算符

  • 简化赋值运算前的层层判断
    const firstName = message?.body?.user?.firstName || 'default';
  • 传导运算符的四种用法:
    • Obj?.prop 读取对象属性
    • Obj?.[expr] 同上
    • func?.(...args) 函数的调用
    • new C?.[...args] 构造函数的调用

Set 和 Map 数据结构

1. Set

  • 两个NaN是相等,两个对象不相等
  • new Set()接收参数可以为:数组或类数组对象
  • Set实例的方法:
    • Set.prototype.constructor,
    • Set.prototype.size,
    • add(value)返回Set本身,所以可以链式调用这个方法,
    • delete(value)
    • has(value)
    • clear()
  • Array.from(set)将set转为数组
  • 数组去重: [...new Set(array)]Array.from(new Set(array))
  • 遍历操作:keys(),values(),entries(),可以直接用for...of遍历

    1
    2
    for(let x of set){}
    set.forEach((value, key) => {}) //注意是先写value后写key
  • 扩展运算符,map, filter方法都可以用于Set

  • 集合的交集、并集、差集的实现

    1
    2
    3
    let union = new Set([...a, ...b]); //并集
    let intersect = new Set([...a].filter(x => b.has(x))); //交集
    let difference = new Set([...a].filter(x => !b.has(x))); //差集
  • 遍历的同时向改变Set元素的值:mapArray.from

    1
    2
    3
    4
    5
    6
    // 方法一
    let set = new Set([1, 2, 3]);
    set = new Set([...set].map(val => val * 2));
    // 方法二
    set = new Set(Array.from(set, val => val * 2));
    // set的值是2, 4, 6

2. WeakSet

(1)与Set的两点不同:

  • 成员只能是对象
  • 成员对象都是弱引用。如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象是否还存在于 WeakSet 之中。
    WeakSet 里面的引用,都不计入垃圾回收机制,所以就不存在忘记取消引用而导致内存无法释放、内存泄漏的问题。因此,WeakSet 适合临时存放一组对象(比如储存 DOM 节点,不用担心这些节点从文档移除时,会引发内存泄漏),以及存放跟对象绑定的信息。只要这些对象在外部消失,它在 WeakMap 里面的引用就会自动消失。
    WeakSet 内部有多少个成员,取决于垃圾回收机制有没有运行,运行前后很可能成员个数是不一样的,而垃圾回收机制何时运行是不可预测的,因此 ES6 规定 WeakSet 不可遍历。

(2)构造函数的对象为数组或类数组对象,且其中元素必须为对象。

3. Map

  • 是对Object Hash结构的弥补:Object结构:“键-值”对应,Map结构:“值-值”对应
  • 构造函数接收数组(元素为键值对形式的数组)作为参数
  • 同一个键多次赋值,会覆盖
  • 同一个键的定义:要为同一个对象(内存地址相同)
  • 实例方法、遍历方法类似于Set结构:
    • set(key, value)可链式调用
  • Map与数组、对象、JSON的互相转换

4. WeakMap

(1) 只接受对象作为键名
(2) 键名所指向的对象不计入垃圾回收机制
如果你要往对象上添加数据,又不想干扰垃圾回收机制,就可以使用 WeakMap。在网页的 DOM 元素上添加数据,就可以使用WeakMap结构。当该 DOM 元素被清除,其所对应的WeakMap记录就会自动被移除。
(3)应用:都是为了避免内存泄漏

  • DOM元素作为键,值可以为要绑定的监听事件handler,或该DOM元素对应的属性值。
  • 部署class的私有属性

Promise对象

1. 含义

  • 是个容器,保存异步操作
  • 是个对象,有统一的API处理异步操作,获取异步操作的消息
  • 对象有三种状态,除了异步操作的结果,没有别的方式能改变状态
  • 一旦状态改变,就不会再变,再给Promise对象添加回调函数也会立即得到结果
  • 缺点:无法取消Promise;Promise内部抛出的错误要反映到外部必须通过设置回调函数,麻烦;Pending状态不明确,无法表明是刚开始还是快完成状态。
  • Stream模式:适用于某些事件不断地反复发生的情况

2. 用法

  • 创造实例:构造函数参数为一个函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    var promise = new Promise(function(resolve, reject) { //resolve和reject两个参数是js引擎提供的两个函数,不用自己部署
    // ... some code
    if (/* 异步操作成功 */){
    resolve(value); //resolve函数作用:将Promise实例的状态变为Resolved,并将异步操作成功执行的结果作为参数value传出去
    } else {
    reject(error);
    }
    });
  • then指定两种状态的回调函数,两个参数分别为两个函数,第二个可选

    1
    2
    3
    4
    5
    promise.then(function(value) { //resolve函数执行后,要在当前所有同步任务执行完之后才会执行这个回调函数,value是resolve函数传来的
    // success
    }, function(error) {
    // failure
    });

实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let promise = new Promise(function(resolve, reject) {
console.log('Promise');
resolve();
});
promise.then(function() {
console.log('Resolved.');
});
console.log('Hi!');
// Promise 因为Promise新建后立即执行
// Hi! 要在当前所有同步任务执行之后再执行Promise的成功回调函数
// Resolved

  • 当一个Promise的resolve参数是另一个Promise时,后者的状态会决定前者的状态,即,前者的状态无效,前者的then函数相当于是后者的。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    var p1 = new Promise(function (resolve, reject) {
    setTimeout(() => reject(new Error('fail')), 3000)
    })
    var p2 = new Promise(function (resolve, reject) {
    setTimeout(() => resolve(p1), 1000) //p1的状态决定了p2的状态
    })
    p2 //p2的语句都是针对p1的
    .then(result => console.log(result))
    .catch(error => console.log(error))
    // Error: fail

3. Promise.prototype.then()

  • 定义在原型上,是Promise实例方法
  • 返回的是一个新的Promise实例 => 可以链式调用另一个then方法

4. Promise.prototype.catch()

  • 捕获Promise对象中的异步操作抛出的错误和then方法指定的回调函数抛出的错误
  • 错误具有“冒泡”性质,会一直向后传递直到被捕获为止
  • 一般来说,不要在then方法里面定义Reject状态的回调函数(即then的第二个参数),总是使用catch方法。
  • 返回的还是一个新的Promise对象,可以调用then方法。若是这个then方法里报错就与前面的catch无关了。
  • catch里面可以再抛出错误

5.Promise.all()

  • 接收数组参数,将多个Promise实例包装成一个新的Promise实例
  • 所有Promise状态都变成resolved,新的Promise状态才为resolved
  • 有一个Promise状态变为rejected,新的Promise状态就为rejected

6. Promise.race()

  • 接收数组参数,将多个Promise实例包装成一个新的Promise实例
  • 只要有一个Promise状态改变了,新的Promise状态就跟着改变

7. Promise.resolve(), Promise.reject()

  • 将现有对象转为Promise对象
  • 四种参数:
    • Promise实例:返回原对象
    • thenable对象:(具有then方法的对象),转为Promise对象后立即执行其then方法
    • 不是对象或不具有then方法的对象:返回状态为resolved/rejected的Promise对象
    • 无参数:同第三种
  • event loop: 立即resolve的Promise对象,是在本轮“事件循环”(event loop)的结束时,而不是在下一轮“事件循环”的开始时。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    setTimeout(function () { //在下一轮“事件循环”开始时执行
    console.log('three');
    }, 0);
    Promise.resolve().then(function () { //在本轮“事件循环”结束时执行
    console.log('two');
    });
    console.log('one'); //立即执行
    // one
    // two
    // three
  • 对比

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    let thenable = {
    then: function(resolve, reject) {
    resolve(42);
    }
    };
    let p1 = Promise.resolve(thenable);
    p1.then(function(value) {
    console.log(value); // 42,这里的值是thenable对象then函数中的value
    });
    //----------------------------------
    const thenable = {
    then(resolve, reject) {
    reject('出错了');
    }
    };
    Promise.reject(thenable)
    .catch(e => {
    console.log(e === thenable) // true, 这里e是thenable对象,而不是then函数中的字符串'出错了'
    });

8. 两个有用的附加方法

  • done(): 在回调链尾端,保证抛出任何可能出现的错误
  • finally(): 无论Promise对象最后状态如何,都会执行其参数回调函数。

9. 应用

  • 加载图片:一旦加载完成,状态就变化

    1
    2
    3
    4
    5
    6
    7
    8
    const preloadImage = function (path) {
    return new Promise(function (resolve, reject) {
    var image = new Image();
    image.onload = resolve;
    image.onerror = reject;
    image.src = path;
    });
    };
  • Generator函数与Promise的结合

10. Promise.try()

模拟try代码块,无论函数是否为异步操作,都用promise处理,用then方法指定下一步操作。

Class

1. 基本语法

1.1 概述

  • ES6的语法糖,为了让构造对象的方式更像面向对象编程的语法
  • class中的constructor函数对应ES5中的构造函数
  • class的数据类型其实就是function,类本身就指向构造函数。如下Point是class定义的“类”:
    Point === Point.prototype.constructor; //true
  • class中的函数,不写function关键字,用ES6简洁写法。各函数之间不用逗号
  • class中定义的方法其实都定义在类的prototype上。调用实例的方法也就是调用类的原型上的方法。如下,b是通过new创建的类B”的实例:
    b.constructor === B.prototype.constructor; //true
  • 可以用Object.assign(Point.prototype, {//funcions...})一次性向类添加多个方法。
  • 类内部定义的所有方法都是不可枚举的
  • 类的属性名、方法名,可以用方括号表达式表示

1.2 constructor方法

  • 类的默认方法。如果没有显示定义会自动添加一个空的。
  • 可以指定返回全新的对象Object.create(null),这样instanceof运算符的结果就不是那个类了

    1
    2
    3
    4
    5
    6
    7
    8
    class Foo {
    constructor() {
    return Object.create(null);
    }
    }
    new Foo() instanceof Foo
    // false
  • 只有用new才能调用构造函数,不同于ES5构造函数可以直接执行不用new

1.3 不存在变量提升

  • ES6不会把class的声明提升到代码头部
  • 必须先定义class再使用(包括new和被继承)
  • 与继承有关,必须保证子类在父类之后定义

1.4 class表达式

  • 如果用类表达式,而不是类声明,要注意类的最终名称是类表达式的变量名而不是class关键字后面的类名,后者只在类内部能使用,且可省略。
  • name属性总是返回紧跟class关键字后面的名字
  • 立即执行类 -> 立即执行函数

1.5 私有方法的实现

  • 体现在命名上:方法名前加下划线_,但是这样仍能被外部访问到
  • 将方法移出class,在class内使用func.call(this,args)来调用func
  • 利用Symbol值唯一性,且只能在class内部取到。将方法名用方括号内的Symbol值表示。

1.6 this的指向

  • class中的方法中的this默认指向类的实例,在class外面使用该方法可能会报错,因为this在外面指向该方法运行时的环境,这个同ES5,解决办法:
    (1)在构造函数中绑定this
    (2)在构造函数中使用箭头函数
    (3)Proxy(暂时没看)

1.7 严格模式

  • 类和模块内部,默认是严格模式
  • ES6其实把整个语言升级到了严格模式

2 class的继承

2.1 基本用法

  • extends关键字
  • 子类必须在constructor方法中调用super方法,否则新建实例时会报错。
    这是因为子类没有自己的this对象,而是继承父类的this对象,然后对其进行加工。如果不调用super方法,子类就得不到this对象。
    ES5的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。ES6的继承机制完全不同,实质是先创造父类的实例对象this(所以必须先调用super方法),然后再用子类的构造函数修改this。
  • 子类的constructor中,必须先调用super才能使用this
  • 子类的实例,用instanceof运算符可验证其既是子类的实例,又是父类的实例

2.2 类的prototype属性和proto属性

  • 两条继承链:
    1
    2
    3
    4
    5
    6
    7
    8
    class A {
    }
    class B extends A {
    }
    B.__proto__ === A // true 构造函数的继承
    B.prototype.__proto__ === A.prototype // true 方法的继承

理解:
作为一个对象:子类B的原型__proto__是父类A;
作为一个构造函数:子类的原型prototype是父类的实例A.prototype

  • 类继承的实质:利用Object.setPrototypeOf(child, parent),而该函数的实现是用__proto__

2.3 extends 后面的三种特殊情况

  • class A extends Object

    1
    2
    A.__proto__ === Object; //true
    A.prototype.__proto__ === Object.prototype; //true
  • 不写继承,则A是一个普通函数

    1
    2
    A.__proto__ === Function.prototype; //true
    A.prototype.__proto__ === Object.prototype; //true
  • class A extends null

    1
    2
    A.__proto__ === Function.prototype; //true
    A.prototype.__proto__ === undefined; //true

2.4 Object.getPrototypeOf()

从子类获取父类。可用来判断一个子类是否继承了另一个父类。
Object.getPrototypeOf(ColorPoint) === Point;

2.5 super关键字

  • 作为函数调用:代表父类的构造函数,子类的构造函数中必须执行一次,并且只能用于子类的构造函数中。虽然执行的是父类的构造函数,但是返回的是子类的实例(可以用new.target.name验证,它永远指向当前正在执行的函数)。相当于Parent.prototype.constructor.call(this);
  • 作为对象使用:指向父类的原型对象Parent.prototype,注意,是原型对象而不是实例。所以定义在父类实例上的方法和属性就不能通过super对象取到了。
  • 作为对象使用,通过super调用父类的方法时,super会绑定子类的this。所以如果通过super对某个属性赋值,这时super就是this,赋值的属性会变成子类实例的属性。但是通过super去读取这个赋值的属性时,super相当于父类的原型对象。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    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();
  • 作为对象使用,如果用在静态方法之中,这时super将指向父类,而不是父类的原型对象。

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

2.6 实例的proto属性

子类实例的原型是父类实例,子类实例的原型的原型是父类实例的原型。
p2.__proto__.__proto__ === p1.__proto__;

3. 原生构造函数的继承

  • ES5不能正确继承原生构造函数,ES6可以。=> ES6可以创建原生数据结构的子类,自定制数据结构。
  • ES5是先新建子类的实例对象this,再将父类的属性添加到子类上,由于父类的内部属性无法获取,导致无法继承原生的构造函数。
  • ES6先新建父类的实例对象this,然后再用子类的构造函数修饰this,使得父类的所有行为都可以继承。

4. class的getter和setter函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class MyClass {
constructor() {
// ...
}
get prop() {
return 'getter';
}
set prop(value) {
console.log('setter: '+value);
}
}
let inst = new MyClass();
inst.prop = 123;
// setter: 123
inst.prop
// 'getter'

5. class的static方法

  • 不会被实例继承,而是直接通过类来调用。可以直接通过类名.方法名访问。
  • 可以被子类继承
  • 可以在子类中通过super.方法名调用

6. class的静态属性和实例属性

定义在实例上的属性:用this.prop
定义在类上的属性为静态属性:class.prop,ES6规定要写在class外部,因为class内部只有静态方法,没有静态属性。但是ES7提案可以写在class内部,加上static关键字即可,并且Babel也支持。

7. 类的私有属性

提案:#属性前缀

8. new.target属性

  • new命令的属性,用于构造函数中。
  • 返回new命令作用于的构造函数。如果构造函数不是由new调用(比如通过call,apply调用)的,则该属性为undefined。如果是通过new调用的,返回相应的class名字或构造函数名。
  • 子类构造函数中该属性指向子类。

9. Mixin模式

通过继承mix类,将多个类合成一个类:

1
2
3
class DistributedEdit extends mix(Class1, Class2) {
// ...
}

Module语法

1. 为什么要引入Module

  • CommonJS和AMD,都是运行时确定模块之间的依赖关系。“运行时加载”:整体加载某个模块,加载其所有方法,生成一个对象,然后从该对象中读取所需的几个方法。运行时才获取该对象,无法在编译时做“静态优化”。
    ES6提出的解决方案,更加静态化。
  • ES6编译时加载(静态加载),之加载需要的某几个方法,可以在编译时就加载完成,

    1
    2
    3
    4
    // CommonJS模块
    let { stat, exists, readFile } = require('fs');
    // ES6模块
    import { stat, exists, readFile } from 'fs';
  • ES6好处:
    1)效率更高;
    2)可以静态分析,拓宽js语法(宏Macro, 类型检验)
    3)浏览器、服务器端都支持,不再需要UMD
    4)将来浏览器新的API可通过模块格式提供,不再需要做成全局变量或navigator对象的属性
    5)不再需要对象作为命名空间(比如Math对象),而是通过模块格式提供

2. 严格模式

ES6模块自动采用严格模式

3. export & import

var, function, class都可以export

1
2
3
4
5
6
7
8
9
10
// 写法一
export var m = 1;
// 写法二
var m = 1;
export {m};
// 写法三
var n = 1;
export {n as m};

CommonJS 模块输出的是值的缓存,不存在动态更新。而ES6输出的值会动态更新。

exportimport不能出现在块级作用域内,可以出现在模块顶层的任何位置。

import的写法和export基本相同。import { ... } from ''后面的路径可以是相对或绝对的,可以省略.js,如果配置文件中配置过的话。import * as circle from './circle'模块的整体加载。加载的对象不可以修改其属性和方法,因为不允许运行时改变。import moduleA;只执行模块,不引入任何变量。

import有变量提升效果,在编译阶段(代码运行前)执行,静态执行,所以不能用表达式和变量。

import语句是 Singleton 模式。如果多次加载同一个模块,对应的也是同一个对象。

4. export default命令

  • 使用模块要先知道模块中输出了哪些东西,才好指定import具体引入哪些东西。
  • 默认输出的函数,在import时不用大括号,可以为这个默认引入的函数指定任意名字调用。
  • 该命令一个模块只能使用一次,因为一个模块只能指定一个默认输出,所以import才不用大括号。
  • 后面不能跟变量赋值语句,可以跟变量。因为默认输出变量default,跟着变量则将该变量赋值给default变量。
  • exportimport结合
  • 具名接口和默认接口互相改变。

5. 跨模块常量

const声明的常量只在当前模块有效,不能跨模块获取到。解决办法:
将不同模块放在同一目录下,再将这些模块引入到一个文件里,最后只需要引入这个文件即可。

6. 动态加载提案:import()

  • 类似于Node的require,运行时加载。import()是异步加载,require()是同步加载。
  • 返回Promise对象。
  • 按需加载,条件加载,加载动态生成路径。

Module的加载实现

1. 浏览器加载

  • 传统方法:

同步加载。浏览器的渲染引擎遇到<script>标签就停下来,先执行脚本(如果是外部脚本,还要先下载),然后再继续渲染。

异步加载:deferasync:遇到<script>先下载,暂不执行。前者要等到整个页面正常渲染结束,才会执行,(先渲染,后执行,多个有序);后者一旦下载完,渲染引擎就会中断渲染,执行这个脚本以后,再继续渲染(仅仅是下载不阻塞,但下载完就执行,多个不一定有序)。

  • 浏览器加载ES6模块:<script type="module" src=""><script type="module"></script>,异步加载。默认渲染完再执行(同defer),也可设置为async

2. ES6模块与CommonJS模块的差别

  • CommonJS 模块输出的是一个值的拷贝(将加载执行后的值缓存了),ES6 模块输出的是值的引用(在静态分析时生成该值的只读(不可以重新赋值)引用,等脚本真正执行时才去模块里根据引用去取值)。所以ES6输出的会随模块内部相应值动态更新而CommonJS不会。
  • CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。

3. Node加载

  • Node加载ES6模块和CommonJS模块是分开的,只要有importexport就认为是ES6模块,否则就为CommonJS模块。
  • ES6 模块之中,顶层的this指向undefined;CommonJS 模块的顶层this指向当前模块
  • 加载规则:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    import './foo';
    // 依次寻找
    // ./foo.js
    // ./foo/package.json
    // ./foo/index.js
    import 'baz';
    // 依次寻找
    // ./node_modules/baz.js
    // ./node_modules/baz/package.json
    // ./node_modules/baz/index.js
    // 寻找上一级目录
    // ../node_modules/baz.js
    // ../node_modules/baz/package.json
    // ../node_modules/baz/index.js
    // 再上一级目录
  • 交叉命令加载:

    • import加载CommonJS模块:将CommonJS模块的module.exports视作export default。采用import...as bar from ''的写法时,要通过bar.default拿到模块的默认输出。加载的模块仍具有CommonJS特性(运行时加载,输出值的拷贝缓存机制),不允许大括号语法。
    • require加载ES6模块:模块的所有输出值都为require的赋值对象的属性。采用这种方式引入ES6模块也是缓存机制,不能动态更新值。

4. 循环加载

CommonJS:同步加载,按照代码顺序执行每个模块,加载时停在这里等它执行完毕才会执行后面的代码。每个模块只会加载一次,如果遇到循环加载,要看该模块有没有被执行过或正在执行中。
ES6:引入的是引用,后期可以根据引用去相应模块中取值。

5. 另外两种ES6转码器

ES6 module transpiler:npm包,es6-module-transpiler
SystemJS: 其实调用了Google的Traceur转码器。<scripts src="system.js">标签引入该垫片库(polyfill),用System.import(''),异步加载,返回一个Promise

分享
0%