Javascript

JS的继承方式

原型链继承

父类的实例作为子类的原型

// 定义一个动物类
function Animal (name) {
  // 属性
  this.name = name || 'Animal';
  // 实例方法
  this.sleep = function(){
    console.log(this.name + '正在睡觉!');
  }
}
// 原型方法
Animal.prototype.eat = function(food) {
  console.log(this.name + '正在吃:' + food);
};

function Cat(){ 
}
Cat.prototype = new Animal();
Cat.prototype.name = 'cat';

var cat = new Cat();
console.log(cat.name); // cat
cat.eat('fish') // cat正在吃:fish
cat.sleep() // cat正在睡觉!
console.log(cat instanceof Animal); //true 
console.log(cat instanceof Cat); //true

优点:

  • 1、非常纯粹的继承关系,实例是子类的实例,也是父类的实例
  • 2、父类新增原型方法/属性,子类都能访问到
  • 3、简单,易于实现

缺点:

  • 1、来自原型对象的所有属性被所有实例共享
  • 2、创建子实例时,无法向父类构造函数传参
  • 3、不支持多继承
  • 4、引用值共享

构造继承

使用父类的构造器来增强子类实例,等于是复制父类的实例属性给子类

function Cat(name) {
  Animal.call(this);
  this.name = name || 'Tom';
}

var cat = new Cat();
console.log(cat.name); // Tom
cat.sleep() // Tom正在睡觉!
console.log(cat instanceof Animal); // false
console.log(cat instanceof Cat); // true

优点:

  • 1、解决了原型链继承中,子类实例共享父类引用属性的问题
  • 2、创建子类实例时,可以向父类传递参数(call方法)
  • 3、可以实现多继承(call多个父类对象)

缺点:

  • 1、实例并不是父类的实例,只是子类的实例
  • 2、只能继承父类的实例属性和方法,不能继承原型属性/方法
  • 3、无法实现函数复用,每个子类都有父类实例函数的副本,影响性能

实例继承

为父类实例添加新特性,作为子类实例返回

function Cat(name){
  var instance = new Animal();
  instance.name = name || 'Tom';
  return instance;
}

var cat = new Cat();
console.log(cat.name) // Tom
cat.sleep() // Tom正在睡觉!
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); // false

优点:

  • 1、不限制调用方式,不管是new 子类()还是子类(),返回的对象具有相同效果

缺点:

  • 1、实例是父类的实例,不是子类的实例
  • 2、不支持多继承

拷贝继承

function Cat(name){
  var animal = new Animal();
  for(var p in animal){
    Cat.prototype[p] = animal[p];
  }
  this.name = name || 'Tom';
}

var cat = new Cat();
console.log(cat.name); // Tom
cat.sleep() // Tom正在睡觉!
console.log(cat instanceof Animal); // false
console.log(cat instanceof Cat); // true

优点:

  • 1、支持多继承

缺点:

  • 1、效率低,内存占用高(因为要拷贝父类的属性)
  • 2、无法获取父类不可枚举方法(不可枚举方法,不能使用for in访问到)

组合继承

通过父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用

function Cat(name){
  Animal.call(this);
  this.name = name || 'Tom';
}
Cat.prototype = new Animal();

Cat.prototype.constructor = Cat;

var cat = new Cat();
console.log(cat.name); // Tom
cat.sleep() // Tom正在睡觉!
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); // true

优点:

  • 1、弥补了构造继承的缺陷,可以继承实例属性/方法,也可继承原型属性/方法
  • 2、既是子类的实例,也是父类的实例
  • 3、不存在引用属性共享问题
  • 4、可传参
  • 5、函数可复用

缺点:

  • 1、调用了两次父类构造函数,生成了两份实例(子类实例将子类原型上的那份屏蔽了)

寄生组合继承

通过寄生方式,砍掉父类的实例属性,这样,在调用两次父类的构造时,就不会初始化两次实例方法/属性,避免继承组合的缺点

function Cat(name) {
  Animal.call(this);
  this.name = name || 'Tom';
}
// 创建一个没有实例方法的类
var Super = function () { };
Super.prototype = Animal.prototype;
//将实例作为子类的原型
Cat.prototype = new Super();

