Set和Map
ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。类似Java的Set
const s = new Set(); [2, 3, 5, 4, 5, 2, 2].forEach(x => s.add(x)); for (let i of s) { console.log(i); } // 2 3 5 4 const items = new Set([1, 2, 3, 4, 5, 5, 5, 5]); items.size // 5 // 去除数组的重复成员 [...new Set(array)] // 去除字符串里面的重复字符 [...new Set('ababbc')].join('') // "abc" // set([NaN, NaN]) // Set {NaN} 认为NaN相等 // set([{},{}]) // set.size 2, 空对象不等
Set.prototype.constructor:构造函数,默认就是Set函数
Set.prototype.size
:返回Set
实例的成员总数Set.prototype.add(value)
:添加某个值,返回 Set 结构本身Set.prototype.delete(value)
:删除某个值,返回一个布尔值,表示删除是否成功Set.prototype.has(value)
:返回一个布尔值,表示该值是否为Set
的成员Set.prototype.clear()
:清除所有成员,没有返回值Set.prototype.keys()
:返回键名的遍历器Set.prototype.values()
:返回键值的遍历器Set.prototype.entries()
:返回键值对的遍历器Set.prototype.forEach()
:使用回调函数遍历每个成员
keys()
,values()
,entries()方法返回的都是遍历器对象由于 Set 结构没有键名,只有键值(或者说键名和键值是同一个值),所以keys方法和values方法的行为完全一致
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"]
WeakSet
WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet 之中。
Map
类似Java的Map,如果 Map 的键是一个简单类型的值(数字、字符串、布尔值),则只要两个值严格相等,Map 将其视为一个键,比如
0
和-0
就是一个键,布尔值true
和字符串true
则是两个不同的键。另外,undefined
和null
也是两个不同的键。虽然NaN
不严格相等于自身,但 Map 将其视为同一个键。
size
属性返回 Map 结构的成员总数Map.prototype.set(key, value)set
方法设置键名key
对应的键值为value
Map.prototype.get(key)get
方法读取key
对应的键值,如果找不到key
,返回undefined
。Map.prototype.has(key)has
方法返回一个布尔值,表示某个键是否在当前 Map 对象之中。Map.prototype.delete(key)delete
方法删除某个键,返回true
。如果删除失败,返回false
。Map.prototype.clear() clear
方法清除所有成员,没有返回值。Map.prototype.keys()
:返回键名的遍历器。Map.prototype.values()
:返回键值的遍历器。Map.prototype.entries()
:返回所有成员的遍历器。Map.prototype.forEach()
:遍历 Map 的所有成员const map = new Map(); map.set('foo', true); map.set('bar', false); map.size // 2 // 支持链式插入 let map = new Map() .set(1, 'a') .set(2, 'b') .set(3, 'c'); // Map的遍历 const map = new Map([ ['F', 'no'], ['T', 'yes'], ]); for (let key of map.keys()) { console.log(key); } // "F" // "T" for (let value of map.values()) { console.log(value); } // "no" // "yes" for (let item of map.entries()) { console.log(item[0], item[1]); } // "F" "no" // "T" "yes" // 或者 for (let [key, value] of map.entries()) { console.log(key, value); } // "F" "no" // "T" "yes" // 等同于使用map.entries() for (let [key, value] of map) { console.log(key, value); } // "F" "no" // "T" "yes"
Map与其它类型相互转换
// 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 转为对象,如果所有 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));
WeakMap
如果将数值1和Symbol值作为 WeakMap 的键名,都会报错,WeakMap
的键名所指向的对象,不计入垃圾回收机制。注意,WeakMap 弱引用的只是键名,而不是键值。键值依然是正常引。
WeakRef
WeakSet 和 WeakMap 是基于弱引用的数据结构,ES2021 更进一步,提供了 WeakRef 对象,用于直接创建对象的弱引用。WeakRef 实例对象有一个
deref()
方法,如果原始对象存在,该方法返回原始对象;如果原始对象已经被垃圾回收机制清除,该方法返回undefined
。let target = {}; let wr = new WeakRef(target); let obj = wr.deref(); if (obj) { // target 未被垃圾回收机制清除 // ... }
Proxy
代理器,可以代理施加在原对象。
var proxy = new Proxy(target, handler);
如果
handler
没有设置任何拦截,那就等同于直接通向原对象。var target = {}; var handler = {}; var proxy = new Proxy(target, handler); proxy.a = 'b'; target.a // "b"
上面代码中,
handler
是一个空对象,没有任何拦截效果,访问proxy
就等同于访问target
。同一个拦截器函数,可以设置拦截多个操作。
var handler = { get: function(target, name) { if (name === 'prototype') { return Object.prototype; } return 'Hello, ' + name; }, apply: function(target, thisBinding, args) { return args[0]; }, construct: function(target, args) { return {value: args[1]}; } }; var fproxy = new Proxy(function(x, y) { return x + y; }, handler); fproxy(1, 2) // 1 new fproxy(1, 2) // {value: 2} fproxy.prototype === Object.prototype // true fproxy.foo === "Hello, foo" // true
对于可以设置、但没有设置拦截的操作,则直接落在目标对象上,按照原先的方式产生结果。
下面是 Proxy 支持的拦截操作一览,一共 13 种。
- get(target, propKey, receiver):拦截对象属性的读取,比如
proxy.foo
和proxy['foo']
。- set(target, propKey, value, receiver):拦截对象属性的设置,比如
proxy.foo = v
或proxy['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)
。
Reflect
Reflect
对象与Proxy
对象一样,也是 ES6 为了操作对象而提供的新 API。Reflect
对象的设计目的有这样几个。(1) 将
Object
对象的一些明显属于语言内部的方法(比如Object.defineProperty
),放到Reflect
对象上。现阶段,某些方法同时在Object
和Reflect
对象上部署,未来的新方法将只部署在Reflect
对象上。也就是说,从Reflect
对象上可以拿到语言内部的方法。// 老写法 try { Object.defineProperty(target, property, attributes); // success } catch (e) { // failure } // 新写法 if (Reflect.defineProperty(target, property, attributes)) { // success } else { // failure }
(2) 修改某些
Object
方法的返回结果,让其变得更合理。比如,Object.defineProperty(obj, name, desc)
在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj, name, desc)
则会返回false
。(3)让
Object
操作都变成函数行为。某些Object
操作是命令式,比如name in obj
和delete obj[name]
,而Reflect.has(obj, name)
和Reflect.deleteProperty(obj, name)
让它们变成了函数行为。// 老写法 'assign' in Object // true // 新写法 Reflect.has(Object, 'assign') // true
(4)
Reflect
对象的方法与Proxy
对象的方法一一对应,只要是Proxy
对象的方法,就能在Reflect
对象上找到对应的方法。这就让Proxy
对象可以方便地调用对应的Reflect
方法,完成默认行为,作为修改行为的基础。也就是说,不管Proxy
怎么修改默认行为,你总可以在Reflect
上获取默认行为。Proxy(target, { set: function(target, name, value, receiver) { var success = Reflect.set(target, name, value, receiver); if (success) { console.log('property ' + name + ' on ' + target + ' set to ' + value); } return success; } });
下面是另一个例子。
var loggedObj = new Proxy(obj, { get(target, name) { console.log('get', target, name); return Reflect.get(target, name); }, deleteProperty(target, name) { console.log('delete' + name); return Reflect.deleteProperty(target, name); }, has(target, name) { console.log('has' + name); return Reflect.has(target, name); } });
上面代码中,每一个
Proxy
对象的拦截操作(get
、delete
、has
),内部都调用对应的Reflect
方法,保证原生行为能够正常执行。添加的工作,就是将每一个操作输出一行日志。
Reflect
对象一共有 13 个静态方法。
- Reflect.apply(target, thisArg, args)
- Reflect.construct(target, args)
- Reflect.get(target, name, receiver)
- Reflect.set(target, name, value, receiver)
- Reflect.defineProperty(target, name, desc)
- Reflect.deleteProperty(target, name)
- Reflect.has(target, name)
- Reflect.ownKeys(target)
- Reflect.isExtensible(target)
- Reflect.preventExtensions(target)
- Reflect.getOwnPropertyDescriptor(target, name)
- Reflect.getPrototypeOf(target)
- Reflect.setPrototypeOf(target, prototype)
实现一个观察者模式
// 存放被触发方法 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; } // 被观察对象 const person = observable({ name: '张三', age: 20 }); function print() { console.log(`${person.name}, ${person.age}`) } observe(print); person.name = '李四';
Promise
理解了观察者模式,对于Promise的理解应该会变得很轻松。Promise就是一个存放一系列事件的容器,这些事件通常是一个个的异步操作。当Promise的内部状态值发生变化时,从pending变为fulfilled和从pending变为rejected,会一直保持这个结果,这时就称为 resolved(已定型)此时就会执行我们此前订阅的一系列异步事件。
有了
Promise
对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise
对象提供统一的接口,使得控制异步操作更加容易。简单的写法:
Promise
构造函数接受一个函数作为参数,该函数的两个参数分别是resolve
和reject
。
resolve
函数的作用是,将Promise
对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved)
reject
函数的作用是,将Promise
对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected)const promise = new Promise(function(resolve, reject) { // ... some code if (/* 异步操作成功 */){ resolve(value); } else { reject(error); } });
Promise
实例生成以后,可以用then
方法分别指定resolved
状态和rejected
状态的回调函数。promise.then(function(value) { // success }, function(error) { // failure });
then
方法可以接受两个回调函数作为参数。第一个回调函数是Promise
对象的状态变为resolved
时调用,第二个回调函数是Promise
对象的状态变为rejected
时调用。这两个函数都是可选的,不一定要提供。它们都接受Promise
对象传出的值作为参数。Promise还支持catch,是代码可读性加强,ES2018引入了finally。下面是一个更优的写法
promise .then(result => {···}) .catch(error => {···}) .finally(() => {···});
Promise.all()
Promise.all()
方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。
(2)只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。
const p = Promise.all([p1, p2, p3]) .then(function (posts) { // ... }) .catch(function(reason){ // ... });;
Promise.race()
const p = Promise.race([p1, p2, p3]);
上面代码中,只要
p1
、p2
、p3
之中有一个实例率先改变状态,p
的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p
的回调函数。Promise.allSettled() ES2020
只有等到参数数组的所有 Promise 对象都发生状态变更,不管是
fulfilled
还是rejected
const promises = [ fetch('/api-1'), fetch('/api-2'), fetch('/api-3'), ]; await Promise.allSettled(promises); removeLoadingIndicator();
Promise.any()
Promise.any()
跟Promise.race()
方法很像,只有一点不同,就是Promise.any()
不会因为某个 Promise 变成rejected
状态而结束,必须等到所有参数 Promise 变成rejected
状态才会结束。Promise.resolve() 、Promise.reject()
有时需要将现有对象转为 Promise 对象,Promise.resolve()方法就起到这个作用。Promise.resolve()等价于下面的写法。
Promise.resolve('foo') // 等价于 new Promise(resolve => resolve('foo'))
const p = Promise.reject('出错了'); // 等同于 const p = new Promise((resolve, reject) => reject('出错了'))
Promise.try()
database.users.get()
可能还会抛出同步错误(比如数据库连接错误,具体要看实现方法),这时你就不得不用try...catch
去捕获。try { database.users.get({id: userId}) .then(...) .catch(...) } catch (e) { // ... }
Iterator
Iterator 的作用有三个:一是为各种数据结构,提供一个统一的、简便的访问接口;二是使得数据结构的成员能够按某种次序排列;三是 ES6 创造了一种新的遍历命令
for...of
循环,Iterator 接口主要供for...of
消费。JavaScript 原有的
for...in
循环,只能获得对象的键名,不能直接获取键值。ES6 提供for...of
循环,允许遍历获得键值。var arr = ['a', 'b', 'c', 'd']; for (let a in arr) { console.log(a); // 0 1 2 3 } for (let a of arr) { console.log(a); // a b c d }
下面是一个模拟
next
方法返回值的例子。var it = makeIterator(['a', 'b']); it.next() // { value: "a", done: false } it.next() // { value: "b", done: false } it.next() // { value: undefined, done: true } function makeIterator(array) { var nextIndex = 0; return { next: function() { return nextIndex < array.length ? {value: array[nextIndex++], done: false} : {value: undefined, done: true}; } }; }
ES6 规定,默认的 Iterator 接口部署在数据结构的
Symbol.iterator
属性,或者说,一个数据结构只要具有Symbol.iterator
属性,就可以认为是“可遍历的”原生具备 Iterator 接口的数据结构如下。
- Array
- Map
- Set
- String
- TypedArray
- 函数的 arguments 对象
- NodeList 对象
下面的例子是数组的
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 }
除此之外,其他数据结构(主要是对象)的 Iterator 接口,都需要自己在
Symbol.iterator
属性上面部署,这样才会被for...of
循环遍历。下面是为对象添加 Iterator 接口的例子。
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 }; } }; } };
class RangeIterator { constructor(start, stop) { this.value = start; this.stop = stop; } [Symbol.iterator]() { return this; } next() { var value = this.value; if (value < this.stop) { this.value++; return {done: false, value: value}; } return {done: true, value: undefined}; } } function range(start, stop) { return new RangeIterator(start, stop); } for (var value of range(0, 3)) { console.log(value); // 0, 1, 2 }
调用 Iterator 接口的场合
(1)解构赋值,对数组和 Set 结构进行解构赋值时,会默认调用
Symbol.iterator
方法(2)扩展运算符, 扩展运算符(...)也会调用默认的 Iterator 接口
(3)yield*,
yield*
后面跟的是一个可遍历的结构,它会调用该结构的遍历器接口。给人直观的就是next逐一取yield的值。let generator = function* () { yield 1; yield* [2,3,4]; yield 5; }; var iterator = generator(); iterator.next() // { value: 1, done: false } iterator.next() // { value: 2, done: false } iterator.next() // { value: 3, done: false } iterator.next() // { value: 4, done: false } iterator.next() // { value: 5, done: false }
Iterator 接口与 Generator 函数
let myIterable = { [Symbol.iterator]: function* () { yield 1; yield 2; yield 3; } }; [...myIterable] // [1, 2, 3] // 或者采用下面的简洁写法 let obj = { * [Symbol.iterator]() { yield 'hello'; yield 'world'; } }; for (let x of obj) { console.log(x); } // "hello" // "world"
上面代码中,
Symbol.iterator()
方法几乎不用部署任何代码,只要用 yield 命令给出每一步的返回值即可。
Generator函数
Generator 函数是一个普通函数,但是有两个特征。一是,
function
关键字与函数名之间有一个星号;二是,函数体内部使用yield
表达式,定义不同的内部状态(yield
在英语里的意思就是“产出”)。为止。换言之,Generator 函数是分段执行的,
yield
表达式是暂停执行的标记,而next
方法可以恢复执行function* helloWorldGenerator() { yield 'hello'; yield 'world'; return 'ending'; } var hw = helloWorldGenerator(); hw.next() // { value: 'hello', done: false } hw.next() // { value: 'world', done: false } hw.next() // { value: 'ending', done: true } hw.next() // { value: undefined, done: true }
yield
表达式只能用在 Generator 函数里面,用在其他地方都会报错。(function (){ yield 1; })() // SyntaxError: Unexpected number
async/await
依次读取文件,使用Generator函数的异步执行如下
const fs = require('fs'); const readFile = function (fileName) { return new Promise(function (resolve, reject) { fs.readFile(fileName, function(error, data) { if (error) return reject(error); resolve(data); }); }); }; const gen = function* () { const f1 = yield readFile('/etc/fstab'); const f2 = yield readFile('/etc/shells'); console.log(f1.toString()); console.log(f2.toString()); }; var co = require('co'); co(gen); // 自动执行Generator函数
上面代码的函数
gen
可以写成async
函数,就是下面这样。const asyncReadFile = async function () { const f1 = await readFile('/etc/fstab'); const f2 = await readFile('/etc/shells'); console.log(f1.toString()); console.log(f2.toString()); };
一比较就会发现,
async
函数就是将 Generator 函数的星号(*
)替换成async
,将yield
替换成await
,仅此而已。
async
函数对 Generator 函数的改进。(1)内置执行器。不像 Generator 函数,需要调用
next
方法,或者用co
模块,才能真正执行,得到最后结果。(2)更好的语义。
(3)更广的适用性。
async
函数的await
命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时会自动转成立即 resolved 的 Promise 对象)(4)返回值是 Promise。
async 函数的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里。
Class
构造函数的
prototype
属性,在 ES6 的“类”上面继续存在。事实上,类的所有方法都定义在类的prototype
属性上面。class Point { constructor() { // ... } toString() { // ... } toValue() { // ... } } // 等同于 Point.prototype = { constructor() {}, toString() {}, toValue() {}, };
因此,在类的实例上面调用方法,其实就是调用原型上的方法。
constructor()
方法是类的默认方法,通过new
命令生成对象实例时,自动调用该方法。一个类必须有constructor()
方法,如果没有显式定义,一个空的constructor()
方法会被默认添加。类必须使用
new
调用,否则会报错。这是它跟普通构造函数的一个主要区别,后者不用new
也可以执行。支持setter/getter, 静态方法,继承和super。
super
作为函数调用时,代表父类的构造函数,ES6 要求,子类的构造函数必须执行一次super
函数。class A {} class B extends A { constructor() { super(); } }
class Foo { static classMethod() { return 'hello'; } } class Bar extends Foo { static classMethod() { return super.classMethod() + ', too'; } } Bar.classMethod() // "hello, too"
class A { } class B extends A { } B.__proto__ === A // true B.prototype.__proto__ === A.prototype // true
Module
在 ES6 之前,社区制定了一些模块加载方案,最主要的有 CommonJS 和 AMD 两种。前者用于服务器,后者用于浏览器。ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。
// CommonJS模块 let { stat, exists, readfile } = require('fs');
上面代码的实质是整体加载
fs
模块(即加载fs
的所有方法),然后再从这个对象上面读取 3 个方法。这种加载称为“运行时加载”,因为只有运行时才能得到这个对象,导致完全没办法在编译时做“静态优化”。ES6 模块不是对象,而是通过
export
命令显式指定输出的代码,再通过import
命令输入。// ES6模块 import { stat, exists, readFile } from 'fs';
上面代码的实质是从
fs
模块加载 3 个方法,其他方法不加载。这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。当然,这也导致了没法引用 ES6 模块本身,因为它不是对象。ES6 的模块自动采用严格模式,不管你有没有在模块头部加上
"use strict";
。对于export我们常用:
// 整体加载,用星号(*)指定一个对象,并重命名这个整体为circle对象来承载 import * as circle from './circle'; // 默认输出 export default function () { console.log('foo'); } // modules.js function add(x, y) { return x * y; } export {add as default}; // 等同于 // export default add; // app.js import { default as foo } from 'modules'; // 等同于 // import foo from 'modules'; // 接口改名和整体输出 export { foo as myFoo } from 'my_module'; // 假设有一个circleplus模块,继承了circle模块。 // 模块之间也可以继承, 假设有一个circleplus模块,继承了circle模块。 // circleplus.js export * from 'circle'; export var e = 2.71828182846; export default function(x) { return Math.exp(x); }
对于import,我们常用如下:
如果想为输入的变量重新取一个名字,
import
命令要使用as
关键字,将输入的变量重命名。import { lastName as surname } from './profile.js';
import
后面的from
指定模块文件的位置,可以是相对路径,也可以是绝对路径。如果不带有路径,只是一个模块名,那么必须有配置文件,告诉 JavaScript 引擎该模块的位置。import { myMethod } from 'util';
注意,
import
命令具有提升效果,会提升到整个模块的头部,首先执行。foo(); import { foo } from 'my_module';
上面的代码不会报错,因为
import
的执行早于foo
的调用。这种行为的本质是,import
命令是编译阶段执行的,在代码运行之前。由于
import
是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构。// 报错 import { 'f' + 'oo' } from 'my_module'; // 报错 let module = 'my_module'; import { foo } from module; // 报错 if (x === 1) { import { foo } from 'module1'; } else { import { foo } from 'module2'; }
上面三种写法都会报错,因为它们用到了表达式、变量和
if
结构。在静态分析阶段,这些语法都是没法得到值的。浏览器加载 ES6 模块,也使用
<script>
标签,但是要加入type="module"
属性。defer
是“渲染完再执行”,async
是“下载完就执行”。defer和async都是异步加载js,async
一旦下载完,渲染引擎就会中断渲染,执行这个脚本以后,再继续渲染。<script type="module" src="./foo.js"></script> <!-- 等同于 --> <script type="module" src="./foo.js" defer></script> // ES6 模块也允许内嵌在网页中,语法行为与加载外部脚本完全一致。 <script type="module"> import utils from "./utils.js"; // other code </script>
Node.js v13.2 版本开始,Node.js 已经默认打开了 ES6 模块支持。node加载.mjs文件总是以 ES6 模块加载,.cjs文件总是以 CommonJS 模块加载,.js文件的加载取决于package.json里面type字段的设置