Module 的語法

文章編寫參考 阮一峯《ECMAScript 6 入門》


1. 概述

模塊化開發對於現在的大型的應用系統來說是必不可少的一種模式,【ES6模塊的設計思想是儘量的靜態化】使得編譯時就能確定模塊的依賴關係,以及輸入和輸出的變量。

ES6模塊不是對象,而是通過export命令顯示指定輸出的代碼,再通過import命令導入。

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

上面代碼從fs模塊中加載了3個方法,其他方法不加載。這種加載成爲“編譯時加載”或者靜態加載,即ES6可以在編譯時就完成模塊的加載。


2. 嚴格模式

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)

其中,尤其需要注意this的限制。ES6 模塊之中,頂層的this指向undefined,即不應該在頂層代碼使用this。


3. export命令

模塊的功能主要由兩個命令構成:export和import。export用於在模塊中導出接口;import命令用於輸入其他模塊提供的功能。export和import是配套使用的。

一個模塊就是一個JS文件,【模塊中所有的變量外部都是無法獲取的】。如果你希望外部能訪問到模塊內部的某個變量,就必須使用【export命令導出變量】。

//// profile.js
export var firstName = 'Michael';
export var lastName = 'Jackson';
export var year = 1958;

上面代碼是profile.js文件,保存了用戶信息。ES6 將其視爲一個模塊,裏面用export命令對外部輸出了三個變量。

export除了上面的那樣的寫法,還有另外一種更常用的方式

// profile.js
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;

export {firstName, lastName, year};

上面代碼在export命令後面,使用大括號指定所要輸出的一組變量。它與前一種寫法(直接放置在var語句前)是等價的,但是應該【優先考慮使用這種寫法】。因爲這樣就可以在腳本尾部,一眼看清楚輸出了哪些變量。

【export命令除了輸出變量,還可以輸出函數或類(class)】

//輸出函數
export function multiply(x, y) {
  return x * y;
};

//輸出類
export class Person{

}

通常情況下,export輸出的變量就是本來的名字,但是可以使用as關鍵字重命名。


function v1() {
    //.....
}
function v2() {
    //...
}
export {
    v1 as streamV1,
    v2 as streamV2,
    v2 as anotherV2
}

上面代碼使用as關鍵字,重命名了函數v1和v2的對外接口。重命名後,v2可以用不同的名字輸出兩次。

【注意】export命令規定的是對外的接口,必須與模塊內部建立一一對應關係。

//報錯
let m = 1;
export m;

//報錯
export 1;

上面的兩種寫法都會報錯,因爲沒有提供對外的接口。第一種寫法導出一個變量,變量爲1,也就是跟第二種寫法一樣,都是導出1,1是一個值,不是一個接口。

// 寫法一
export var m = 1;

// 寫法二
var m = 1;
export {m};

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

上面三種寫法都是正確的,規定了對外的接口m。其他腳本可以通過這個接口,取到值1。它們的實質是,在接口名與模塊內部變量之間,建立了一一對應的關係。

同樣的,function和class的輸出,也必須遵守這樣的寫法。

//報錯
function fun() { }
export fun;

//正確
export function fun() { }

//正確
function fun() { }
export { fun }

【注意】export輸出的接口,與其對應的值是動態綁定的,也就是說通過模塊接口可以取得模塊內部實時的值。

export let name = 'Blue';
setTimeout(function () {
    name = 'Crazy'
}, 500);

上面代碼導出的name接口,初始爲Blue,但是500毫秒之後就變成了Crazy,模塊外部的值與模塊內部的值是實時綁定的。

【export可以出現在模塊的任何位置,但是必須處於頂層作用域中】

function foo() {
  export default 'bar' // SyntaxError
}
foo()

上面代碼由於export命令出現在了函數中,所以導致代碼報錯。


4. import命令

在模塊中使用了export導出接口之後,我們使用import在其他JS文件中導入接口以加載模塊。

////module
let sayHi = () => "I am blue";

class Person {

}

let name = 'Blue';

export {sayHi,Person,name}
//導入模塊
import { sayHi, Person, name } from './model'

上面代碼中,從model模塊中導入了三個接口,分別是一個方法、一個類、和一個變量。【import大括號中的名稱必須和模塊導出的名稱一致,又別名的與別名一致】

如果我們想對輸出的接口進行重名名,那麼可以在import的大括號中進行操作

import { sayHi as sayHello, Person, name } from './model'
sayHello()  //I am blue

import後面的from指定模塊文件的位置,可以是相對路徑,也可以是絕對路徑,.js路徑可以省略。如果只是模塊名,不帶有路徑,那麼必須有配置文件,告訴 JavaScript 引擎該模塊的位置。

import {myMethod} from 'util';

上面代碼中,util是模塊文件名,由於不帶有路徑,必須通過配置,告訴引擎怎麼取到這個模塊。

【注意】import命令存在命令提升,會提升到整個代碼的頭部

sayHello()  //I am blue
import { sayHi as sayHello, Person, name } from './model'

上面的代碼不會報錯,因爲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結構。在靜態分析階段,這些語法都是沒法得到值的。

import語句會執行所加載的模塊,因此可以有下面的寫法。

import 'lodash';

上面代碼僅僅執行lodash模塊,但是不輸入任何值。

如果多次重複執行同一句import語句,那麼只會執行一次,而不會執行多次。

import 'lodash';
import 'lodash';

上面代碼加載了兩次lodash,但是隻會執行一次。

import { foo } from 'my_module';
import { bar } from 'my_module';

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

