0%

ES6学习笔记

说明

系统记录 ES6 知识,方便学习查看~

块级作用域绑定

let / const

  • const 一旦声明变量,就必须立即初始化,不能留到以后赋值
  • const 命令只是保证变量名指向的地址不变,并不保证该地址的数据不变。如果真的想将对象冻结,应该使用 Object.freeze 方法
  • 不能重复声明,如果作用域已经存在某个声明过的标识符,则不能再使用 let / const 关键字声明它
  • for-infor-of 循环中使用 const 时的行为与使用 let 一致
  • var 命令和 function 命令声明的全局变量,依旧是全局对象(window,不适用于 node 环境)的属性;另一方面规定, let 命令、 const 命令、 class 命令声明的全局变量,不属于全局对象的属性。变量没有声明就赋值,则自动添加到全局对象上(包括 node 环境)
1
2
3
4
5
6
var a = 1;
// 或者采用通用方法,写成 this.a
window.a; // 1

let b = 1;
window.b; // undefined
  • 花括号内形成块级作用域,花括号外无法访问
1
2
3
4
{
let bigData = 'big data ...';
}
console.log(bigData); // ReferenceError: bigData is not defined
  • 临时死区
1
2
console.log(typeof value); // ReferenceError: value is not defined
let value = 'blue';
1
2
3
4
console.log(typeof value); // undefined
{
let value = 'blue';
}

在 let / const 声明的作用域外对该变量使用 typeof 不会报错

字符串和正则表达式

Unicode

UTF-16

在 UTF-16 中,前 $2^{16}$ 个码位均以 16 位的编码单元表示,这个范围被称作 基本多文种平面 (BMP) 。超出这个范围的码位使用 _代理对_,用两个 16 位编码单元表示一个码位

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var a = '𠮷';
console.log(a.length); // 2
console.log(/^.$/.test(a)); // false

console.log(a.charAt(0)); // �
console.log(a.charAt(1)); // �

console.log(a.charCodeAt(0)); // 55362
console.log(a.charCodeAt(1)); // 57271

console.log(a.codePointAt(0)); // 134071,返回完整的码位
console.log(a.codePointAt(1)); // 57271

console.log(String.fromCodePoint(a.codePointAt(0))); // 𠮷

normalize() 方法

normalize | MDN

  • 在对比字符串之前,一定先把他们标准化为同一种形式
1
2
3
4
5
6
7
8
9
10
11
12
values.sort((first, second) => {
let firstNormalized = first.normalize();
let secondNormalized = second.normalize();

if (firstNormalized < secondNormalized) {
return -1;
} else if (firstNormalized === secondNormalized) {
return 0;
} else {
return 1;
}
});

正则表达式 u 修饰符

  • 添加 u 修饰符后,正则表达式就不会视代理对为两个字符
1
2
3
4
let a = '𠮷';

console.log(/^.$/.test(a)); // false
console.log(/^.$/u.test(a)); // true
  • 可以通过正则来检测字符串的码位数量
1
2
3
4
5
6
7
function codePointLen(text) {
let result = text.match(/[\s\S]/gu);
return result ? result.length : 0;
}

console.log(codePointLen('𠮷')); // 1
console.log('𠮷'.length); // 2
  • 检测是否支持 u 修饰符,对于 y 修饰符也是同理
1
2
3
4
5
6
7
8
function hasRegExpU() {
try {
var pattern = new RegExp('.', 'u');
return true;
} catch (e) {
return false;
}
}

其他字符串变更

  • repeat() 方法
1
console.log('x'.repeat(3)); // xxx

其他正则表达式变更

正则表达式 y 修饰符
  • y 修饰符:影响正则表达式搜索过程中的 sticky 属性,当在字符串中开始字符匹配时,它会通知搜索从正则表达式的 lastIndex 属性开始进行,如果在指定位置没能成功匹配,则停止继续匹配。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let text = 'hello1 hello2 hello3',
pattern = /hello\d\s?/,
result = pattern.exec(text),
globalPattern = /hello\d\s?/g,
globalResult = globalPattern.exec(text),
stickyPattern = /hello\d\s?/y,
stickyResult = stickyPattern.exec(text);

console.log(result[0]); // hello1
console.log(globalResult[0]); // hello1
console.log(stickyResult[0]); // hello1

pattern.lastIndex = 1;
globalPattern.lastIndex = 1;
stickyPattern.lastIndex = 1;

result = pattern.exec(text);
globalResult = globalPattern.exec(text);
stickyResult = stickyPattern.exec(text);

console.log(result[0]); // hello1
console.log(globalResult[0]); // hello2
console.log(stickyResult); // null

只有调用 exec()test() 这些正则表达式对象的方法才会涉及 lastIndex 属性。

正则表达式的复制
  • 在 ES5 中抛出错误,在 ES6 中正常运行。可以通过第二个参数修改其修饰符
1
2
3
4
5
var re1 = /ab/i;
var re2 = new RegExp(re1, 'g');

console.log(re1.toString()); // /ab/i
console.log(re2.toString()); // /ab/g

flags 属性

  • ES5:通过 source 属性获取正则表达式的文本
  • ES6 新增,新增 flags 属性,它与 source 属性都是只读的
1
2
3
let re = /ab/g;
console.log(re.source); // ab
console.log(re.flags); // g

模板字符串

  • 字符串可以被 for…of 循环遍历,最大的优点是可以识别大于 0xFFFF 的码点,传统的 for 循环无法识别这样的码点
  • 字符串中嵌入变量,需要将变量名写在 ${} 之中
  • 多行模板字符串会在每一行的最后添加一个“\n”字面量,相当于使用 LF 换行符,所以在读取多行字符串的长度时,除最后一行以外,每一行的长度都会加 1,即增加了”\n”
1
2
3
4
5
const str = `A
B
C
D`; // A\nB\nC\nD
console.log(str.length); // 7

如要打印出反勾号可以在反勾号前面加上一个反斜杠

标签模板

  • 定义标签
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function passthru(literals, ...substitutions) {
let result = '';

console.log(literals); // [ '', ' items cost ', '' ]
console.log(substitutions); // [ 10, 2.5 ]

for (let i = 0; i < substitutions.length; i++) {
result += literals[i];
result += substitutions[i];
}

result += literals[literals.length - 1];
return result;
}

