ES6进修笔记4-Proxy、Reflect、Decorator、Module

Proxy

Proxy 这个词的原意是代办,用在这里示意由它来“代办”某些操纵,可以译为“代办器”,即用本身的定义覆蓋了言语的原始定义。ES6 原生供应 Proxy 组织函数,用来天生 Proxy 实例。

var proxy = new Proxy(target, handler);

上面代码中的new Proxy()示意天生一个Proxy实例,target参数示意所要阻拦的目标对象,handler参数也是一个对象,用来定制阻拦行动。

var a={};
var proxy = new Proxy(a, {
  get: function(target, property) {
    return 35;
  }
});
proxy.tt="1";
proxy.tt    //35
a.tt    //"1"

上面代码中,关于proxy来讲,与a对象除了get要领不一样,其他悉数一样。且对proxy 的操纵就相当于对a举行操纵。

要使得Proxy起作用,必需针对Proxy实例(上例是proxy对象)举行操纵,而不是针对目标对象(上例是空对象)举行操纵。

假如handler没有设置任何阻拦,那就等同于直接通向原对象。

var target = {};
var handler = {};
var proxy = new Proxy(target, handler);
proxy.a = 'b';
target.a // "b"

上面代码中,handler是一个空对象,没有任何阻拦效果,接见proxy就等同于接见target。

Proxy 支撑的阻拦操纵

关于可以设置、但没有设置阻拦的操纵,则直接落在目标对象上,依据原本的体式格局发作效果。

  1. get(target, propKey, receiver):阻拦对象属性的读取,比方proxy.foo和proxy[‘foo’]。末了一个参数receiver是一个对象,可选。
  2. set(target, propKey, value, receiver):阻拦对象属性的设置,比方proxy.foo = v或proxy[‘foo’] = v,返回一个布尔值。
  3. has(target, propKey):阻拦propKey in proxy的操纵,返回一个布尔值。
  4. deleteProperty(target, propKey):阻拦delete proxy[propKey]的操纵,返回一个布尔值。
  5. ownKeys(target):阻拦Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy),返回一个数组。该要领返回目标对象一切本身的属性的属性名,而Object.keys()的返回效果仅包含目标对象本身的可遍历属性。
  6. getOwnPropertyDescriptor(target, propKey):阻拦Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的形貌对象。
  7. defineProperty(target, propKey, propDesc):阻拦Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一个布尔值。
  8. preventExtensions(target):阻拦Object.preventExtensions(proxy),返回一个布尔值。
  9. getPrototypeOf(target):阻拦Object.getPrototypeOf(proxy),返回一个对象。
  10. isExtensible(target):阻拦Object.isExtensible(proxy),返回一个布尔值。
  11. setPrototypeOf(target, proto):阻拦Object.setPrototypeOf(proxy, proto),返回一个布尔值。

假如目标对象是函数,那末另有两种分外操纵可以阻拦。

  1. apply(target, object, args):阻拦 Proxy 实例作为函数的挪用、call和apply操纵,比方proxy(…args)、proxy.call(object, …args)、proxy.apply(…)。
  2. construct(target, args):阻拦 Proxy 实例作为组织函数挪用的操纵,比方new proxy(…args)。

Proxy.revocable()

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的一个运用场景是,目标对象不许可直接接见,必需经由过程代办接见,一旦接见终了,就收回代办权,不许可再次接见。

this 题目

虽然 Proxy 可以代办针对目标对象的接见,但它不是目标对象的通明代办,即不做任何阻拦的状况下,也没法保证与目标对象的行动一致。主要原因就是在 Proxy 代办的状况下,目标对象内部的this关键字会指向 Proxy 代办。

const target = {
  m: function () {
    console.log(this === proxy);
  }
};
const handler = {};

const proxy = new Proxy(target, handler);

target.m() // false
proxy.m()  // true

上面代码中,一旦proxy代办,后者内部的this就是指向proxy,而不是target。