// Test Code
var cat = new Cat();
console.log(cat.name); // Tom
cat.sleep() // Tom正在睡觉!
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); //true

装饰器

装饰器是一种函数,写成@ + 函数名

类中不同声明上的装饰器将按以下规定的顺序应用:

  1. 参数装饰器,然后依次是方法装饰器访问符装饰器,或属性装饰器应用到每个实例成员。
  2. 参数装饰器,然后依次是方法装饰器访问符装饰器,或属性装饰器应用到每个静态成员。
  3. 参数装饰器应用到构造函数。
  4. 类装饰器应用到类。

作用于于同一对象的多个装饰器

  • 首先,由上至下依次对装饰器表达式求值,得到返回的真实函数(如果有的话)
  • 而后,求值的结果会由下至上依次调用

类的装饰器

function addProp(constructor: Function) {
  constructor.prototype.job = 'fe';
}

@addProp
class P {
  job: string;
  constructor(public name: string) {}
}

let p = new P('林不渡');

console.log(p.job); // fe

类方法的装饰器

function addProps(): MethodDecorator {
  return (target, propertyKey, descriptor) => {
    console.log(target);
    console.log(propertyKey);
    console.log(JSON.stringify(descriptor));

    descriptor.writable = false;
  };
}

class A {
  @addProps()
  originMethod() {
    console.log("I'm Original!");
  }
}

const a = new A();

a.originMethod = () => {
  console.log("I'm Changed!");
};

a.originMethod(); // I'm Original! 并没有被修改

类属性的装饰器

function addProps(): PropertyDecorator {
  return (target, propertyKey) => {
    console.log(target);
    console.log(propertyKey);
  };
}

class A {
  @addProps()
  originProps: any;
}   

参数的装饰器

function paramDeco(params?: any): ParameterDecorator {
  return (target, propertyKey, index) => {
    console.log(target);
    console.log(propertyKey);
    console.log(index);
    target.constructor.prototype.fromParamDeco = '呀呼!';
  };
}

class B {
  someMethod(@paramDeco() param1: any, @paramDeco() param2: any) {
    console.log(`${param1}  ${param2}`);
  }
}

new B().someMethod('啊哈', '林不渡!');
// @ts-ignore
console.log(B.prototype.fromParamDeco);

Iterator 和 for…of 循环

遍历器(Iterator)就是这样一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。

原生具备 Iterator 接口的数据结构如下

  • Array
  • Map
  • Set
  • String
  • TypedArray(类型化数组
  • 函数的 arguments 对象
  • NodeList 对象

遍历过程

(1)创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。

(2)第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员。

(3)第二次调用指针对象的next方法,指针就指向数据结构的第二个成员。

(4)不断调用指针对象的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};
    }
  };
}

将对象变为可迭代对象

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 };
      }
    };
  }
};
let obj = {
  data: {
    a: 1,
    b: 2
  },
  [Symbol.iterator]() {
    const map = new Map()
    let keys = Object.keys(this.data)
    keys.forEach(item => {
      map.set(item, this.data[item])
    })
    let i = 0
    return {
      next: () => {
        let mapentries = [...map.entries()]
        if (i < map.size) {
          return {
            value: mapentries[i++],
            done: false
          }
        }
        return {
          value: undefined,
          done: true
        }
      }
    }
  }
}
let iter = obj[Symbol.iterator]()
console.log(iter.next());
console.log(iter.next());
console.log(iter.next());

Set

ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。

方法

  • Set.prototype.add(value):添加某个值,返回 Set 结构本身。
  • Set.prototype.delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
  • Set.prototype.has(value):返回一个布尔值,表示该值是否为Set的成员。
  • Set.prototype.clear():清除所有成员,没有返回值。

属性

  • Set.prototype.size:返回Set实例的成员总数。

遍历操作

  • Set.prototype.keys():返回键名的遍历器
  • Set.prototype.values():返回键值的遍历器
  • Set.prototype.entries():返回键值对的遍历器
  • Set.prototype.forEach():使用回调函数遍历每个成员