let count = 10;
let price = 0.25;
let message = passthru`${count} items cost ${count * price}`;

console.log(message); // 10 items cost 2.5

标签函数的第一个参数是一个数组,它还有一个额外的属性 raw。literals.raw[i]

String.raw()

1
2
3
4
5
6
7
8
9
10
11
12
let msg1 = 'a\nb';
let msg2 = String.raw`a\nb`;

console.log(msg1);
// a
// b

console.log(msg2);
// a\nb

console.log(msg1.length); // 3
console.log(msg2.length); // 4

函数

函数默认参数值

1
2
3
4
5
function fn(arg = 'foo') {
console.log(arg);
}
fn(); // foo
fn('bar'); // bar

与解构赋值默认值结合

  • 默认参数值
1
2
3
4
5
6
7
8
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
  • 指定了默认值以后,函数的length属性,将返回没有指定默认值的参数个数
1
2
3
4
5
6
7
8
9
(function (a) {}
.length(
// 1
function (a = 5) {}
)
.length(
// 0
function (a, b, c = 5) {}
).length); // 2
  • 在 ES6 中,arguments 对象的行为都将与 ES5 严格模式下保持一致。默认参数值的存在使得 arguments 对象保持与命名参数分离
1
2
3
4
5
6
7
8
9
10
11
function mixArg(first, second = 'b') {
return {
len: arguments.length,
equalFirstArgs: first === arguments[0],
equalSecondArgs: second === arguments[1],
};
}

console.log(mixArg('a')); // { len: 1, equalFirstArgs: true, equalSecondArgs: false }
console.log(mixArg('a', 'b')); // { len: 2, equalFirstArgs: true, equalSecondArgs: true }
console.log(mixArg('a', 'b', 'c')); // { len: 3, equalFirstArgs: true, equalSecondArgs: true }

默认参数表达式

1
2
3
4
5
6
7
8
9
10
11
12
let value = 0;

function getVal() {
return value++;
}

function add(first, second = getVal()) {
return first + second;
}

console.log(add(1)); // 1
console.log(add(1, 1)); // 2

初次解析函数声明时不会调用 getValue() 方法,只有当调用 add() 函数且不传入第二个参数时(或者为 undefined)才会调用

不定参数

  • 每个函数最多只能声明一个不定参数,而且一定要放在所有参数的末尾,否则会抛出错误
  • 不定参数不能用于对象字面量 setter 之中,否则报错
1
2
function fn1(...rest) { /* ... */ }  // Correct
function fn2(...rest, foo) { /* ... */ } // SyntaxError: Rest parameter must be last formal parameter

函数的多重用途

函数有两个不同的内部方法:[[Call]] 和 [[Contruct]]。当通过 new 关键字调用函数时,执行的是 [[Contruct]] 函数,当通过 callapply 调用时,则执行 [[Call]] 函数。具有 [[Contruct]] 方法的函数被称为构造函数。不是所有函数都有 [[Construct]] 方法,因此不是所有函数都可以通过 new 来调用,如箭头函数

  • ES5 判定函数被调用的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Person(name) {
if (this instanceof Person) {
this.name = name;
} else {
throw new Error('必须通过 new 关键字来调用 Person');
}
}

let pj = new Person('pj');
console.log(pj.name); // pj

let sr = Person.call(pj, 'sr'); // 不报错
console.log(sr); // undefined

let jy = Person('jy'); // Error: 必须通过 new 关键字来调用 Person

无法区分是通过 Person.call()(或者是 Person.apply())还是 new 关键字调用得到的 Person 的实例

元属性 new.target

当调用函数的 [[Construct]] 方法时,new.target 被赋值为 new 操作符的目标,通常是新创建对象实例,也就是函数体内 this 的构造函数;如果调用 [[Call]] 方法,则 new.target 的值为 undefined

1
2
3
4
5
6
7
8
9
10
11
12
function Person(name) {
if (typeof new.target !== 'undefined') {
this.name = name;
} else {
throw new Error('必须通过 new 关键字来调用 Person');
}
}

let pj = new Person('pj');
console.log(pj.name); // pj

let sr = Person.call(pj, 'sr'); // Error: 必须通过 new 关键字来调用 Person

箭头函数

this

  • 没有 this、super、arguments 和 new.target 绑定,这些值由外围最近一层非箭头函数决定

  • 在箭头函数出现之前,每个新定义的函数都有它自己的 this值(在构造函数的情况下是一个新对象,在严格模式的函数调用中为 undefined)

  • 箭头函数不会创建自己的this;它使用封闭执行上下文this值。因此,在下面的代码中,传递给setInterval的函数内的this与封闭函数中的this值相同:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    function Person() {
    this.age = 0;

    setInterval(() => {
    this.age++; // |this| 正确地指向person 对象
    }, 1000);
    }

    var p = new Person();
  • 鉴于 this 是词法层面上的,严格模式中与 this 相关的规则都将被忽略

  • 不能通过 new 关键字调用,没有原型,不支持 arguments 对象,但可以访问外围函数的 arguments 对象,不支持重复的命名参数

  • this 已经在词法层面完成了绑定,通过 call() apply() 方法调用一个函数时,只是传入了参数而已,对 this 并没有什么影响。箭头函数的 this 一旦绑定后就无法被修改

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    const a = {
    init() {
    this.bar = () => this.dam;
    },
    dam: 'hei',
    foo() {
    return this.dam;
    },
    };

    const b = {
    dam: 'ha',
    };

    a.init(); // 箭头函数中的this执行时绑定了,为a

    console.log(a.bar()); // hei
    console.log(a.bar.call(b)); // hei。无法修改其this