上面代碼中,雖然foo和bar在兩個語句中加載,但是它們對應的是同一個my_module實例。也就是說,import語句是 Singleton 模式。


5. 模塊的整體加載

除了指定加載某個輸出值,還可以使用整體加載,即用星號(*)指定一個對象,所有輸出值都加載在這個對象上面。

//逐一列舉導出接口
import { sayHi as sayHello, Person, name } from './model'

//整體加載
import * as exportobj from './model'
exportobj;
/*
    { sayHi: [Function: sayHi],
  Person: [Function: Person],
  name: 'Blue' }
*/

上面代碼中用*整體加載了一個模塊,在這個對象擁有所有模塊導出的接口
【注意】模塊的整體加載也是靜態的,不允許導入模塊之後再模塊外部運行時對接口進行改變。

import * as circle from './circle';

// 下面兩行都是不允許的
circle.foo = 'hello';
circle.area = function () {};

6. export default命令

從前面的例子可以看出,使用import命令的時候,用戶需要知道所要加載的變量名或函數名,否則無法加載。但是,用戶肯定希望快速上手,未必願意閱讀文檔,去了解模塊有哪些屬性和方法。

爲了給用戶提供方便,讓他們不用閱讀文檔就能加載模塊,就要用到export default命令,爲模塊指定默認輸出。

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

上面代碼是一個模塊文件export-default.js,它的默認輸出是一個函數

他模塊加載該模塊時,import命令可以爲該匿名函數指定任意名字。

//// import-default.js
import customName from './export-default';
customName(); // 'foo'

上面代碼的import命令,可以用任意名稱指向export-default.js輸出的方法,這時就不需要知道原模塊輸出的函數名。需要注意的是,這時import命令後面,不使用大括號。

export default還可以用於非匿名函數

export default function sayHi() {
  console.log('I am Blue');
}

//等同於

function sayHi() {
    console.log('I am Blue');
}
export default sayHi;

上面代碼中,函數名sayHi在模塊外部是無效的,在加載的時候視爲匿名函數。

下面比較一下默認輸出和正常輸出

//第一組
export default function crc32() { // 輸出
  // ...
}

import crc32 from 'crc32'; // 輸入

// 第二組
export function crc32() { // 輸出
  // ...
};

import {crc32} from 'crc32'; // 輸入

上面代碼的兩組寫法,第一組是使用export default時,對應的import語句不需要使用大括號;第二組是不使用export default時,對應的import語句需要使用大括號。

【一個模塊只能有一個默認輸出】所以,import命令後面纔不用加大括號,因爲只可能對應一個方法。

本質上,export 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命令,輸入模塊時就非常直觀了,以輸入 lodash 模塊爲例。

import _ from 'lodash';

如果想在一條import語句中,同時輸入默認方法和其他接口,可以寫成下面這樣。

import _, { each, each as forEach } from 'lodash';

對應上面代碼的export語句如下。

export default function (obj) {
  // ···
}

export function each(obj, iterator, context) {
  // ···
}

export { each as forEach };

上面代碼的最後一行的意思是,暴露出forEach接口,默認指向each接口,即forEach和each指向同一個方法。

export default也可以用來輸出類。

const Person = class {
    constructor(name) {
        this.name = name
    }
    sayHi() {
        console.log('I am ', this.name);
    }
}
export default Person;
import Person from './model';
let p = new Person('Blue');
p.sayHi();  //I am  Blue

7. export 與import的複合寫法

如果在一個模塊中,先輸入後輸出同一個模塊,import語句可以與export語句寫在一起。

export { Person } from './model';

//等同於

import Person from './model';
export { Person };

上面代碼中,將model模塊導入後導出,寫成了一個export和import的複合寫法。

【改名輸出】

export { Person as People } from './model';

上面代碼使用 as 將導入的接口改名後導出。

【整體輸出】

export * from './model';

【默認輸出】

export { default } from './model';

【具名接口改成默認接口輸出】

export { Person as default } from './model';

【默認接口改成具名接口輸出】

export { default as Person } from './model';

8. 模塊的繼承

模塊之間產生繼承給我的感覺就是把一個模塊導入這個模塊兒,在該模塊中添加新的方法或者變量,然後導出。

假設有一個circleplus模塊,繼承了circle模塊。

// circleplus.js

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

上面代碼中的export **,表示再輸出circle模塊的所有屬性和方法。然後,上面代碼又輸出了自定義的e變量和默認方法。

【注意】export * 命令會忽略circle模塊的default方法。

這時也可以將circle模塊的屬性或者方法,改名後再輸出。

// circleplus.js

export { area as circleArea } from 'circle';

上面代碼表示,只輸出circle模塊的area方法,且將其改名爲circleArea。

加載上面模塊的寫法如下。

// main.js

import * as math from 'circleplus';
import exp from 'circleplus';
console.log(exp(math.e));

上面代碼中的import exp表示,將circleplus模塊的默認方法加載爲exp方法。


9. 跨模塊常量

介紹const命令的時候說過,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

如果要使用的常量非常多,可以建一個專門的constants目錄,將各種常量寫在不同的文件裏面,保存在該目錄下。

// constants/db.js
export const db = {
  url: 'http://my.couchdbserver.local:5984',
  admin_username: 'admin',
  admin_password: 'admin password'
};

// constants/user.js
export const users = ['root', 'admin', 'staff', 'ceo', 'chief', 'moderator'];

然後,將這些文件輸出的常量,合併在index.js裏面。

// constants/index.js
export {db} from './db';
export {users} from './users';

使用的時候,直接加載index.js就可以了。

// script.js
import {db, users} from './constants';

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章