Set 结构没有键名,只有键值(或者说键名和键值是同一个值),所以keys方法和values方法的行为完全一致。

Set的遍历顺序就是插入顺序。这个特性有时非常有用,比如使用 Set 保存一个回调函数列表,调用时就能保证按照添加顺序调用。

WeakSet

相同点

WeakSet 结构与 Set 类似,也是不重复的值的集合。

不同点

1、WeakSet 的成员只能是对象,而不能是其他类型的值。

2、WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet 之中。

用途

WeakSet 适合临时存放一组对象,以及存放跟对象绑定的信息。只要这些对象在外部消失,它在 WeakSet 里面的引用就会自动消失。(DOM节点)

Map

ES6 提供了 Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应,是一种更完善的 Hash 结构实现。

方法

  • Map.prototype.set(key, value):set方法设置键名key对应的键值为value,然后返回整个 Map 结构。
  • 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 的所有成员。

WeakMap

相同点

WeakMap结构与Map结构类似,也是用于生成键值对的集合。

不同点

1、WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键名。

2、WeakMap的键名所指向的对象,不计入垃圾回收机制。

用途

有时我们想在某个对象上面存放一些数据,但是这会形成对于这个对象的引用。

一旦不再需要这两个对象,我们就必须手动删除这个引用,否则垃圾回收机制就不会释放e1e2占用的内存。

const e1 = document.getElementById('foo');
const e2 = document.getElementById('bar');
const arr = [
  [e1, 'foo 元素'],
  [e2, 'bar 元素'],
];
// 不需要 e1 和 e2 的时候
// 必须手动删除引用
arr [0] = null;
arr [1] = null;

WeakMap 就是为了解决这个问题而诞生的,它的键名所引用的对象都是弱引用,即垃圾回收机制不将该引用考虑在内。因此,只要所引用的对象的其他引用都被清除,垃圾回收机制就会释放该对象所占用的内存。也就是说,一旦不再需要,WeakMap 里面的键名对象和所对应的键值对会自动消失,不用手动删除引用。(DOM作键名)

注意,WeakMap 弱引用的只是键名,而不是键值。键值依然是正常引用。

Proxy

在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作

var proxy = new Proxy({}, {
  get: function(target, propKey) {
    return 35;
  }
});

proxy.time // 35
proxy.name // 35
proxy.title // 35

拦截技巧

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

var object = { proxy: new Proxy(target, handler) };

Proxy 实例也可以作为其他对象的原型对象,实现拦截

var proxy = new Proxy({}, {
  get: function(target, propKey) {
    return 35;
  }
});

let obj = Object.create(proxy);
obj.time // 35

实例方法

get

get方法用于拦截某个属性的读取操作,可以接受三个参数,依次为目标对象、属性名和 proxy 实例本身(严格地说,是操作行为所针对的对象),其中最后一个参数可选。

set

set方法用来拦截某个属性的赋值操作,可以接受四个参数,依次为目标对象、属性名、属性值和 Proxy 实例本身,其中最后一个参数可选。

apply

apply方法拦截函数的调用、callapply操作。

apply方法可以接受三个参数,分别是目标对象、目标对象的上下文对象(this)和目标对象的参数数组。

has

has()方法用来拦截HasProperty操作,即判断对象是否具有某个属性时,这个方法会生效。典型的操作就是in运算符。

has()方法可以接受两个参数,分别是目标对象、需查询的属性名。

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

construct()方法返回的必须是一个对象,否则会报错。