别的,有些原生对象的内部属性,只需经由过程准确的this才拿到,所以 Proxy 也没法代办这些原生对象的属性。

const target = new Date();
const handler = {};
const proxy = new Proxy(target, handler);

proxy.getDate();
// TypeError: this is not a Date object.

上面代码中,getDate要领只能在Date对象实例上面拿到,假如this不是Date对象实例就会报错。这时刻,this绑定原始对象,就可以处置惩罚这个题目。

const target = new Date('2015-01-01');
const handler = {
  get(target, prop) {
    if (prop === 'getDate') {
      return target.getDate.bind(target);
    }
    return Reflect.get(target, prop);
  }
};
const proxy = new Proxy(target, handler);

proxy.getDate() //1

Reflect

Reflect对象的设想目标有以下几个:

  1. 将Object对象的一些显著属于言语内部的要领(比方Object.defineProperty),放到Reflect对象上。现阶段,某些要领同时在Object和Reflect对象上布置,将来的新要领将只布置在Reflect对象上。也就是说,从Reflect对象上可以拿到言语内部的要领。
  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)让它们变成了函数行动。
  4. Reflect对象的要领与Proxy对象的要领一一对应,只如果Proxy对象的要领,就可以在Reflect对象上找到对应的要领。

静态要领

Reflect对象一共有13个静态要领。

  1. Reflect.apply(target,thisArg,args):Reflect.apply要领等同于Function.prototype.apply.call(func, thisArg, args),用于绑定this对象后实行给定函数。
  2. Reflect.construct(target,args):Reflect.construct要领等同于new target(…args),这供应了一种不运用new,来挪用组织函数的要领。
  3. Reflect.get(target,name,receiver):Reflect.get要领查找并返回target对象的name属性,假如没有该属性,则返回undefined。
  4. Reflect.set(target,name,value,receiver):Reflect.set要领设置target对象的name属性即是value。
  5. Reflect.defineProperty(target,name,desc):Reflect.defineProperty要领基础等同于Object.defineProperty,用来为对象定义属性。将来,后者会被逐步取销,请从如今最先就运用Reflect.defineProperty替换它。
  6. Reflect.deleteProperty(target,name):Reflect.deleteProperty要领等同于delete obj[name],用于删除对象的属性。
  7. Reflect.has(target,name):Reflect.has要领对应name in obj内里的in运算符。
  8. Reflect.ownKeys(target):Reflect.ownKeys要领用于返回对象的一切属性,基础等同于Object.getOwnPropertyNames与Object.getOwnPropertySymbols之和。
  9. Reflect.isExtensible(target):Reflect.isExtensible要领对应Object.isExtensible,返回一个布尔值,示意当前对象是不是可扩大。
  10. Reflect.preventExtensions(target):Reflect.preventExtensions对应Object.preventExtensions要领,用于让一个对象变成不可扩大。它返回一个布尔值,示意是不是操纵胜利。
  11. Reflect.getOwnPropertyDescriptor(target, name):Reflect.getOwnPropertyDescriptor基础等同于Object.getOwnPropertyDescriptor,用于取得指定属性的形貌对象,将来会替换掉后者。
  12. Reflect.getPrototypeOf(target):Reflect.getPrototypeOf要领用于读取对象的__proto__属性,对应Object.getPrototypeOf(obj)。
  13. Reflect.setPrototypeOf(target, prototype):Reflect.setPrototypeOf要领用于设置对象的__proto__属性,返回第一个参数对象,对应Object.setPrototypeOf(obj, newProto)。

Decorator

类和要领的润饰

润饰器(Decorator)是一个函数,用来修正类的行动。这是 ES 的一个提案,如今 Babel 转码器已支撑。
下面的@decorator就是一个润饰器。

@decorator
class A {}
// 等同于
class A {}
A = decorator(A) || A;


@testable
class MyTestableClass {
  // ...
}
function testable(target) {
  target.isTestable = true;
}
MyTestableClass.isTestable // true
//@testable就是一个润饰器。它修正了MyTestableClass这个类的行动,为它加上了静态属性isTestable。target指会被润饰的类。