注意事项

  1. 箭头函数不能用作构造器,和 new一起用会抛出错误

  2. 箭头函数没有prototype属性,其值为undefined

  3. 不能在箭头函数中使用yield关键字(除非是嵌套在允许使用的函数内)

  4. 记得用圆括号把返回的对象字面量包起来

    1
    var func = () => ({ foo: 1 });
  5. 箭头函数在参数和箭头之间不能换行,否则报错

  6. 箭头函数具有与常规函数不同的特殊运算符优先级解析规则。

    1
    2
    3
    4
    5
    6
    7
    8
    let callback;

    callback = callback || function() {}; // ok

    callback = callback || () => {};
    // SyntaxError: invalid arrow-function arguments

    callback = callback || (() => {}); // ok
  7. 箭头函数中也没有 arguments、callee,可以使用后续参数代替arguments

    1
    2
    3
    4
    const fn = (...args) => {
    console.log(args[0]);
    };
    fn(1, 2); // 1

尾调用优化

尾调用指的是函数作为另一个函数的最后一条语句被调用

1
2
3
function doSomething() {
return doSomethingElse(); // 尾调用
}

ES6 尾调用优化

ES6 严格模式下缩减了尾调用栈的大小(非严格模式下不受影响),如果满足以下条件,尾调用不再创建新的栈帧,而是清除并重用当前栈帧

  • 尾调用不访问当前栈帧的变量(也就是说函数不是一个闭包
  • 在函数内部,尾调用是最后一条语句
  • 尾调用的结果作为函数值返回
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 以下情况均无法优化
'use strict';
function doSomething() {
// 无法优化,无返回
doSomethingElse();
}

function doSomething() {
return 1 + doSomethingElse();
}

function doSomething() {
// 无法优化,尾调用不在尾部
let result = doSomethingElse();
return result;
}

function doSomething() {
var num = 1,
func = () => num;
// 无法优化,该函数是一个闭包
return func();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 优化前
function factorial(n) {
if (n <= 1) {
return 1;
} else {
return n * factorial(n - 1);
}
}

// 优化后
function factorial(n, p = 1) {
if (n <= 1) {
return 1 * p;
} else {
let result = n * p;
return factorial(n - 1, result);
}
}

扩展对象的功能性

对象字面量扩展语法

可计算属性名

ES6 引入的新语法允许我们直接使用一个表达式来表达一个属性名用法:{ [statement]: value}

1
2
3
4
5
6
const prefix = 'ES6';
const obj = {
[prefix + 'enhancedObject']: 'foo',
};

console.log(obj); // { ES6enhancedObject: 'foo' }

属性初始值简写

变量名和属性名都是相同的,我们可以对属性名定义进行省略。

1
2
3
4
5
6
7
8
9
const foo = 123;
const bar = () => foo;

const obj = {
foo,
bar,
};

console.log(obj); //{ foo: 123, bar: [Function] }

Object.assign()

Object.assign() 方法不能将提供者的访问器属性复制到接收对象中,由于 Object.assign() 方法执行了赋值操作,因此提供者的访问器属性最终会转变为接收对象中的一个数据属性

1
2
3
4
5
6
7
8
9
10
11
12
13
var receiver = {},
supplier = {
get name() {
return 'file.js';
},
};

Object.assign(receiver, supplier);

var descriptor = Object.getOwnPropertyDescriptor(receiver, 'name');

console.log(descriptor.value); // file.js
console.log(descriptor.get); // undefined

自有属性枚举顺序

ES5 没有定义对象属性的枚举顺序,ES6 规定了,这会影响到 Object.getOwnPropertyNames() 方法及 Reflect.ownKeys 返回属性的方式

自有属性枚举顺序的基本规则是:

  1. 所有数字键按升序排序
  2. 所有字符串键按照它们被加入对象的
  3. 所有 symbol 键按照它们被加入对象的顺序排序
1
2
3
4
5
6
7
8
9
10
11
var obj = {
a: 1,
0: 1,
c: 1,
2: 1,
b: 1,
1: 1,
};

obj.d = 1;
console.log(Object.getOwnPropertyNames(obj).join('')); // 012acbd

增强对象原型

简化原型访问的 Super 引用

Super 引用相当于指向对象原型的指针,实际也就是 Object.getPrototypeOf(this) 的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let person = {
getGreeting() {
return 'hello';
},
};

let friend = {
getGreeting() {
// return Object.getPrototypeOf(this).getGreeting.call(this) + ', hi'
// 功能同上
return super.getGreeting() + ', hi';
},
};
Object.setPrototypeOf(friend, person);

console.log(friend.getGreeting()); // hello, hi
  • 多重继承时,Object.getPrototypeOf() 方法会出现问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let person = {
getGreeting() {
return 'Hello';
},
};

let friend = {
getGreeting() {
// 使用 super 则正常
// return super.getGreeting() + ', hi'
return Object.getPrototypeOf(this).getGreeting.call(this) + ', hi';
},
};

Object.setPrototypeOf(friend, person);

let relative = Object.create(friend);

console.log(person.getGreeting()); // Hello
console.log(friend.getGreeting()); // Hello, hi
console.log(relative.getGreeting()); // RangeError: Maximum call stack size exceeded【如果使用 super 则正常】

Super 引用不是动态变化的,它总是指向正确的对象,在这个示例中,无论有多少其他方法继承了 getGreeting 方法,super.getGreeting() 始终指向 person.getGreeting() 方法

解构

数组解构

  • 解构赋值中,如果希望跳过数组中某些元素,可以通过空开一个元素的方式实现
1
2
3
4
// 用法:[ arg1, , arg2 ] = [ value1, value2, value3]

const [foo, , bar] = [1, 2, 3];
console.log(foo, bar); //1 3
  • 解构赋值中,获取指定位置的元素以外,也可以不定项地获取后续的元素,那么可以用 语句来实现,在被结构的数组中,不定元素必须为最后一个条目,否则报错:
1
2
3
4
5
// 用法:[ arg1, arg2, ...rest ] = [ value1, value2, value3, value4]

const [a, b, ...rest] = [1, 2, 3, 4, 5];
console.log(a, b); // 1 2
console.log(rest); // [3, 4, 5]
1
2
3
4
5
6
7
8
9
10
function fetchData() {
return new Promise((resolve, reject) => {
// ..
resolve(['foo', 'bar']);
});
}

fetchData().then(([value1, value2]) => {
console.log(value1, value2); // foo bar
});
1
2
3
4
5
6
7
8
9
// 变量值交换
let foo = 1;
let bar = 2;

// Swap
[foo, bar] = [bar, foo];

// After Swap
console.log(foo, bar); // 2 1
  • nullundefined 进行解构,会报错

对象解构

设置默认值,当无法匹配时使用

1
2
3
4
5
const { foo = 1 } = { bar: 1 };
console.log(foo); // 1

const [a, b = 2] = [1];
console.log(a, b); // 1 2

深度匹配

1
2
3
4
5
6
7
8
9
10
11
12
13
// Object
const {
a,
b: { c },
} = { a: 1, b: { c: 2 } };
console.log(a, c); //1 2

// Array in Object
const {
d,
e: [f],
} = { d: 1, e: [2, 3] };
console.log(d, f); //1 2
1
2
3
4
5
6
7
8
9
10
const arr = ['Mike', 'Peter', 'Ben', 'William', 'John'];

for (const [index, item] of arr.entries()) {
console.log(index, item);
if (item.match(/^W/)) break; // Break!
}
// 0 "Mike"
// 1 "Peter"
// 2 "Ben"
// 3 "William"

forEach无法像forwhile 等循环语句一样被break等控制语句终止,所以可以使用for-of循环语法

结构参数

必须传值的解构参数

1
2
3
4
5
function setCookie(name, value, { secure, path, domain, expires }) {
// ...
}

setCookie('type', 'js'); // TypeError: Cannot destructure property `secure` of 'undefined' or 'null'.

如果不传第 3 个参数,会导致其值为 undefined,等同于对 undefined 进行解构,可以通过如下方法解决

1
2
3
4
5
function setCookie(name, value, { secure, path, domain, expires } = {}) {
// ...
}

setCookie('type', 'js'); // 正常
  • 默认值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const setCookieDefaults = {
secure: false,
path: '/',
domain: 'example',
expires: new Date(Date.now() + 3600),
};

function setCookie(
name,
value,
{
secure = setCookieDefaults.secure,
path = setCookieDefaults.path,
domain = setCookieDefaults.domain,
expires = setCookieDefaults.expires,
} = setCookieDefaults
) {
// ...
}

补充

不只是声明

赋值表达式并不必须是变量标识符。任何合法的赋值表达式都可以

1
2
3
4
5
6
7
8
9
10
11
12
13
function foo() {
return [1, 2, 3];
}
function bar() {
return { x: 4, y: 5, z: 6 };
}

var o = {};
[o.a, o.b, o.c] = foo();
({ x: o.x, y: o.y, z: o.z } = bar());

console.log(o.a, o.b, o.c); // 1 2 3
console.log(o.x, o.y, o.z); // 4 5 6

重复赋值

对象解构形式允许多次列出同一个源属性(持有值类型任意)

1
2
3
var { a: X, a: Y } = { a: 1 };
X; // 1
Y; // 1
解构赋值表达式

对象或者数组解构的赋值表达式的完成值是所有右侧对象 / 数组的值

1
2
3
4
5
6
let o = { a: 1, b: 2, c: 3 };
let a, b, c, p;

p = { a, b, c } = o;
console.log(a, b, c); // 1 2 3
p === o; // true

p 赋值为对象 o 的引用

Symbol

  • Symbol 是一种值类型而非引用类型
  • Symbol 与一个字符串拼接,会报错

用法

  • Symbol 值通过Symbol函数生成。凡是属性名属于 Symbol 类型,就都是独一无二的,可以保证不会与其他属性名产生冲突。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    let s = Symbol();
    console.log(typeof s);
    // "symbol"

    let s1 = Symbol('foo');
    console.log(s1); // Symbol(foo)

    let s2 = Symbol('foo');
    console.log(s1 === s2); // false
  • Symbol函数可以接受一个字符串作为参数,表示对 Symbol 实例的描述,主要是为了在控制台显示,或者转为字符串时,比较容易区分

    1
    2
    3
    4
    5
    6
    7
    8
    let s1 = Symbol('foo');
    let s2 = Symbol('bar');

    console.log(s1); // Symbol(foo)
    console.log(s2); // Symbol(bar)

    console.log(s1.toString()); // "Symbol(foo)"
    console.log(s2.toString()); // "Symbol(bar)"

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

注册全局可重用 Symbol

开发者可以通过一个 key 向当前运行时注册一个需要在其他程序中使用的 Symbol:Symbol.for([key]) : Symbol

1
cosnt symbol = Symbol.for('foo');

Symbol.for() 会根据传入的 key 在全局作用域中注册一个 Symbol 值,如果某一个 key 从未被注册到全局作用域中,便会创建一个 Symbol 值并根据 key 注册到全局环境中,如果该 key 已被注册,就会返回一个与第一次使用创建的 Symbol 的值等价的 Symbol 值

1
2
let sy = Symbol.for('key');
console.log(sy === Symbol.for('key')); // true

Object.getOwnPropertySymbols

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

注意:Symbol 作为属性名,该属性不会出现在for...infor...of循环中,也不会被Object.keys()Object.getOwnPropertyNames()JSON.stringify()返回

1
2
3
4
5
6
7
8
9
10
11
const obj = {};
let a = Symbol('a');
let b = Symbol('b');

obj[a] = 'Hello';
obj[b] = 'World';

const objectSymbols = Object.getOwnPropertySymbols(obj);

console.log(objectSymbols);
// [Symbol(a), Symbol(b)]

获取全局 Symbol 的 key

使用同一个 Symbol 值,Symbol.for方法可以做到这一点,它接受一个字符串作为参数,然后搜索有没有以该参数作为名称的 Symbol 值。如果有,就返回这个 Symbol 值,否则就新建并返回一个以该字符串为名称的 Symbol 值

用法:Symbol.keyFor(someSymbol) : String

1
2
const symbol = Symbol.for('foo');
console.log(Symbol.keyFor(symbol)); // foo

通过 well-known Symbol 暴露内部操作

  • Symbol.hasInstance:用于确定对象是否为函数的实例,用于重新定义 instanceof 操作符返回结果。不可写,不可配置,不可枚举
  • Symbol.isConcatSpreadable:一个布尔值,用于表示当传递一个集合作为 Array.prototype.concat() 方法的参数时,是否应该将集合内的元素规整到同一层级
  • Symbol.iterator:返回迭代器的方法
  • Symbol.match: 在调用 String.prototype.match() 方法时调用的方法
  • Symbol.replace:在调用 String.prototype.replace() 方法时调用的方法
  • Symbol.search:在调用 String.prototype.search() 方法时调用的方法
  • Symbol.species:用于创建派生对象的构造函数
  • Symbol.split:在调用 String.prototype.split() 方法时调用的方法
  • Symbol.toPrimitive:返回对象原始值的方法
  • Symbol.toStringTag:调用 Object.prototype.toString() 方法时使用的字符串
  • Symbol.unscopables:定义一些不可被 with 语句引用的对属性名称的对象集合

Symbol.hasInstance

obj instanceof Array 等价于 Array[Symbol.hasInstance](obj)

Symbol.hasInstance 为开发者提供了可以用于扩展 instanceof 语句内部逻辑的权限,开发者可以将其作为属性键,或用于为一个类定义静态方法,该方法的第一个形参便是被检测的对象,而该方法的返回值便是决定了 instanceof 语句的返回结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 用法1
function SpecialNumber() {}

Object.defineProperty(SpecialNumber, Symbol.hasInstance, {
value: function (v) {
return v instanceof Number && v >= 1 && v <= 100;
},
});

let zero = new Number(0);
let one = new Number(1);

console.log(zero instanceof SpecialNumber); // false
// equal
console.log(SpecialNumber[Symbol.hasInstance](zero)); // false

console.log(one instanceof SpecialNumber); // true
// equal
console.log(SpecialNumber[Symbol.hasInstance](one)); // true
1
2
3
4
5
6
7
8
9
// 用法2
class Foo {
static [Symbol.hasInstance](obj) {
console.log(obj); // {}
return true;
}
}

console.log({} instanceof Foo); // true

Symbol.isConcatSpreadable

Symbol.isConcatSpreadable 属性是一个布尔值,如果该布尔值为 true,则表示对象有 length 属性和数字键,故它的数值型属性值应该被独立添加到 concat() 调用的结果中

1
2
3
4
5
6
7
8
9
let collection = {
0: 'zero',
1: 'one',
length: 2,
[Symbol.isConcatSpreadable]: true,
};

let messages = ['begin'].concat(collection);
console.log(messages); // [ 'begin', 'zero', 'one' ]

Symbol.match, Symbol.replace, Symbol.search, Symbol.split

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 实际上等价于  /^.{10}$/
let hasLengthOf10 = {
[Symbol.match]: function (value) {
return value.length === 10 ? [value] : null;
},
};

let msg1 = 'hello world';
let msg2 = 'hello John';

let match1 = msg1.match(hasLengthOf10);
let match2 = msg2.match(hasLengthOf10);

console.log(match1); // null
console.log(match2); // ['hello John']

Symbol.toPrimitive

JS 执行特定操作时,会尝试将对象转换到相应的原始值。例如,使用 == 运算符比较对象和字符串,对象会在比较操作执行前被转换为一个原始值。

Symbol.toPrimitive 方法被定义在每一个标准类型的原型上,并且规定了当对象呗转换为原始值时应当执行的操作。当执行原始值转换时,总会调用 Symbol.toPrimitive 方法并传入一个参数,这个参数值被称为类型提示(hint)。

类型提示参数的值只有 3 种:**’number’, ‘string’ 或 ‘default’**。传递这些参数时, Symbol.toPrimitive 返回的分别是 数字、字符串或无类型偏好的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function Temperature(degrees) {
this.degrees = degrees;
}

Temperature.prototype[Symbol.toPrimitive] = function (hint) {
switch (hint) {
case 'string':
return this.degrees + '°';
break;
case 'number':
return this.degrees;
case 'default':
return this.degrees + ' degrees';
}
};

let freezing = new Temperature(32);

console.log(freezing + '!'); // 32 degrees!
console.log(freezing / 2); // 16
console.log(String(freezing)); // 32°

Symbol.toStringTag

1
2
3
4
5
6
7
8
9
function Person() {}

let pj = new Person();

console.log(pj.toString()); // [object Object]

Person.prototype[Symbol.toStringTag] = 'Person';

console.log(pj.toString()); // [object Person]

Set & Map

Set

  • 初始化:Set 函数可以接受一个数组(或者具有 iterable 接口的其他数据结构)作为参数,用来初始化
  • 通过 add、delete 和 clear 方法来添加、删除和清空集合内的元素。
  • 通过 has 方法用于检查某集合中是否包含某一个元素。
  • size 属性获取数量
  • 引擎内部使用 Object.is() 方法检测两个值是否一致。唯一的例外是,Set 集合中的 +0 和 -0 被认为是相等的
  • 如果多次调用 add() 方法传入相同的值作为参数,那么后续的调用实际上会被忽略
  • forEach 的回调函数接受 3 个参数:值,与第一个参数一样的值,被遍历的 Set 集合
1
2
3
4
5
6
7
8
9
10
let set = new Set([1, 2]);
set.forEach(function (val, key, ownSet) {
console.log(key + ' ' + val);
console.log(ownSet === set);
});
// output
// 1 1
// true
// 2 2
// true

WeakSet

与 Set 的差别:

  • 如果 add() 方法传入非对象参数会导致程序报错,向 has() 和 delete() 方法传入非对象参数则会返回 false
  • 不可迭代,不能使用 for-of 循环,不能使用 forEach 方法
  • 不暴露任何迭代器(如 keys() 和 values())
  • 不支持 size

实现将字符串等值类型加入到 WeakSet 数据结构中:

1
2
3
4
5
6
7
8
var ws = new WeakSet();
var str = new String('Hello');

ws.add(str);
console.log(ws.has(str)); // true

str += 'world';
console.log(ws.has(str)); // false

被加入到 WeakSet 中的字符串和数字等不能被修改,因为一旦进行修改其引用便会丢失,甚至导致被移除出集合。

WeakSet 不能包含无引用的对象,否则会自动清除出集合

1
2
3
4
5
const weakset = new WeakSet();
let foo = {};
weakset.add(foo);
foo = null;
console.log(weakset.has(foo)); // false

Map

  • set(key, value) :添加键值对到映射中
  • get(key):获取映射中某一个键的对应值
  • delete(key):将某一键值对移除出映射中
  • clear(): 清空映射中所有的键值对
  • has(key):检查映射中是否包含某一键值对
  • entries():返回一个以二元数组(键值对)作为元素的数组
  • keys(): 返回一个一当前映射中所有键作为元素的可迭代对象
  • values():返回一个一当前映射中所有值作为元素的可迭代对象
  • map.size:键值对数量
  • forEach 的回调函数接受 3 个参数:值,值对应的键名,被遍历的 Map 集合
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let myMap = new Map([
['key1', 1],
['key2', 2],
]);
myMap.set('key3', 3);

for (let [key, v] of myMap) {
console.log(`${key} ==> ${v}`);
}
// key1 ==> 1
// key2 ==> 2
// key3 ==> 3

for (let v of myMap) {
console.log(v);
}
// [ 'key1', 1 ]
// [ 'key2', 2 ]
// [ 'key3', 3 ]

console.log([...myMap]); // [ [ 'key1', 1 ], [ 'key2', 2 ], [ 'key3', 3 ] ]

WeakMap

  • 键名必须是一个对象,否则报错

迭代器(Iterator) & 生成器(Generator)

https://sluggishpj.github.io/blog/2018/08/28/es6-iterator-generator/

类(Class)

类的声明

  • 类属性不可被重新赋值,只可读

  • 类声明与 let 声明类似,不能被提升

  • 类声明中的所有代码自动运行在严格模式下,而且无法强行让代码脱离严格模式执行

  • 类中所有方法都是不可枚举的

  • Class 的取值函数(getter)和存值函数(setter)

与 ES5 一样,在“类”的内部可以使用 getset 关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。存值函数和取值函数会被子类继承

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

console.log(inst.prop);
// 'getter'

类表达式

1
2
3
4
5
6
7
8
9
10
11
12
let Person = class {
constructor(name) {
this.name = name;
}

sayName() {
console.log(this.name);
}
};

let pj = new Person('pj');
pj.sayName(); // pj

命名类表达式

1
2
3
4
5
6
7
8
9
10
11
12
let Person = class PersonClass {
constructor(name) {
this.name = name;
}

sayName() {
console.log(this.name);
}
};

console.log(typeof Person); // function
console.log(typeof PersonClass); // undefined

作为一等公民的类

1
2
3
4
5
6
7
8
9
10
let pj = new (class {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
})('pj');

pj.sayName(); // pj

Class 继承

  • 如果一个子类继承了一个父类,**子类在构造函数中访问 this 前一定要调用 super()**,否则新建实例时会报错。
  • 如果不调用 super 方法,子类就得不到 this 对象。如果子类不写 constructor 方法,默认调用父类的 constructor 方法
1
2
3
4
5
class Square extends Rectangle {
constructor() {}
}

let square = new Square(3); // ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
1
2
3
4
5
6
7
8
9
10
class Square extends Rectangle {
// 没有构造函数
}

// 等价于
class Square extends Rectangle {
constructor(...args) {
super(...args);
}
}

静态成员继承

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
class Rectangle {
constructor(length, width) {
this.length = length;
this.width = width;
}

getArea() {
return this.length * this.width;
}

static create(length, width) {
return new Rectangle(length, width);
}
}

class Square extends Rectangle {
constructor(length) {
super(length, length);
}
}

let rect = Square.create(3, 4);

console.log(rect instanceof Rectangle); // true
console.log(rect.getArea()); // 12
console.log(rect instanceof Square); // false

继承自表达式的类

  • ES2015 的继承语法同样可以将以前使用的构造函数模拟的类作为父类来继承
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Rectangle(length, width) {
this.length = length;
this.width = width;
}

Rectangle.prototype.getArea = function () {
return this.length * this.width;
};

class Square extends Rectangle {
constructor(length) {
super(length, length);
}
}

let square = new Square(3);
console.log(square.getArea()); // 9
console.log(square instanceof Rectangle); // true

内建对象继承

ES5 之前无法通过继承的方式创建 特殊数组

1
2
3
4
5
6
7
8
9
class MyArray extends Array {}

let colors = new MyArray();
colors[0] = 'red';

console.log(colors.length); // 1

colors.length = 0;
console.log(colors[0]); // undefined

Symbol.species

用于定义子类的方法返回实例时,应当返回的值的类型。该属性被返回的函数是一个构造函数,每当要在实例的方法中(不是构造函数中)创建类的实例时必须使用这个构造函数。

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
class MyClass {
static get [Symbol.species]() {
return this;
}

constructor(value) {
this.value = value;
}

clone() {
return new this.constructor[Symbol.species](this.value);
}
}

class MyDerivedClass1 extends MyClass {}

class MyDerivedClass2 extends MyClass {
static get [Symbol.species]() {
return MyClass;
}
}

let instance1 = new MyDerivedClass1('foo');
let clone1 = instance1.clone();

let instance2 = new MyDerivedClass2('bar');
let clone2 = instance2.clone();

console.log(clone1 instanceof MyClass); // true
console.log(clone1 instanceof MyDerivedClass1); // true
console.log(clone2 instanceof MyClass); // true
console.log(clone2 instanceof MyDerivedClass2); // false

MyDerivedClass1 继承 MyClass 时未改变 Symbol.species 属性, this.constructor[Symbol.species] 的返回值是 MyDerivedClass1clone() 返回的是 MyDerivedClass1 的实例
MyDerivedClass2 继承 MyClass 时重写了 Symbol.species 让其返回 MyClassclone() 返回的是一个 MyClass 实例

在类构造函数中使用 new.target

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Rectngle {
constructor(length, width) {
console.log(new.target === Rectngle);
this.length = length;
this.width = width;
}
}

let rect = new Rectngle(3, 4); // 输出 true

class Square extends Rectngle {
constructor(length) {
super(length, length);
}
}

let square = new Square(3); // 输出 false

改进的数组

新增方法

定型数组

https://sluggishpj.github.io/blog/2018/07/28/es6-typed-array/

Promise

使用方法

1
2
3
4
5
6
7
8
9
10
11
let myPromise = new Promise(function (resolve, reject) {
let randomNum = Math.random();

if (randomNum > 0.5) {
resolve(randomNum);
} else {
reject(randomNum);
}
});

myPromise.then(handleSuccess, handleFail);

resolve函数的参数除了正常的值以外,还可能是另一个 Promise 实例

1
2
3
4
5
6
7
8
9
10
const p1 = new Promise(function (resolve, reject) {
// ...
});

const p2 = new Promise(function (resolve, reject) {
// ...
resolve(p1);
});

p2.then((result) => console.log('result', result)).catch((error) => console.log('error', error));

p1的状态决定了p2的状态。如果p1的状态是pending,那么p2的回调函数就会等待p1的状态改变;如果p1的状态已经是resolved或者rejected,那么p2的回调函数将会立刻执行

p1的状态决定p2的状态。所以,后面的then语句都变成针对p1

then方法返回的是一个新的Promise实例(注意,不是原来那个Promise实例),因此可以采用链式写法

一般来说,不要在then方法里面定义 Reject 状态的回调函数(即then的第二个参数),总是使用catch方法。

捕获错误

跟传统的try/catch代码块不同的是,如果没有使用catch方法指定错误处理的回调函数,Promise 对象抛出的错误不会传递到外层代码,即不会阻止其他代码的执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const someAsyncThing = function () {
return new Promise(function (resolve, reject) {
// 下面一行会报错,因为x没有声明
resolve(x + 2);
});
};

someAsyncThing().then(function () {
console.log('everything is great');
});

setTimeout(() => {
console.log(123);
}, 2000);
// Uncaught (in promise) ReferenceError: x is not defined
// 123(两秒后输出)

注意事项

  • 如果一个 Promise 处于已处理状态,在这之后添加到任务队列中的处理程序仍将执行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const fs = require('fs');

let promise = new Promise(function (resolve, reject) {
fs.readFile('./somefile.js', 'utf8', function (err, contents) {
if (err) return reject(err);
resolve(contents);
});
});

promise.then(function (contents) {
console.log(contents); // 文件内容

promise.then(function (contents) {
console.log(contents); // 输出同样内容
});
});
  • Promise 的执行器会立即执行,然后才执行后续流程中的代码
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
// Hi
// Resolved

全局的 Promise 拒绝处理

Node.js 环境的拒绝处理

处理 Promise 拒绝时会触发 process 对象上的两个事件

  • unhandledRejection: 在一个事件循环中,当 Promise 被拒绝,并且没有提供拒绝处理程序时,触发该事件
  • rejectionHandled:在一个事件循环后,当 Promise 被拒绝时,若拒绝处理程序被调用,触发该事件
1
2
3
4
5
6
7
8
9
10
// unhandledRejection
let rejected = Promise.reject(new Error('Explosion!'));

process.on('unhandledRejection', function (reason, promise) {
console.log(reason.message);
console.log(rejected === promise);
});

// Explosion!
// true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// rejectionHandled
let rejected = Promise.reject(new Error('Explosion!'));

process.on('rejectionHandled', function (promise) {
console.log(rejected === promise);
});

setTimeout(() => {
rejected.catch((err) => {
console.log(err.message);
});
}, 1000);

// 1s 后输出
// true
// Explosion!

Promise 链的返回值

在完成处理程序中指定一个返回值,就可以沿着这条链继续传递数据

1
2
3
4
5
6
7
8
9
10
let p1 = new Promise(function (resolve, reject) {
resolve(42);
});

p1.then(function (value) {
console.log(value); // 42
return value + 1;
}).then(function (value) {
console.log(value); // 43
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
let p1 = new Promise(function (resolve, reject) {
resolve(42);
});

let p2 = new Promise(function (resolve, reject) {
resolve(43);
});

p1.then(function (value) {
console.log(value); // 42
return p2;
}).then(function (value) {
console.log(value); // 43
});

Promise.all(iterable)

该方法传入一个可迭代对象(如数组),并返回一个 Promise 对象,该 Promise 对象会在当可迭代对象中的所有 Promise 对象都进入完成状态(包括成功和失败)后被激活。

​ 如果可迭代对象中的所有 Promise 对象都进入了成功状态,那么该方法返回的 Promise 对象也会进入成功状态,并以一个可迭代对象来承载其中一个的所有返回值。

​ 如果可迭代对象中 Promise 对象的其中一个进入了失败状态,那么该方法返回的 Promise 对象也会进入失败状态,并以那个进入失败状态的错误信息作为自己的错误信息。

1
2
3
4
5
6
const promise = [async(1), async(2), async(3), async(4)];
Promise.all(promises)
.then((values) => {
// ... values 是个数组,含4个返回结果
})
.catch((err) => console.error(err));

Promise.race(iterable)

这个方法会监听所有的 Promise 对象,并等待其中的第一个进入完成状态的 Promise 对象。一旦有第一个 Promise 对象进入了完成状态,该方法返回的 Promise 对象便会根据这第一个完成的 Promsie 对象的状态而改变。

1
2
3
4
5
6
const promise = [async(1), async(2), async(3), async(4)];
Promise.race(promises)
.then((values) => {
// ...
})
.catch((err) => console.error(err));

Promise.resolve()

将现有对象转为 Promise 对象

1
const jsPromise = Promise.resolve($.ajax('/whatever.json'));
  • 如果参数是 Promise 实例,那么Promise.resolve将不做任何修改

  • 参数是一个 thenable 对象(thenable 对象指的是具有then方法的对象,Promise.resolve方法会将这个对象转为 Promise 对象,然后就立即执行thenable对象的then方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    let thenable = {
    then: function (resolve, reject) {
    resolve(42);
    },
    };

    let p1 = Promise.resolve(thenable);
    p1.then(function (value) {
    console.log(value); // 42
    });

    thenable对象的then方法执行后,对象p1的状态就变为resolved,从而立即执行最后那个then方法指定的回调函数,输出 42

  • 如果参数是一个原始值,或者是一个不具有then方法的对象,则Promise.resolve方法返回一个新的 Promise 对象,状态为resolved

    1
    2
    3
    4
    5
    6
    const p = Promise.resolve('Hello');

    p.then(function (s) {
    console.log(s);
    });
    // Hello
  • 不带有任何参数

    Promise.resolve方法允许调用时不带参数,直接返回一个resolved状态的 Promise 对象

    1
    2
    3
    4
    5
    const p = Promise.resolve();

    p.then(function () {
    // ...
    });

    立即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

Promise.reject()

Promise.reject(reason)方法也会返回一个新的 Promise 实例,该实例的状态为rejected

Promise.reject()方法的参数,会原封不动地作为reject的理由,变成后续方法的参数

代理(Proxy)和反射(Reflection)

https://sluggishpj.github.io/blog/2018/09/25/es6-proxy-reflection/

模块

导入 import

注意:当从模块导入一个绑定时,它相当于使用 const 定义的一样,因此无法定义同名变量,也无法改变绑定的值

1
2
3
4
5
// 择需加载
import { isInteger } from '../jutil.js';
import { isInteger, name } from '../jutil.js';
// 模块的整体加载
import * as jutil from '../jutil.js';

导出 export

注意:不能单独使用 export 导出匿名函数或类,除非结合使用 default 关键字

1
2
3
4
5
export function isInteger(num) {
return num === Math.round(num);
}

export const name = 'pj';

导出默认值

1
2
3
4
5
6
7
8
9
10
// 为模块指定默认输出。
// export-default.js
export default function () {
console.log('foo');
}

// 其他模块加载该模块时,import命令可以为该匿名函数指定任意名字。
// import-default.js
import customName from './export-default';
customName(); // 'foo'

重新导出一个绑定

1
2
3
4
5
import { sum } from './example.js';
export { sum };

// 等价于
export { sum } from './example.js';

无绑定导入

用来执行模块代码

1
import './example.js';

Web 浏览器中使用模块

在 <script> 中使用模块

将 type 设置为 ‘module’ 即可。

注意:执行时自动应用 defer 属性。文件名必须带上后缀(可以为 js 或 mjs)

1
<script type="module" src="./module.js"></script>

将模块作为 Worker 加载

1
let worker = new Worker('test2.js', { type: 'module' });

浏览器模块说明符解析

  • 以 / 开头的解析为从根目录开始
  • 以 ./ 开头的解析为从当前目录开始

注意点

  • export 和 import 的一个重要的限制是,它们必须在其他语句和函数之外使用,必须在最外层作用域
  • 代码是在模块作用域之中运行,而不是在全局作用域运行。模块内部的顶层变量,外部不可见
  • 模块脚本自动采用严格模式,不管有没有声明use strict
  • 模块之中,可以使用import命令加载其他模块,也可以使用export命令输出对外接口
  • 模块之中,顶层的this关键字返回undefined,而不是指向window。也就是说,在模块顶层使用this关键字,是无意义的
  • 模块是单例,同一个模块如果加载多次,将只执行一次
  • ES6 模块的 API 是静态的。也就是说,需要在模块的公开 API 中静态定义所有最高层导出,之后无法补充。
  • 模块的公开 API 中暴露的属性和方法并不仅仅是普通的值或引用的赋值。它们是到内部模块定义中的标识符的实际绑定(几乎类似于指针)。导出一个局部私有变量,即使当前它持有一个原生字符串 / 数字等,导出的都是到这个变量的绑定。如果模块修改了这个变量的值,外部导入绑定现在会决议到新的值。eg.
1
2
3
4
5
6
// count.mjs
export let num = 1;

setTimeout(() => {
num = 2;
}, 1000);
1
2
3
4
5
6
7
8
// index.mjs
import { num } from './count.mjs';

console.log(num); // 1

setTimeout(() => {
console.log(num); // 2
}, 2000);
  • 导入模块和静态请求加载(如果还没加载的话)这个模块是一样的。如果是在浏览器环境中,这意味着通过网络阻塞加载;如果是在服务器上(比如 Node.js),则是从文件系统的阻塞加载。但是,不要惊慌于这里的性能暗示。因为 ES6 模块具有静态定义,导入需求可以静态扫描预先加载,甚至是在使用这个模块之前。

与 CommonJS 模块差异

  1. CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用

    • 值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值
    • JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值,不能重新赋值(好比是 const 变量)
1
2
3
4
5
// util.js
export var name = 'pj';
export function changeName() {
name = 'rj';
}
1
2
3
4
5
6
7
8
9
// index.js
import { name, changeName } from './util.js';
console.log(name); // pj

changeName();

setTimeout(() => {
console.log(name); // rj
}, 1000);

模块依赖环

模块 A

1
2
3
4
5
import bar from 'B';
export default function foo(x) {
if (x > 10) return bar(x - 1);
return x * 2;
}

模块 B

1
2
3
4
5
import foo from 'A';
export default function bar(y) {
if (y > 5) return foo(y / 2);
return y * 3;
}

ES6 模块声明在完全不同的作用域,所以 ES6 需要额外的工作来支持这样的循环引用。

下面是从粗略概念的意义上循环的 import 依赖如何生效和解析的过程。

  • 如果先加载模块 “A”,第一步是扫描这个文件分析所有的导出,这样就可以注册所有可以导入的绑定。然后处理 import .. from "B",这表示它需要取得 “B”。
  • 引擎加载 “B” 之后,会对它的导出绑定进行同样的分析。当看到 import .. from "A",它已经了解 “A” 的 API,所以可以验证 import 是否有效。现在它了解 “B” 的 API,就可以验证等待的 “A” 模块中 import .. from "B" 的有效性。
1
2
import foo from 'foo';
foo(25); // 11
1
2
import bar from 'bar';
bar(25); // 11.5

foo(25)bar(25) 调用执行的时候,所有模块的所有分析 / 编译都已经完成。这意味着 foo(..) 内部已经直接了解 bar(..),而 bar(..) 内部也已经直接了解 foo(..)

import 语句的静态加载语义意味着可以确保通过 import 相互依赖的 “foo” 和 “bar” 在其中任何一个运行之前,二者都会被加载、解析和编译。

REF

参考链接 1:http://blog.csdn.net/kaelyn_X

参考链接 2:http://es6.ruanyifeng.com/

参考书籍:《UNDERESTANDING ECMASCRIPT 6》&《You Don’t Know JS》