construct()方法用于拦截new命令,construct()方法可以接受三个参数。

  • target:目标对象。
  • args:构造函数的参数数组。
  • newTarget:创造实例对象时,new命令作用的构造函数(下面例子的p
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

取消访问

Proxy.revocable()方法返回一个可取消的 Proxy 实例。

let target = {};
let handler = {};

let {proxy, revoke} = Proxy.revocable(target, handler);

proxy.foo = 123;
proxy.foo // 123

revoke();
proxy.foo // TypeError: Revoked

Proxy.revocable()方法返回一个对象,该对象的proxy属性是Proxy实例,revoke属性是一个函数,可以取消Proxy实例。上面代码中,当执行revoke函数之后,再访问Proxy实例,就会抛出一个错误。

Proxy.revocable()的一个使用场景是,目标对象不允许直接访问,必须通过代理访问,一旦访问结束,就收回代理权,不允许再次访问。

Reflect

Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。这些方法与proxy handlers (en-US)的方法相同。Reflect不是一个函数对象,因此它是不可构造的。

特点

1、将Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上

2、增加了对Object方法错误的处理

// 老写法
try {
  Object.defineProperty(target, property, attributes);
  // success
} catch (e) {
  // failure
}

// 新写法
if (Reflect.defineProperty(target, property, attributes)) {
  // success
} else {
  // failure
}

3、让Object操作都变成函数行为。

// 老写法
'assign' in Object // true

// 新写法
Reflect.has(Object, 'assign') // true

4、Reflect对象的方法与Proxy对象的方法一一对应,只要是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;
  }
});

观察者模式

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

静态方法

Promise.all

const p = Promise.all([p1, p2, p3]);

p的状态由p1p2p3决定,分成两种情况。

(1)只有p1p2p3的状态都变成fulfilledp的状态才会变成fulfilled,此时p1p2p3的返回值组成一个数组,传递给p的回调函数。

(2)只要p1p2p3之中有一个被rejectedp的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。

注意,如果作为参数的 Promise 实例,自己定义了catch方法,那么它一旦被rejected,并不会触发Promise.all()catch方法。

Promise.allSettled

Promise.allSettled()方法接受一个数组作为参数,数组的每个成员都是一个 Promise 对象,并返回一个新的 Promise 对象。只有等到参数数组的所有 Promise 对象都发生状态变更(不管是fulfilled还是rejected),返回的 Promise 对象才会发生状态变更。

该方法返回的新的 Promise 实例,一旦发生状态变更,状态总是fulfilled,不会变成rejected。状态变成fulfilled后,它的回调函数会接收到一个数组作为参数,该数组的每个成员对应前面数组的每个 Promise 对象。

results的每个成员是一个对象,对象的格式是固定的,对应异步操作的结果。

// 异步操作成功时
{status: 'fulfilled', value: value}

// 异步操作失败时
{status: 'rejected', reason: reason}

Promise.any

只要参数实例有一个变成fulfilled状态,包装实例就会变成fulfilled状态;如果所有参数实例都变成rejected状态,包装实例就会变成rejected状态。

Promise.resolve

1、如果参数是 Promise 实例,那么Promise.resolve将不做任何修改、原封不动地返回这个实例。

2、thenable对象,指的是具有then方法的对象,比如下面这个对象。

let thenable = {
  then: function(resolve, reject) {
    resolve(42);
  }
};

Promise.resolve()方法会将这个对象转为 Promise 对象,然后就立即执行thenable对象的then()方法。

let thenable = {
  then: function(resolve, reject) {
    resolve(42);
  }
};

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

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

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

Promise.reject

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

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

Promise.try

不知道或者不想区分,函数f是同步函数还是异步操作,但是想用 Promise 来处理它。因为这样就可以不管f是否包含异步操作,都用then方法指定下一步流程,用catch方法处理f抛出的错误

const f = () => console.log('now');
Promise.try(f);
console.log('next');
// now
// next
database.users.get({id: userId})
.then(...)
.catch(...)

但是database.users.get()可能还会抛出同步错误(比如数据库连接错误,具体要看实现方法),这时你就不得不用try...catch去捕获。

try {
  database.users.get({id: userId})
  .then(...)
  .catch(...)
} catch (e) {
  // ...
}

上面这样的写法就很笨拙了,这时就可以统一用promise.catch()捕获所有同步和异步的错误。

Promise.try(() => database.users.get({id: userId}))
  .then(...)
  .catch(...)

事实上,Promise.try就是模拟try代码块,就像promise.catch模拟的是catch代码块。

Generator

Generator 函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号。不同的是,调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象

必须调用遍历器对象的next方法,使得指针移向下一个状态。也就是说,每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式(或return语句)为止。

yield

(1)遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。

(2)下一次调用next方法时,再继续往下执行,直到遇到下一个yield表达式。