注重,润饰器对类的行动的转变,是代码编译时发作的,而不是在运转时。这意味着,润饰器能在编译阶段运转代码。也就是说,润饰器本质就是编译时实行的函数

润饰器函数的第一个参数,就是所要润饰的目标类。

function testable(target) {
  // ...
}

假如以为一个参数不够用,可以在润饰器表面再封装一层函数。

function testable(isTestable) {
  return function(target) {
    target.isTestable = isTestable;
  }
}

@testable(true)
class MyTestableClass {}
MyTestableClass.isTestable // true

@testable(false)
class MyClass {}
MyClass.isTestable // false

润饰器也可以润饰类的属性。

class Person {
  @readonly
  name() { return `${this.first} ${this.last}` }
}
//润饰器readonly用来润饰“类”的name要领。

润饰器函数润饰类的属性时一共可以接收三个参数,第一个参数是所要润饰的目标对象,第二个参数是所要润饰的属性名,第三个参数是该属性的形貌对象。

假如统一个要领有多个润饰器,会像剥洋葱一样,先从外到内进入,然后由内向外实行。

function dec(id){
    console.log('evaluated', id);
    return (target, property, descriptor) => console.log('executed', id);
}

class Example {
    @dec(1)
    @dec(2)
    method(){}
}
// evaluated 1
// evaluated 2
// executed 2
// executed 1

上面代码中,外层润饰器@dec(1)先进入,然则内层润饰器@dec(2)先实行。

润饰器只能用于类和类的要领,不能用于函数,因为存在函数提拔,而类是不会提拔的。

core-decorators.js

core-decorators.js是一个第三方模块,供应了几个罕见的润饰器。

  1. @autobind:autobind润饰器使得要领中的this对象,绑定原始对象。
  2. @readonly:readonly润饰器使得属性或要领不可写。
  3. @override:override润饰器搜检子类的要领,是不是准确覆蓋了父类的同名要领,假如不准确会报错。
  4. @deprecate (别号@deprecated):deprecate或deprecated润饰器在控制台显现一条正告,示意该要领将取销。
  5. @suppressWarnings:suppressWarnings润饰器抑止decorated润饰器致使的console.warn()挪用。然则,异步代码发出的挪用除外。

Mixin

在润饰器的基础上,可以完成Mixin形式。所谓Mixin形式,就是在一个对象当中混入别的一个对象的要领。

//布置一个通用剧本mixins.js,将mixin写成一个润饰器。
export function mixins(...list) {
  return function (target) {
    Object.assign(target.prototype, ...list);
  };
}


//经由过程mixins这个润饰器,完成了在MyClass类上面“混入”Foo对象的foo要领。
import { mixins } from './mixins';
const Foo = {
  foo() { console.log('foo') }
};
@mixins(Foo)
class MyClass {}
let obj = new MyClass();
obj.foo() // "foo"

Module

ES6 模块经由过程export敕令显式指定输出的代码,再经由过程import敕令输入。 ES6 模块是编译时加载,使得静态剖析成为能够。

// ES6模块
import { stat, exists, readFile } from 'fs';

上面代码的本质是从fs模块加载3个要领,其他要领不加载。这类加载称为“编译时加载”或许静态加载,即 ES6 可以在编译时就完成模块加载,效力要比 CommonJS 模块的加载体式格局高。

严厉形式

ES6 的模块自动采纳严厉形式,不论你有无在模块头部加上”use strict”;。
严厉形式主要有以下限定:

  1. 变量必需声明后再运用
  2. 函数的参数不能有同名属性,不然报错
  3. 不能运用with语句
  4. 不能对只读属性赋值,不然报错
  5. 不能运用前缀0示意八进制数,不然报错
  6. 不能删除不可删除的属性,不然报错
  7. 不能删除变量delete prop,会报错,只能删除属性delete global[prop]
  8. eval不会在它的外层作用域引入变量
  9. eval和arguments不能被从新赋值
  10. arguments不会自动反应函数参数的变化
  11. 不能运用arguments.callee
  12. 不能运用arguments.caller
  13. 制止this指向全局对象
  14. 不能运用fn.caller和fn.arguments猎取函数挪用的客栈
  15. 增加了保留字(比方protected、static和interface)

