ES6學習筆記4-Proxy、Reflect、Decorator、Module

Proxy

Proxy 這個詞的原意是代理,用在這裏表示由它來「代理」某些操做,能夠譯爲「代理器」,即用本身的定義覆蓋了語言的原始定義。ES6 原生提供 Proxy 構造函數,用來生成 Proxy 實例。node

var proxy = new Proxy(target, handler);

上面代碼中的new Proxy()表示生成一個Proxy實例,target參數表示所要攔截的目標對象,handler參數也是一個對象,用來定製攔截行爲。es6

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進行操做。express

要使得Proxy起做用,必須針對Proxy實例(上例是proxy對象)進行操做,而不是針對目標對象(上例是空對象)進行操做。json

若是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),返回一個布爾值。

若是目標對象是函數,那麼還有兩種額外操做能夠攔截。app

  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實例,就會拋出一個錯誤。async

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 入門

相關文章
相關標籤/搜索