(3)如果没有再遇到新的yield表达式,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值。

(4)如果该函数没有return语句,则返回的对象的value属性值为undefined

next

yield表达式本身没有返回值,或者说总是返回undefined

如果传参,本次next方法所传的参数,就是上一个yield表达式返回的值

实现对象的可迭代

let obj = {
  data: {
    a: 1,
    b: 2
  },
  [Symbol.iterator]: function*() {
    const map = new Map()
    let keys = Object.keys(this.data)
    keys.forEach(item => {
      map.set(item, this.data[item])
    })
    let i = 0
    while (i < map.size) {
      yield mapentries[i++]
    }
  }
}
let iter = obj[Symbol.iterator]()
console.log(iter.next());
console.log(iter.next());
console.log(iter.next());
var myIterable = {};
myIterable[Symbol.iterator] = function* () {
  yield 1;
  yield 2;
  yield 3;
};

[...myIterable] // [1, 2, 3]

async函数

语法

返回值是一个Promise实例

async function f() {
  return 'hello world';
}

f().then(v => console.log(v))
// "hello world"

状态变化等到内部所有await命令后面的 Promise 对象执行完

async function getTitle(url) {
  let response = await fetch(url);
  let html = await response.text();
  return html.match(/<title>([\s\S]+)<\/title>/i)[1];
}
getTitle('https://tc39.github.io/ecma262/').then(console.log)
// "ECMAScript 2017 Language Specification"

await

await命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。

任何一个await语句后面的 Promise 对象变为reject状态,那么整个async函数都会中断执行。

async function f() {
  // 等同于
  // return 123;
  return await 123;
}

f().then(v => console.log(v))
function sleep(interval) {
  return new Promise(resolve => {
    setTimeout(resolve, interval);
  })
}

// 用法
async function one2FiveInAsync() {
  for(let i = 1; i <= 5; i++) {
    console.log(i);
    await sleep(1000);
  }
}

one2FiveInAsync();

错误处理

希望即使前一个异步操作失败,也不要中断后面的异步操作

async function f() {
  try {
    await Promise.reject('出错了');
  } catch(e) {
  }
  return await Promise.resolve('hello world');
}

f()
.then(v => console.log(v))
// hello world
async function f() {
  await Promise.reject('出错了')
    .catch(e => console.log(e));
  return await Promise.resolve('hello world');
}

f()
.then(v => console.log(v))
// 出错了
// hello world

代码风格建议

1、await命令后面的Promise对象,运行结果可能是rejected,所以最好把await命令放在try...catch代码块中。

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

// 写法一
let [foo, bar] = await Promise.all([getFoo(), getBar()]);

// 写法二
let fooPromise = getFoo();
let barPromise = getBar();
let foo = await fooPromise;
let bar = await barPromise;

顶层await

解决模块异步加载的问题。

Past

导入一个异步模块,必须要等到异步操作执行完,才能确保模块的正确导入

awaiting.js除了输出output,还默认输出一个 Promise 对象(async 函数立即执行后,返回一个 Promise 对象),从这个对象判断异步操作是否结束。

// awaiting.js
let output;
export default (async function main() {
  const dynamic = await import(someMission);
  const data = await fetch(url);
  output = someProcess(dynamic.default, data);
})();
export { output };
// usage.js
import promise, { output } from "./awaiting.js";

function outputPlusValue(value) { return output + value }

promise.then(() => {
  console.log(outputPlusValue(100));
  setTimeout(() => console.log(outputPlusValue(100)), 1000);
});

Latest

顶层的await命令,就是为了解决这个问题。它保证只有异步操作完成,模块才会输出值

注意,顶层await只能用在 ES6 模块,不能用在 CommonJS 模块。这是因为 CommonJS 模块的require()是同步加载,如果有顶层await,就没法处理加载了。

// awaiting.js
const dynamic = import(someMission);
const data = fetch(url);
export const output = someProcess((await dynamic).default, await data);
// usage.js
import { output } from "./awaiting.js";
function outputPlusValue(value) { return output + value }

console.log(outputPlusValue(100));
setTimeout(() => console.log(outputPlusValue(100)), 1000);