ES6 模块当中,顶层的this指向undefined,即不该该在顶层代码运用this。

export 敕令

模块功用主要由两个敕令组成:export和import。export敕令用于划定模块的对外接口,import敕令用于输入其他模块供应的功用。

一个模块就是一个自力的文件。该文件内部的一切变量,外部没法猎取。假如你愿望外部可以读取模块内部的某个变量,就必需运用export关键字输出该变量。

export的准确写法:

// 写法一
export var m = 1;

// 写法二
var m = 1;
export {m};//在export敕令背面,运用大括号指定所要输出的一组变量。应优先运用该写法。

// 写法三
var n = 1;
export {n as m};

export敕令除了输出变量,还可以输出函数或类(class)。一般状况下,export输出的变量就是原本的名字,然则可以运用as关键字重命名。

export function f() {};

function v1() { ... }
function v2() { ... }

export {
  v1 as streamV1,
  v2 as streamV2,
  v2 as streamLatestVersion
};
//代码运用as关键字,重命名了函数v1和v2的对外接口。重命名后,v2可以用差别的名字输出两次。

export语句输出的接口,与其对应的值是动态绑定关联,即经由过程该接口,可以取到模块内部及时的值。

export var foo = 'bar';
setTimeout(() => foo = 'baz', 500);

上面代码输出变量foo,值为bar,500毫秒今后变成baz。这一点与 CommonJS 范例完整差别。CommonJS 模块输出的是值的缓存,不存在动态更新。

export敕令可以出如今模块的任何位置,只需处于模块顶层就可以。假如处于块级作用域内,就会报错,import敕令也是云云。这是因为处于前提代码块当中,就没法做静态优化。

function foo() {
  export default 'bar' // SyntaxError
}
foo()
//export语句放在函数当中,效果报错。

import 敕令

运用export敕令定义了模块的对外接口今后,其他 JS 文件就可以经由过程import敕令加载这个模块。

// main.js
import {firstName, lastName, year} from './profile';

function setName(element) {
  element.textContent = firstName + ' ' + lastName;
}

上面代码的import敕令,用于加载profile.js文件,并从中输入变量。import敕令接收一对大括号,内里指定要从其他模块导入的变量名。大括号内里的变量名,必需与被导入模块(profile.js)对外接口的称号雷同。
假如想为输入的变量从新取一个名字,import敕令要运用as关键字,将输入的变量重命名。

import { lastName as surname } from './profile';

import背面的from指定模块文件的位置,可所以相对途径,也可所以相对途径,.js途径可以省略。假如只是模块名,不带有途径,那末必需有设置文件,通知 JavaScript 引擎该模块的位置。

import {myMethod} from 'util';
//util是模块文件名,因为不带有途径,必需经由过程设置,通知引擎怎样取到这个模块。

import敕令具有提拔效果,会提拔到全部模块的头部,起首实行。这是因为import敕令是编译阶段实行的,在代码运转之前。

foo();
import { foo } from 'my_module';
//该代码不会报错,因为import的实行早于foo的挪用。

因为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构造。在静态剖析阶段,这些语法都是没法取得值的。

import语句会实行所加载的模块。假如屡次反复实行统一句import语句,那末只会实行一次,而不会实行屡次。

import 'lodash';
import 'lodash';

上面代码加载了两次lodash,仅实行一次lodash,且不输入任何值。

//foo和bar在两个语句中加载,然则它们对应的是统一个my_module实例。
import { foo } from 'my_module';
import { bar } from 'my_module';

// 等同于
import { foo, bar } from 'my_module';

模块的团体加载

模块团体加载即用星号(*)指定一个对象,一切输出值都加载在这个对象上面。

下面是一个circle.js文件,它输出两个要领area和circumference。

// circle.js

export function area(radius) {
  return Math.PI * radius * radius;
}
export function circumference(radius) {
  return 2 * Math.PI * radius;
}

如今,加载这个模块。

import * as circle from './circle';

console.log('圆面积:' + circle.area(4));
console.log('圆周长:' + circle.circumference(14));

模块团体加载地点的谁人对象(上例是circle)不许可运转时转变。所以下面的写法都是不许可的。

import * as circle from './circle';

// 下面两行都是不许可的
circle.foo = 'hello';
circle.area = function () {};

export default 敕令

export default敕令,为模块指定默许输出。其他模块加载该模块时,import敕令可以为该输出指定恣意名字。export default背面不须要运用大括号,且export default敕令只能运用一次。

// export-default.js
export default function () {
  console.log('foo');
}

// import-default.js
import customName from './export-default'; //这时刻的import敕令背面,不运用大括号。
customName(); // 'foo'

export default敕令用在非匿名函数前,也是可以的。

// export-default.js
export default function foo() {
  console.log('foo');
}

// 或许写成

function foo() {
  console.log('foo');
}
export default foo;//export default背面没有运用大括号。

上面代码中,foo函数的函数名foo,在模块外部是无效的。加载的时刻,视同匿名函数加载。

默许输出和一般输出的比较:运用export default时,对应的import语句不须要运用大括号,名字可恣意;不运用export default时,对应的import语句须要运用大括号,名字须与被导入模块对外接口的称号雷同。

// 第一组
export default function crc32() { // 输出
  // ...
}
import crc32 from 'crc32'; // 输入



// 第二组
export function crc32() { // 输出
  // ...
};
import {crc32} from 'crc32'; // 输入

本质上,export default敕令是将该敕令背面的值,赋给default变量今后再输出,即输出一个叫做default的变量或要领,然后体系许可你为它取恣意名字。所以,下面的写法是有用的。

// modules.js
function add(x, y) {
  return x * y;
}
export {add as default};
// 等同于
// export default add;

// app.js
import { default as xxx } from 'modules';
// 等同于
// import xxx from 'modules';

export default敕令背面不能跟变量声明语句,因为它只是输出一个叫做default的变量。

// 准确
export var a = 1;
// 准确
var a = 1;
export default a;
// 毛病
export default var a = 1;


// 准确
export default 42; //准确是因为指定外对接口为default。
// 报错
export 42;//报错是因为没有指定对外的接口。

假如想在一条import语句中,同时输入默许要领和其他变量,可以写成下面如许。

export default function (obj) {
  // ···
}
export function each(obj, iterator, context) {
  // ···
}
export { each as forEach };//该行语句的意义是暴露出forEach接口,默许指向each接口,即forEach和each指向统一个要领。

import _, { each } from 'lodash';

export default也可以用来输出类。

// MyClass.js
export default class { ... }

// main.js
import MyClass from 'MyClass';
let o = new MyClass();

export 与 import 的复合写法

假如在一个模块当中,先输入后输出统一个模块,import语句可以与export语句写在一同。

export { foo, bar } from 'my_module';

// 等同于
import { foo, bar } from 'my_module';
export { foo, bar };

默许接口的写法以下。

export { default } from 'foo';

模块的继承

模块之间也可以继承。
假设有一个circleplus模块,继承了circle模块。

// circleplus.js

export * from 'circle';
export var e = 2.71828182846;
export default function(x) {
  return Math.exp(x);
}

上面代码中的export ,示意再输出circle模块的一切属性和要领。注重,export 敕令会疏忽circle模块的default要领。然后,上面代码又输出了自定义的e变量和默许要领。

跨模块常量

const声明的常量只在当前代码块有用。假如想设置跨模块的常量(即跨多个文件),或许说一个值要被多个模块同享,可以采纳下面的写法。

// constants.js 模块
export const A = 1;
export const B = 3;
export const C = 4;

// test1.js 模块
import * as constants from './constants';
console.log(constants.A); // 1
console.log(constants.B); // 3

// test2.js 模块
import {A, B} from './constants';
console.log(A); // 1
console.log(B); // 3

import()

import敕令会被 JavaScript 引擎静态剖析,先于模块内的其他模块实行,属于编译时加载。有一个提案,发起引入import()函数,完成动态加载,即运转时加载模块。

import(specifier)

上面代码中,import函数的参数specifier,指定所要加载的模块的位置。import敕令可以接收什么参数,import()函数就可以接收什么参数,二者区分主如果后者为动态加载。

import()返回一个 Promise 对象。import()加载模块胜利今后,这个模块会作为一个对象,看成then要领的参数。下面是一个例子。

const main = document.querySelector('main');

import(`./section-modules/${someVariable}.js`)
  .then(module => {
    module.loadPageInto(main);
  })
  .catch(err => {
    main.textContent = err.message;
  });

import()函数可以用在任何地方,不仅仅是模块,非模块的剧本也可以运用。它是运转时实行,也就是说,什么时刻运转到这一句,就会加载指定的模块。import()类似于 Node 的require要领,区分主如果前者是异步加载,后者是同步加载。

实用场所

  1. 按需加载:import()可以在须要的时刻,再加载某个模块。
  2. 前提加载:import()可以放在if代码块,依据差别的状况,加载差别的模块。
  3. 动态的模块途径:import()许可模块途径动态天生。

    import(f())
    .then(...);
    //代码中,依据函数f的返回效果,加载差别的模块。

注重点

import()加载模块胜利今后,这个模块会作为一个对象,看成then要领的参数。因而,可以运用对象解构赋值的语法,猎取输出接口。假如模块有default输出接口,可以用参数直接取得。

import('./myModule.js')
.then(myModule => {
  console.log(myModule.default);
});

假如想同时加载多个模块,可以采纳下面的写法。

Promise.all([
  import('./module1.js'),
  import('./module2.js'),
  import('./module3.js'),
])
.then(([module1, module2, module3]) => {
   ···
});

import()也可以用在 async 函数当中。

Module 的加载完成

浏览器加载

传统要领

浏览器许可剧本异步加载,下面就是两种异步加载的语法。

<script src="path/to/myModule.js" defer></script>
<script src="path/to/myModule.js" async></script>

defer与async的区分是:前者要比及全部页面一般衬着终了,才会实行;后者一旦下载完,衬着引擎就会中缀衬着,实行这个剧本今后,再继承衬着。一句话,defer是“衬着完再实行”,async是“下载完就实行”。别的,假如有多个defer剧本,会依据它们在页面涌现的递次加载,而多个async剧本是不能保证加载递次的。

加载划定规矩

浏览器加载 ES6 模块,也运用<script>标签,然则要到场type=”module”属性。

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

上面代码在网页中插进去一个模块foo.js,因为type属性设为module,所以浏览器晓得这是一个 ES6 模块。

浏览器关于带有type="module"的<script>,都是异步加载,不会形成梗塞浏览器,即比及全部页面衬着完,再实行模块剧本,等同于翻开了<script>标签的defer属性。

<script type="module" src="foo.js"></script>
<!-- 等同于 -->
<script type="module" src="foo.js" defer></script>

<script>标签的async属性也可以翻开,这时刻只需加载完成,衬着引擎就会中缀衬着马上实行。实行完成后,再恢复衬着。

<script type="module" src="foo.js" async></script>

ES6 模块也许可内嵌在网页中,语法行动与加载外部剧本完整一致。

<script type="module">
  import utils from "./utils.js";

  // other code
</script>

关于外部的模块剧本(上例是foo.js),有几点须要注重:

  • 代码是在模块作用域当中运转,而不是在全局作用域运转。模块内部的顶层变量,外部不可见。
  • 模块剧本自动采纳严厉形式,不论有无声明use strict。
  • 模块当中,可以运用import敕令加载其他模块(.js后缀不可省略,须要供应相对 URL 或相对 URL),也可以运用export敕令输出对外接口。
  • 模块当中,顶层的this关键字返回undefined,而不是指向window。也就是说,在模块顶层运用this关键字,是无意义的。
  • 统一个模块假如加载屡次,将只实行一次

ES6 模块与 CommonJS 模块的差别

有两个严重差别:

  • CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的援用。
  • CommonJS 模块是运转时加载,ES6 模块是编译时输出接口。

第二个差别是因为 CommonJS 加载的是一个对象(即module.exports属性),该对象只需在剧本运转完才会天生。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态剖析阶段就会天生。

CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。ES6 模块的运转机制与 CommonJS 不一样。JS 引擎对剧本静态剖析的时刻,碰到模块加载敕令import,就会天生一个只读援用。比及剧本真正实行时,再依据这个只读援用,到被加载的谁人模块内里去取值。因而,ES6 模块不会缓存运转效果,而是动态地去被加载的模块取值。

ES6 输入的模块变量是只读的,对它举行从新赋值会报错。

// lib.js
export let obj = {};

// main.js
import { obj } from './lib';

obj.prop = 123; // OK
obj = {}; // TypeError

上面代码中,main.js从lib.js输入变量obj,可以对obj增加属性,然则从新赋值就会报错。因为变量obj指向的地点是只读的,不能从新赋值。

export经由过程接口,输出的是统一个值。差别的剧本加载这个接口,取得的都是一样的实例。

Node 加载

Node 对 ES6 模块的处置惩罚比较贫苦,因为它有本身的 CommonJS 模块花样,与 ES6 模块花样是不兼容的。如今的处置惩罚计划是,将二者离开,ES6 模块和 CommonJS 采纳各自的加载计划。

在静态剖析阶段,一个模块剧本只需有一行import或export语句,Node 就会以为该剧本为 ES6 模块,不然就为 CommonJS 模块。假如不输出任何接口,然则愿望被 Node 以为是 ES6 模块,可以在剧本中加一行语句。

export {};//不输出任何接口的 ES6 规范写法。

假如不指定相对途径,Node 加载 ES6 模块会顺次寻觅以下剧本,与require()的划定规矩一致。

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
// 再上一级目次

ES6 模块当中,顶层的this指向undefined;CommonJS 模块的顶层this指向当前模块,这是二者的一个严重差别。

import 敕令加载 CommonJS 模块

Node 采纳 CommonJS 模块花样,模块的输出都定义在module.exports这个属性上面。在 Node 环境中,运用import敕令加载 CommonJS 模块,Node 会自动将module.exports属性,看成模块的默许输出,即等同于export default。

CommonJS 模块的输出缓存机制,在 ES6 加载体式格局下依旧有用。

因为 ES6 模块是编译时肯定输出接口,CommonJS 模块是运转时肯定输出接口,所以采纳import敕令加载 CommonJS 模块时,不许可采纳下面的写法。

import {readfile} from 'fs';

上面的写法不准确,因为fs是 CommonJS 花样,只需在运转时才肯定readfile接口,而import敕令请求编译时就肯定这个接口。处置惩罚要领就是改成团体输入。

import * as express from 'express';
const app = express.default();

import express from 'express';
const app = express();

require 敕令加载 ES6 模块

采纳require敕令加载 ES6 模块时,ES6 模块的一切输出接口,会成为输入对象的属性。

// es.js
let foo = {bar:'my-default'};
export default foo;
foo = null;

// cjs.js
const es_namespace = require('./es');
console.log(es_namespace.default);
// {bar:'my-default'}

上面代码中,default接口变成了es_namespace.default属性。别的,因为存在缓存机制,es.js对foo的从新赋值没有在模块外部反应出来。

轮回加载

“轮回加载”(circular dependency)指的是,a剧本的实行依靠b剧本,而b剧本的实行又依靠a剧本。

一般,“轮回加载”示意存在强耦合,假如处置惩罚不好,还能够致使递归加载,使得顺序没法实行,因而应当防止涌现。

CommonJS模块的加载道理

CommonJS的一个模块,就是一个剧本文件。require敕令第一次加载该剧本,就会实行全部剧本,然后在内存天生一个对象。今后须要用到这个模块的时刻,就会到exports属性上面取值。纵然再次实行require敕令,也不会再次实行该模块,而是到缓存当中取值。也就是说,CommonJS模块不管加载多少次,都只会在第一次加载时运转一次,今后再加载,就返回第一次运转的效果,除非手动消灭体系缓存。

{
  id: '...',
  exports: { ... },
  loaded: true,
  ...
}

上面代码就是Node内部加载模块后天生的一个对象。该对象的id属性是模块名,exports属性是模块输出的各个接口,loaded属性是一个布尔值,示意该模块的剧本是不是实行终了。其他另有许多属性。

CommonJS模块的轮回加载

CommonJS模块的主要特征是加载时实行,即剧本代码在require的时刻,就会悉数实行。一旦涌现某个模块被”轮回加载”,就只输出已实行的部份,还未实行的部份不会输出。比方:

//剧本文件a.js代码
exports.done = false;
var b = require('./b.js');
console.log('在 a.js 当中,b.done = %j', b.done);
exports.done = true;
console.log('a.js 实行终了');

上面代码当中,a.js剧本先输出一个done变量,然后加载另一个剧本文件b.js。注重,此时a.js代码就停在这里,守候b.js实行终了,再往下实行。

//剧本文件b.js的代码
exports.done = false;
var a = require('./a.js');
console.log('在 b.js 当中,a.done = %j', a.done);
exports.done = true;
console.log('b.js 实行终了');

上面代码当中,b.js实行到第二行,就会去加载a.js,这时刻,就发作了“轮回加载”。体系会去a.js模块对应对象的exports属性取值,但是因为a.js还没有实行完,从exports属性只能取回已实行的部份,而不是末了的值。

a.js已实行的部份,只需一行。

exports.done = false;

因而,关于b.js来讲,它从a.js只输入一个变量done,值为false。

然后,b.js接着往下实行,比及悉数实行终了,再把实行权交还给a.js。因而,a.js接着往下实行,直到实行终了。

ES6模块的轮回加载

ES6模块是动态援用,假如运用import从一个模块加载变量(即import foo from ‘foo’),那些变量不会被缓存,而是成为一个指向被加载模块的援用,须要开发者本身保证,真正取值的时刻可以取到值。

// a.js以下
import {bar} from './b.js';
console.log('a.js');
console.log(bar);
export let foo = 'foo';

// b.js
import {foo} from './a.js';
console.log('b.js');
console.log(foo);
export let bar = 'bar';

//运转效果
b.js
undefined
a.js
bar

上面代码中,因为a.js的第一行是加载b.js,所以先实行的是b.js。而b.js的第一行又是加载a.js,这时刻因为a.js已最先实行了,所以不会反复实行,而是继承往下实行b.js,所以第一行输出的是b.js。

接着,b.js要打印变量foo,这时刻a.js还没实行完,取不到foo的值,致使打印出来是undefined。b.js实行完,最先实行a.js,这时刻就一切一般了。

ES6模块的转码

浏览器如今还不支撑ES6模块,为了如今就可以运用,可以将转为ES5的写法。除了Babel可以用来转码以外,另有以下两个要领,也可以用来转码。

  1. ES6 module transpiler:可以将 ES6 模块转为 CommonJS 模块或 AMD 模块的写法,从而在浏览器中运用。
  2. SystemJS:可以在浏览器内加载 ES6 模块、AMD 模块和 CommonJS 模块,将其转为 ES5 花样。

参考自:ECMAScript 6 入门

    原文作者:lijinxieyang
    原文地址: https://segmentfault.com/a/1190000009662793
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