Created By JishuBao on 2019-03-20 12:38:22
Recently revised in 2019-03-20 12:38:22javascript
歡迎你們來到技術寶的掘金世界,您的star是我寫文章最大的動力!GitHub地址 css
文章簡介:前端
一、前端模塊化發展簡述java
二、模塊的演變node
三、CommonJs概述jquery
四、AMD概述git
五、CMD概述es6
六、AMD與CMD的區別github
七、嚴格模式web
八、ES6的Module
九、Moudle的加載實現
十、module.exports與exports、export、export default、exports.default區別
歷史上,JavaScript一直沒有模塊(module)體系,沒法將一個大程序拆分紅互相依賴的小文件,再用簡單的方法拼接起來。其餘語言都有這項功能,好比Ruby的require、Python的import,甚至就連css都有@import,可是JavaScript任何這方面的支持都沒有,這對開發大型的、複雜的項目造成了巨大障礙。
在JavaScript發展初期就是爲了實現簡單的頁面交互邏輯,寥寥數語便可;現在CPU、瀏覽器性能獲得了極大的提高,不少頁面邏輯遷移到了客戶端(表單驗證等),隨着web2.0時代的到來,Ajax技術獲得普遍應用,jQuery等前端庫層出不窮,前端代碼日益膨脹。
這時候JavaScript做爲嵌入式的腳本語言的定位動搖了,JavaScript卻沒有爲組織代碼提供任何明顯幫助,甚至沒有類的概念,更不用說模塊module了,JavaScript極其簡單的代碼組織規範不足以駕馭如此龐大規模的代碼。
既然JavaScript不能handle如此大規模的代碼,咱們能夠借鑑一下其它語言是怎麼處理大規模程序設計的,在Java中有一個重要帶概念——package,邏輯上相關的代碼組織到同一個包內,包內是一個相對獨立的王國,不用擔憂命名衝突什麼的,那麼外部若是使用呢?直接import對應的package便可
import java.util.ArrayList;
複製代碼
遺憾的是JavaScript在設計時定位緣由,沒有提供相似的功能,開發者須要模擬出相似的功能,來隔離、組織複雜的JavaScript代碼,咱們稱爲模塊化。
一個模塊就是實現特定功能的文件,有了模塊,咱們就能夠更方便地使用別人的代碼,想要什麼功能,就加載什麼模塊。模塊開發須要遵循必定的規範,各行其是就都亂套了。
規範造成的過程是痛苦的,前端的先驅在刀耕火種、茹毛飲血的階段開始,發展到如今初具規模,簡單瞭解一下這段不凡的歷程:
咱們在講函數的時候提到,函數一個功能就是實現特定邏輯的一組語句打包,並且JavaScript的做用域就是基於函數的,因此把函數做爲模塊化的第一步是很天然的事情,在一個文件裏面編寫幾個相關函數就是最開始的模塊了。
function fn1(){
statement
......
}
function fn2(){
statement
......
}
複製代碼
這樣在須要的之後夾在函數所在文件,調用函數就能夠了。
這種作法的缺點很明顯:污染了全局變量,沒法保證不與其餘模塊發生變量名衝突,並且模塊成員之間沒什麼關係。
爲了解決上面問題,對象的寫法應運而生,能夠把全部的模塊成員封裝在一個對象中。
var myModule = {
var1: 1,
var2: 2,
fn1: function(){
},
fn2: function(){
}
}
複製代碼
這樣咱們在但願調用模塊的時候引用對應文件,而後
myModule.fn2();
複製代碼
這樣避免了變量污染,只要保證模塊名惟一便可,同時同一模塊內的成員也有了關係
看似不錯的解決方案,可是也有缺陷,外部能夠隨意修改內部成員
myModel.var1 = 100;
複製代碼
這樣就會產生意外的安全問題
能夠經過當即執行函數,來達到隱藏細節的目的。
var myModule = (function(){
var var1 = 1;
var var2 = 2;
function fn1(){
}
function fn2(){
}
return {
fn1: fn1,
fn2: fn2
};
})();
複製代碼
這樣在模塊外部沒法修改咱們沒有暴露出來的變量、函數
上述作法就是咱們模塊化的基礎,目前,通行的JavaScript模塊規範主要有兩種:CommonJS和AMD。
咱們先從CommonJS談起,由於在網頁端沒有模塊化編程只是頁面JavaScript邏輯複雜,但也能夠工做下去,在服務器端卻必定要有模塊,因此雖然JavaScript在web端發展這麼多年,第一個流行的模塊化規範卻由服務器端的JavaScript應用帶來,CommonJS規範是由NodeJS發揚光大,這標誌着JavaScript模塊化編程正式登上舞臺。
//模塊定義 myModel.js
var name = 'Byron';
function printName(){
console.log(name);
}
function printFullName(firstName){
console.log(firstName + name);
}
module.exports = {
printName: printName,
printFullName: printFullName
}
//加載模塊
var nameModule = require('./myModel.js');
nameModule.printName();
複製代碼
不一樣的實現對require時的路徑有不一樣要求,通常狀況能夠省略js拓展名,能夠使用相對路徑,也能夠使用絕對路徑,甚至能夠省略路徑直接使用模塊名(前提是該模塊是系統內置模塊)
仔細看上面的代碼,會發現require是同步的。模塊系統須要同步讀取模塊文件內容,並編譯執行以獲得模塊接口。
這在服務器端實現很簡單,也很天然,然而, 想在瀏覽器端實現問題卻不少。
瀏覽器端,加載JavaScript最佳、最容易的方式是在document中插入script 標籤。但腳本標籤天生異步,傳統CommonJS模塊在瀏覽器環境中沒法正常加載。
解決思路之一是,開發一個服務器端組件,對模塊代碼做靜態分析,將模塊與它的依賴列表一塊兒返回給瀏覽器端。 這很好使,但須要服務器安裝額外的組件,並所以要調整一系列底層架構。
另外一種解決思路是,用一套標準模板來封裝模塊定義,可是對於模塊應該怎麼定義和怎麼加載,又產生的分歧。
AMD 即Asynchronous Module Definition,中文名是異步模塊定義的意思。它是一個在瀏覽器端模塊化開發的規範
因爲不是JavaScript原生支持,使用AMD規範進行頁面開發須要用到對應的庫函數,也就是大名鼎鼎RequireJS,實際上AMD 是 RequireJS 在推廣過程當中對模塊定義的規範化的產出。
requireJS主要解決兩個問題
// 定義模塊 myModule.js
define(['myModule'], function(){
var name = 'Byron';
function printName(){
console.log(name);
}
return {
printName: printName
};
});
// 加載模塊
require(['myModule'], function (my){
my.printName();
});
複製代碼
requireJS定義了一個函數 define,它是全局變量,用來定義模塊。
define(id?, dependencies?, factory);
id:可選參數,用來定義模塊的標識,若是沒有提供該參數,腳本文件名(去掉拓展名)
dependencies:是一個當前模塊依賴的模塊名稱數組
factory:工廠方法,模塊初始化要執行的函數或對象。若是爲函數,它應該只被執行一次。若是是對象,此對象應該爲模塊的輸出值
在頁面上使用require函數加載模塊。
require([dependencies], function(){});
require()函數接受兩個參數
require()函數在加載依賴的函數的時候是異步加載的,這樣瀏覽器不會失去響應,它指定的回調函數,只有前面的模塊都加載成功後,纔會運行,解決了依賴性的問題。
CMD 即Common Module Definition通用模塊定義,CMD規範是國內發展出來的,就像AMD有個requireJS,CMD有個瀏覽器的實現SeaJS,SeaJS要解決的問題和requireJS同樣,只不過在模塊定義方式和模塊加載(能夠說運行、解析)時機上有所不一樣。
Sea.js 推崇一個模塊一個文件,遵循統一的寫法
define(id?, deps?, factory)
由於CMD推崇
factory有三個參數
function(require, exports, module) 複製代碼
require
require 是 factory 函數的第一個參數
require(id)
require 是一個方法,接受 模塊標識 做爲惟一參數,用來獲取其餘模塊提供的接口
exports
exports 是一個對象,用來向外提供模塊接口
module
module 是一個對象,上面存儲了與當前模塊相關聯的一些屬性和方法
// 定義模塊 myModule.js
define(function(require, exports, module) {
var $ = require('jquery.js')
$('div').addClass('active');
});
// 加載模塊
seajs.use(['myModule.js'], function(my){
});
複製代碼
最明顯的區別就是在模塊定義時對依賴的處理不一樣
這種區別各有優劣,只是語法上的差距,並且requireJS和SeaJS都支持對方的寫法。
最大的區別是對依賴模塊的執行時機處理不一樣,注意不是加載的時機或者方式不一樣
不少人說requireJS是異步加載模塊,SeaJS是同步加載模塊,這麼理解其實是不許確的,其實加載模塊都是異步的,只不過AMD依賴前置,js能夠方便知道依賴模塊是誰,當即加載,而CMD就近依賴,須要使用把模塊變爲字符串解析一遍才知道依賴了那些模塊,這也是不少人詬病CMD的一點,犧牲性能來帶來開發的便利性,實際上解析模塊用的時間短到能夠忽略
爲何咱們說兩個的區別是依賴模塊執行時機不一樣,爲何不少人認爲ADM是異步的,CMD是同步的(除了名字的緣由。。。)
一樣都是異步加載模塊,AMD在加載模塊完成後就會執行改模塊,全部模塊都加載執行完後會進入require的回調函數,執行主邏輯,這樣的效果就是依賴模塊的執行順序和書寫順序不必定一致,看網絡速度,哪一個先下載下來,哪一個先執行,可是主邏輯必定在全部依賴加載完成後才執行。
CMD加載完某個依賴模塊後並不執行,只是下載而已,在全部依賴模塊加載完成後進入主邏輯,遇到require語句的時候才執行對應的模塊,這樣模塊的執行順序和書寫順序是徹底一致的。
這也是不少人說AMD用戶體驗好,由於沒有延遲,依賴模塊提早執行了,CMD性能好,由於只有用戶須要的時候才執行的緣由。
ES6 的模塊自動採用嚴格模式,無論你有沒有在模塊頭部加上"use strict",嚴格模式主要有如下限制。
其中,尤爲須要注意this的限制。ES6模塊之中,頂層的this指向undefined,即不該該在頂層代碼使用this。
模塊功能主要由兩個命令構成:export和import。export命令用於規定模塊的對外接口,import命令用於輸入其餘模塊提供的功能。
一個模塊就是一個獨立的文件。該文件內部的全部變量,外部沒法獲取。若是你但願外部可以讀取模塊內部的某個變量,就必須使用export關鍵字輸出該變量。
// profile.js
//第一種寫法
export var firstName = 'Michael';
export var lastName = 'Jackson';
export var year = 1958;
//第二種寫法
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;
export {firstName,lastName,year}
複製代碼
上面代碼是profile.js文件,保存了用戶信息。ES6 將其視爲一個模塊,裏面用export命令對外部輸出了三個變量。
它與前一種寫法(直接放置在var語句前)是等價的,可是應該優先考慮使用這種寫法。由於這樣就能夠在腳本尾部,一眼看清楚輸出了哪些變量。
export命令除了輸出變量,還能夠輸出函數或類(class)。
export function multiply(x, y) {
return x * y;
};
複製代碼
一般狀況下,export輸出的變量就是原本的名字,可是能夠使用as關鍵字重命名。
function v1() { ... }
function v2() { ... }
export {
v1 as streamV1,
v2 as streamV2,
v2 as streamLatestVersion
};
複製代碼
上面代碼使用as關鍵字,重命名了函數v1和v2的對外接口。重命名後,v2能夠用不一樣的名字輸出兩次。
須要特別注意的是,export命令規定的是對外的接口,必須與模塊內部的變量創建一一對應關係。
// 報錯
export 1;
// 報錯
var m = 1;
export m;
複製代碼
上面兩種寫法都會報錯,由於沒有提供對外的接口。第一種寫法直接輸出 1,第二種寫法經過變量m,仍是直接輸出 1。1只是一個值,不是接口。正確的寫法是下面這樣。
// 寫法一
export var m = 1;
// 寫法二
var m = 1;
export {m};
// 寫法三
var n = 1;
export {n as m};
複製代碼
上面三種寫法都是正確的,規定了對外的接口m。其餘腳本能夠經過這個接口,取到值1。它們的實質是,在接口名與模塊內部變量之間,創建了一一對應的關係。
一樣的,function和class的輸出,也必須遵照這樣的寫法。
// 報錯
function f() {}
export f;
// 正確
export function f() {};
// 正確
function f() {}
export {f};
複製代碼
另外,export語句輸出的接口,與其對應的值是動態綁定關係,即經過該接口,能夠取到模塊內部實時的值。
export var foo = 'bar';
setTimeout(() => foo = 'baz', 500);
複製代碼
上面代碼輸出變量foo,值爲bar,500 毫秒以後變成baz。
最後,export命令能夠出如今模塊的任何位置,只要處於模塊頂層就能夠。若是處於塊級做用域內,就會報錯,下一節的import命令也是如此。這是由於處於條件代碼塊之中,就無法作靜態優化了,違背了 ES6 模塊的設計初衷。
function foo() {
export default 'bar' // SyntaxError
}
foo()
複製代碼
上面代碼中,export語句放在函數之中,結果報錯。
使用export命令定義了模塊的對外接口之後,其餘JS文件就能夠經過import命令加載這個模塊。
// main.js
import {firstName, lastName, year} from './profile.js';
function setName(element) {
element.textContent = firstName + ' ' + lastName;
}
複製代碼
上面代碼的import命令,用於加載profile.js文件,並從中輸入變量。import命令接受一對大括號,裏面指定要從其餘模塊導入的變量名。大括號裏面的變量名,必須與被導入模塊(profile.js)對外接口的名稱相同。
若是想爲輸入的變量從新取一個名字,import命令要使用as關鍵字,將輸入的變量重命名。
import { lastName as surname } from './profile.js';
複製代碼
import命令輸入的變量都是只讀的,由於它的本質是輸入接口。也就是說,不容許在加載模塊的腳本里面,改寫接口。
import {a} from './xxx.js'
a = {}; // Syntax Error : 'a' is read-only;
複製代碼
上面代碼中,腳本加載了變量a,對其從新賦值就會報錯,由於a是一個只讀的接口。可是,若是a是一個對象,改寫a的屬性是容許的。
import {a} from './xxx.js'
a.foo = 'hello'; // 合法操做
複製代碼
上面代碼中,a的屬性能夠成功改寫,而且其餘模塊也能夠讀到改寫後的值。不過,這種寫法很難查錯,建議凡是輸入的變量,都看成徹底只讀,輕易不要改變它的屬性。
import後面的from指定模塊文件的位置,能夠是相對路徑,也能夠是絕對路徑,.js後綴能夠省略。若是隻是模塊名,不帶有路徑,那麼必須有配置文件,告訴 JavaScript 引擎該模塊的位置。
import {myMethod} from 'util';
複製代碼
上面代碼中,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結構。在靜態分析階段,這些語法都是無法獲得值的。
最後,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 模式。
目前階段,經過Babel 轉碼,CommonJS 模塊的require命令和 ES6模塊的import命令,能夠寫在同一個模塊裏面,可是最好不要這樣作。由於import在靜態解析階段執行,因此它是一個模塊之中最先執行的。下面的代碼可能不會獲得預期結果。
require('core-js/modules/es6.symbol');
require('core-js/modules/es6.promise');
import React from 'React';
複製代碼
除了指定加載某個輸出值,還能夠使用總體加載,即用星號(*)指定一個對象,全部輸出值都加載在這個對象上面。
下面是一個circle.js文件,它輸出兩個方法area和circumference。
// circle.js
export function area(radius) {
return Math.PI * radius * radius;
}
export function circumference(radius) {
return 2 * Math.PI * radius;
}
複製代碼
如今,加載這個模塊。
// main.js
import { area, circumference } from './circle';
console.log('圓面積:' + area(4));
console.log('圓周長:' + circumference(14));
複製代碼
上面寫法是逐一指定要加載的方法,總體加載的寫法以下。
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 () {};
複製代碼
從前面的例子能夠看出,使用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.js
export default function foo() {
console.log('foo');
}
// 或者寫成
function foo() {
console.log('foo');
}
export default foo;
複製代碼
上面代碼中,foo函數的函數名foo,在模塊外部是無效的。加載的時候,視同匿名函數加載。
下面比較一下默認輸出和正常輸出。
// 第一組
export default function crc32() { // 輸出
// ...
}
import crc32 from 'crc32'; // 輸入
// 第二組
export function crc32() { // 輸出
// ...
};
import {crc32} from 'crc32'; // 輸入
複製代碼
上面代碼的兩組寫法,第一組是使用export default時,對應的import語句不須要使用大括號;第二組是不使用export default時,對應的import語句須要使用大括號。
export default命令用於指定模塊的默認輸出。顯然,一個模塊只能有一個默認輸出,所以export default命令只能使用一次。因此,import命令後面纔不用加大括號,由於只可能惟一對應export default命令。
本質上,export default就是輸出一個叫作default的變量或方法,而後系統容許你爲它取任意名字。因此,下面的寫法是有效的。
// 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 default命令其實只是輸出一個叫作default的變量,因此它後面不能跟變量聲明語句。
// 正確
export var a = 1;
// 正確
var a = 1;
export default a;
// 錯誤
export default var a = 1;
複製代碼
上面代碼中,export default a的含義是將變量a的值賦給變量default。因此,最後一種寫法會報錯。
一樣地,由於export default命令的本質是將後面的值,賦給default變量,因此能夠直接將一個值寫在export default以後。
// 正確
export default 42;
// 報錯
export 42;
複製代碼
上面代碼中,後一句報錯是由於沒有指定對外的接口,而前一句指定對外接口爲default。
有了export default命令,輸入模塊時就很是直觀了,以輸入 lodash 模塊爲例。
import _ from 'lodash';
複製代碼
若是想在一條import語句中,同時輸入默認方法和其餘接口,能夠寫成下面這樣。
import _, { each, forEach } from 'lodash';
複製代碼
對應上面代碼的export語句以下。
export default function (obj) {
// ···
}
export function each(obj, iterator, context) {
// ···
}
export { each as forEach };
複製代碼
上面代碼的最後一行的意思是,暴露出forEach接口,默認指向each接口,即forEach和each指向同一個方法。
export default也能夠用來輸出類。
// MyClass.js
export default class { ... }
// main.js
import MyClass from 'MyClass';
let o = new MyClass();
複製代碼
若是在一個模塊之中,先輸入後輸出同一個模塊,import語句能夠與export語句寫在一塊兒。
export { foo, bar } from 'my_module';
// 能夠簡單理解爲
import { foo, bar } from 'my_module';
export { foo, bar };
複製代碼
上面代碼中,export和import語句能夠結合在一塊兒,寫成一行。但須要注意的是,寫成一行之後,foo和bar實際上並無被導入當前模塊,只是至關於對外轉發了這兩個接口,致使當前模塊不能直接使用foo和bar。
模塊的接口更名和總體輸出,也能夠採用這種寫法。
// 接口更名
export { foo as myFoo } from 'my_module';
// 總體輸出
export * from 'my_module';
複製代碼
默認接口的寫法以下。
export { default } from 'foo';
複製代碼
具名接口改成默認接口的寫法以下。
export { es6 as default } from './someModule';
// 等同於
import { es6 } from './someModule';
export default es6;
複製代碼
一樣地,默認接口也能夠更名爲具名接口。
export { default as es6 } from './someModule';
複製代碼
下面三種import語句,沒有對應的複合寫法。
import * as someIdentifier from "someModule";
import someIdentifier from "someModule";
import someIdentifier, { namedIdentifier } from "someModule";
複製代碼
爲了作到形式的對稱,如今有提案,提出補上這三種複合寫法。
export * as someIdentifier from "someModule";
export someIdentifier from "someModule";
export someIdentifier, { namedIdentifier } from "someModule";
複製代碼
模塊之間也能夠繼承。
假設有一個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變量和默認方法。
這時,也能夠將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方法。
本書介紹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/index';
複製代碼
前面介紹過,import命令會被 JavaScript 引擎靜態分析,先於模塊內的其餘語句執行(import命令叫作「鏈接」 binding 其實更合適)。因此,下面的代碼會報錯。
// 報錯
if (x === 2) {
import MyModual from './myModual';
}
複製代碼
上面代碼中,引擎處理import語句是在編譯時,這時不會去分析或執行if語句,因此import語句放在if代碼塊之中毫無心義,所以會報句法錯誤,而不是執行時錯誤。也就是說,import和export命令只能在模塊的頂層,不能在代碼塊之中(好比,在if代碼塊之中,或在函數之中)。
這樣的設計,當然有利於編譯器提升效率,但也致使沒法在運行時加載模塊。在語法上,條件加載就不可能實現。若是import命令要取代 Node 的require方法,這就造成了一個障礙。由於require是運行時加載模塊,import命令沒法取代require的動態加載功能。
const path = './' + fileName;
const myModual = require(path);
複製代碼
上面的語句就是動態加載,require到底加載哪個模塊,只有運行時才知道。import命令作不到這一點。
所以,有一個提案,建議引入import()函數,完成動態加載。
import(specifier)
複製代碼
上面代碼中,import函數的參數specifier,指定所要加載的模塊的位置。import命令可以接受什麼參數,import()函數就能接受什麼參數,二者區別主要是後者爲動態加載。
const main = document.querySelector('main');
import(`./section-modules/${someVariable}.js`)
.then(module => {
module.loadPageInto(main);
})
.catch(err => {
main.textContent = err.message;
});
複製代碼
import()函數能夠用在任何地方,不只僅是模塊,非模塊的腳本也能夠使用。它是運行時執行,也就是說,何時運行到這一句,就會加載指定的模塊。另外,import()函數與所加載的模塊沒有靜態鏈接關係,這點也是與import語句不相同。import()相似於 Node 的require方法,區別主要是前者是異步加載,後者是同步加載。
下面是import()的一些適用場合。
import()能夠在須要的時候,再加載某個模塊。
button.addEventListener('click', event => {
import('./dialogBox.js')
.then(dialogBox => {
dialogBox.open();
})
.catch(error => {
/* Error handling */
})
});
複製代碼
上面代碼中,import()方法放在click事件的監聽函數之中,只有用戶點擊了按鈕,纔會加載這個模塊。
import()能夠放在if代碼塊,根據不一樣的狀況,加載不一樣的模塊。
if (condition) {
import('moduleA').then(...);
} else {
import('moduleB').then(...);
}
複製代碼
上面代碼中,若是知足條件,就加載模塊 A,不然加載模塊 B。
import()容許模塊路徑動態生成。
import(f())
.then(...);
複製代碼
上面代碼中,根據函數f的返回結果,加載不一樣的模塊。
import('./myModule.js')
.then(({export1, export2}) => {
// ...·
});
複製代碼
上面代碼中,export1和export2都是myModule.js的輸出接口,能夠解構得到。
若是模塊有default輸出接口,能夠用參數直接得到。
import('./myModule.js')
.then(myModule => {
console.log(myModule.default);
});
複製代碼
上面的代碼也能夠使用具名輸入的形式。
import('./myModule.js')
.then(({default: theDefault}) => {
console.log(theDefault);
});
複製代碼
若是想同時加載多個模塊,能夠採用下面的寫法。
Promise.all([
import('./module1.js'),
import('./module2.js'),
import('./module3.js'),
])
.then(([module1, module2, module3]) => {
···
});
複製代碼
import()也能夠用在 async 函數之中。
async function main() {
const myModule = await import('./myModule.js');
const {export1, export2} = await import('./myModule.js');
const [module1, module2, module3] =
await Promise.all([
import('./module1.js'),
import('./module2.js'),
import('./module3.js'),
]);
}
main();
複製代碼
HTML 網頁中,瀏覽器經過<script>標籤加載 JavaScript 腳本。
<!-- 頁面內嵌的腳本 -->
<script type="application/javascript"> // module code </script>
<!-- 外部腳本 -->
<script type="application/javascript" src="path/to/myModule.js"> </script>
複製代碼
上面代碼中,因爲瀏覽器腳本的默認語言是 JavaScript,所以type="application/javascript"能夠省略。
默認狀況下,瀏覽器是同步加載 JavaScript 腳本,即渲染引擎遇到<script>標籤就會停下來,等到執行完腳本,再繼續向下渲染。若是是外部腳本,還必須加入腳本下載的時間。
若是腳本體積很大,下載和執行的時間就會很長,所以形成瀏覽器堵塞,用戶會感受到瀏覽器「卡死」了,沒有任何響應。這顯然是很很差的體驗,因此瀏覽器容許腳本異步加載,下面就是兩種異步加載的語法。
<script src="path/to/myModule.js" defer></script>
<script src="path/to/myModule.js" async></script>
複製代碼
上面代碼中,<script>標籤打開defer或async屬性,腳本就會異步加載。渲染引擎遇到這一行命令,就會開始下載外部腳本,但不會等它下載和執行,而是直接執行後面的命令。
defer與async的區別是:defer要等到整個頁面在內存中正常渲染結束(DOM 結構徹底生成,以及其餘腳本執行完成),纔會執行;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 type="module">,它們會按照在頁面出現的順序依次執行。
<script>標籤的async屬性也能夠打開,這時只要加載完成,渲染引擎就會中斷渲染當即執行。執行完成後,再恢復渲染。
<script type="module" src="./foo.js" async></script>
複製代碼
一旦使用了async屬性,<script type="module">就不會按照在頁面出現的順序執行,而是只要該模塊加載完成,就執行該模塊。
ES6 模塊也容許內嵌在網頁中,語法行爲與加載外部腳本徹底一致。
<script type="module">
import utils from "./utils.js";
// other code
</script>
複製代碼
對於外部的模塊腳本(上例是foo.js),有幾點須要注意。
下面是一個示例模塊。
import utils from 'https://example.com/js/utils.js';
const x = 1;
console.log(x === window.x); //false
console.log(this === undefined); // true
複製代碼
利用頂層的this等於undefined這個語法點,能夠偵測當前代碼是否在 ES6 模塊之中。
const isNotModuleScript = this !== undefined;//判斷是否在ES6模塊中
複製代碼
第二個差別是由於 CommonJS 加載的是一個對象(即module.exports屬性),該對象只有在腳本運行完纔會生成。而 ES6 模塊不是對象,它的對外接口只是一種靜態定義,在代碼靜態解析階段就會生成。
下面重點解釋第一個差別。
CommonJS 模塊輸出的是值的拷貝,也就是說,一旦輸出一個值,模塊內部的變化就影響不到這個值。請看下面這個模塊文件lib.js的例子。
// lib.js
var counter = 3;
function incCounter() {
counter++;
}
module.exports = {
counter: counter,
incCounter: incCounter,
};
複製代碼
上面代碼輸出內部變量counter和改寫這個變量的內部方法incCounter。而後,在main.js裏面加載這個模塊。
// main.js
var mod = require('./lib');
console.log(mod.counter); // 3
mod.incCounter();
console.log(mod.counter); // 3
複製代碼
上面代碼說明,lib.js模塊加載之後,它的內部變化就影響不到輸出的mod.counter了。這是由於mod.counter是一個原始類型的值,會被緩存。除非寫成一個函數,才能獲得內部變更後的值。
// lib.js
var counter = 3;
function incCounter() {
counter++;
}
module.exports = {
get counter() {
return counter
},
incCounter: incCounter,
};
複製代碼
上面代碼中,輸出的counter屬性其實是一個取值器函數。如今再執行main.js,就能夠正確讀取內部變量counter的變更了。
$ node main.js
3
4
複製代碼
ES6 模塊的運行機制與 CommonJS 不同。JS 引擎對腳本靜態分析的時候,遇到模塊加載命令import,就會生成一個只讀引用。等到腳本真正執行時,再根據這個只讀引用,到被加載的那個模塊裏面去取值。換句話說,ES6 的import有點像 Unix 系統的「符號鏈接」,原始值變了,import加載的值也會跟着變。所以,ES6 模塊是動態引用,而且不會緩存值,模塊裏面的變量綁定其所在的模塊。
// lib.js
export let counter = 3;
export function incCounter() {
counter++;
}
// main.js
import { counter, incCounter } from './lib';
console.log(counter); // 3
incCounter();
console.log(counter); // 4
複製代碼
上面代碼說明,ES6 模塊輸入的變量counter是活的,徹底反應其所在模塊lib.js內部的變化。
// m1.js
export var foo = 'bar';
setTimeout(() => foo = 'baz', 500);
// m2.js
import {foo} from './m1.js';
console.log(foo);
setTimeout(() => console.log(foo), 500);
複製代碼
上面代碼中,m1.js的變量foo,在剛加載時等於bar,過了 500 毫秒,又變爲等於baz。讓咱們看看,m2.js可否正確讀取這個變化。
$ babel-node m2.js
bar
baz
複製代碼
上面代碼代表,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指向的地址是隻讀的,不能從新賦值,這就比如main.js創造了一個名爲obj的const變量。
最後,export經過接口,輸出的是同一個值。不一樣的腳本加載這個接口,獲得的都是一樣的實例。
// mod.js
function C() {
this.sum = 0;
this.add = function () {
this.sum += 1;
};
this.show = function () {
console.log(this.sum);
};
}
export let c = new C();
複製代碼
上面的腳本mod.js,輸出的是一個C的實例。不一樣的腳本加載這個模塊,獲得的都是同一個實例。
// x.js
import {c} from './mod';
c.add();
// y.js
import {c} from './mod';
c.show();
// main.js
import './x';
import './y';
複製代碼
如今執行main.js,輸出的是1。
$ babel-node main.js
1
複製代碼
這就證實了x.js和y.js加載的都是C的同一個實例。
Node 對 ES6 模塊的處理比較麻煩,由於它有本身的 CommonJS 模塊格式,與 ES6 模塊格式是不兼容的。目前的解決方案是,將二者分開,ES6 模塊和 CommonJS 採用各自的加載方案。
Node 要求 ES6 模塊採用.mjs後綴文件名。也就是說,只要腳本文件裏面使用import或者export命令,那麼就必須採用.mjs後綴名。require命令不能加載.mjs文件,會報錯,只有import命令才能夠加載.mjs文件。反過來,.mjs文件裏面也不能使用require命令,必須使用import。
目前,這項功能還在試驗階段。安裝 Node v8.5.0 或以上版本,要用--experimental-modules參數才能打開該功能。
$ node --experimental-modules my-app.mjs
複製代碼
爲了與瀏覽器的import加載規則相同,Node 的.mjs文件支持 URL 路徑。
import './foo?query=1'; // 加載 ./foo 傳入參數 ?query=1
複製代碼
上面代碼中,腳本路徑帶有參數?query=1,Node 會按 URL 規則解讀。同一個腳本只要參數不一樣,就會被加載屢次,而且保存成不一樣的緩存。因爲這個緣由,只要文件名中含有:、%、#、?等特殊字符,最好對這些字符進行轉義。
目前,Node 的import命令只支持加載本地模塊(file:協議),不支持加載遠程模塊。
若是模塊名不含路徑,那麼import命令會去node_modules目錄尋找這個模塊。
import 'baz';
import 'abc/123';
複製代碼
若是模塊名包含路徑,那麼import命令會按照路徑去尋找這個名字的腳本文件。
import 'file:///etc/config/app.json';
import './foo';
import './foo?search';
import '../bar';
import '/baz';
複製代碼
若是腳本文件省略了後綴名,好比import './foo',Node 會依次嘗試四個後綴名:./foo.mjs、./foo.js、./foo.json、./foo.node。若是這些腳本文件都不存在,Node 就會去加載./foo/package.json的main字段指定的腳本。若是./foo/package.json不存在或者沒有main字段,那麼就會依次加載./foo/index.mjs、./foo/index.js、./foo/index.json、./foo/index.node。若是以上四個文件仍是都不存在,就會拋出錯誤。
最後,Node 的import命令是異步加載,這一點與瀏覽器的處理方法相同。
ES6 模塊應該是通用的,同一個模塊不用修改,就能夠用在瀏覽器環境和服務器環境。爲了達到這個目標,Node 規定 ES6 模塊之中不能使用 CommonJS 模塊的特有的一些內部變量。
首先,就是this關鍵字。ES6 模塊之中,頂層的this指向undefined;CommonJS 模塊的頂層this指向當前模塊,這是二者的一個重大差別
其次,如下這些頂層變量在 ES6 模塊之中都是不存在的。
arguments
require
module
exports
__filename
__dirname
複製代碼
若是你必定要使用這些變量,有一個變通方法,就是寫一個 CommonJS 模塊輸出這些變量,而後再用 ES6 模塊加載這個 CommonJS 模塊。可是這樣一來,該 ES6 模塊就不能直接用於瀏覽器環境了,因此不推薦這樣作。
// expose.js
module.exports = {__dirname};
// use.mjs
import expose from './expose.js';
const {__dirname} = expose;
複製代碼
上面代碼中,expose.js是一個 CommonJS 模塊,輸出變量__dirname,該變量在 ES6 模塊之中不存在。ES6 模塊加載expose.js,就能夠獲得__dirname。
CommonJS 模塊的輸出都定義在module.exports這個屬性上面。Node 的import命令加載 CommonJS模塊,Node 會自動將module.exports屬性,看成模塊的默認輸出,即等同於export default xxx。
// a.js
module.exports = {
foo: 'hello',
bar: 'world'
};
// 等同於
export default {
foo: 'hello',
bar: 'world'
};
複製代碼
import命令加載上面的模塊,module.exports會被視爲默認輸出,即import命令實際上輸入的是這樣一個對象{ default: module.exports }。
因此,一共有三種寫法,能夠拿到 CommonJS 模塊的module.exports。
// 寫法一
import baz from './a';
// baz = {foo: 'hello', bar: 'world'};
// 寫法二
import {default as baz} from './a';
// baz = {foo: 'hello', bar: 'world'};
// 寫法三
import * as baz from './a';
// baz = {
// get default() {return module.exports;},
// get foo() {return this.default.foo}.bind(baz),
// get bar() {return this.default.bar}.bind(baz)
// }
複製代碼
上面代碼的第三種寫法,能夠經過baz.default拿到module.exports。foo屬性和bar屬性就是能夠經過這種方法拿到了module.exports。
// b.js
module.exports = null;
// es.js
import foo from './b';
// foo = null;
import * as bar from './b';
// bar = { default:null };
複製代碼
上面代碼中,es.js採用第二種寫法時,要經過bar.default這樣的寫法,才能拿到module.exports。
// c.js
module.exports = function two() {
return 2;
};
// es.js
import foo from './c';
foo(); // 2
import * as bar from './c';
bar.default(); // 2
bar(); // throws, bar is not a function
複製代碼
上面代碼中,bar自己是一個對象,不能看成函數調用,只能經過bar.default調用。
CommonJS 模塊的輸出緩存機制,在 ES6 加載方式下依然有效。
// foo.js
module.exports = 123;
setTimeout(_ => module.exports = null);
複製代碼
上面代碼中,對於加載foo.js的腳本,module.exports將一直是123,而不會變成null。
因爲 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();
複製代碼
CommonJS 模塊加載 ES6 模塊,不能使用require命令,而要使用import()函數。ES6 模塊的全部輸出接口,會成爲輸入對象的屬性。
// es.mjs
let foo = { bar: 'my-default' };
export default foo;
// cjs.js
const es_namespace = await import('./es.mjs');
// es_namespace = {
// get default() {
// ...
// }
// }
console.log(es_namespace.default);
// { bar:'my-default' }
複製代碼
上面代碼中,default接口變成了es_namespace.default屬性。
// es.js
export let foo = { bar:'my-default' };
export { foo as bar };
export function f() {};
export class c {};
// cjs.js
const es_namespace = await import('./es');
// es_namespace = {
// get foo() {return foo;}
// get bar() {return foo;}
// get f() {return f;}
// get c() {return c;}
// }
複製代碼
「循環加載」(circular dependency)指的是,a腳本的執行依賴b腳本,而b腳本的執行又依賴a腳本。
// a.js
var b = require('b');
// b.js
var a = require('a');
複製代碼
一般,「循環加載」表示存在強耦合,若是處理很差,還可能致使遞歸加載,使得程序沒法執行,所以應該避免出現。
可是實際上,這是很難避免的,尤爲是依賴關係複雜的大項目,很容易出現a依賴b,b依賴c,c又依賴a這樣的狀況。這意味着,模塊加載機制必須考慮「循環加載」的狀況。
對於 JavaScript 語言來講,目前最多見的兩種模塊格式CommonJS 和 ES6,處理「循環加載」的方法是不同的,返回的結果也不同。
CommonJs 模塊的重要特性是加載時執行,即腳本代碼在require的時候,就會所有執行。一旦出現某個模塊被"循環加載",就只輸出已經執行的部分,還未執行的部分不會輸出。
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接着往下執行,直到執行完畢。咱們寫一個腳本main.js,驗證這個過程。
var a = require('./a.js');
var b = require('./b.js');
console.log('在 main.js 之中, a.done=%j, b.done=%j', a.done, b.done);
複製代碼
執行main.js,運行結果以下。
$ node main.js
在 b.js 之中,a.done = false
b.js 執行完畢
在 a.js 之中,b.done = true
a.js 執行完畢
在 main.js 之中, a.done=true, b.done=true
複製代碼
上面的代碼證實了兩件事。一是,在b.js之中,a.js沒有執行完畢,只執行了第一行。二是,main.js執行到第二行時,不會再次執行b.js,而是輸出緩存的b.js的執行結果,即它的第四行。
exports.done = true;
複製代碼
總之,CommonJS 輸入的是被輸出值的拷貝,不是引用。
另外,因爲 CommonJS 模塊遇到循環加載時,返回的是當前已經執行的部分的值,而不是代碼所有執行後的值,二者可能會有差別。因此,輸入變量的時候,必須很是當心。
var a = require('a'); // 安全的寫法
var foo = require('a').foo; // 危險的寫法
exports.good = function (arg) {
return a.foo('good', arg); // 使用的是 a.foo 的最新值
};
exports.bad = function (arg) {
return foo('bad', arg); // 使用的是一個部分加載時的值
};
複製代碼
上面代碼中,若是發生循環加載,require('a').foo的值極可能後面會被改寫,改用require('a')會更保險一點。
ES6 處理「循環加載」與 CommonJS 有本質的不一樣。ES6 模塊是動態引用,若是使用import從一個模塊加載變量(即import foo from 'foo'),那些變量不會被緩存,而是成爲一個指向被加載模塊的引用,須要開發者本身保證,真正取值的時候可以取到值。
// a.mjs
import {bar} from './b';
console.log('a.mjs');
console.log(bar);
export let foo = 'foo';
// b.mjs
import {foo} from './a';
console.log('b.mjs');
console.log(foo);
export let bar = 'bar';
複製代碼
上面代碼中,a.mjs加載b.mjs,b.mjs又加載a.mjs,構成循環加載。執行a.mjs,結果以下。
$ node --experimental-modules a.mjs
b.mjs
ReferenceError: foo is not defined
複製代碼
上面代碼中,執行a.mjs之後會報錯,foo變量未定義,這是爲何?
讓咱們一行行來看,ES6 循環加載是怎麼處理的。首先,執行a.mjs之後,引擎發現它加載了b.mjs,所以會優先執行b.mjs,而後再執行a.mjs。接着,執行b.mjs的時候,已知它從a.mjs輸入了foo接口,這時不會去執行a.mjs,而是認爲這個接口已經存在了,繼續往下執行。執行到第三行console.log(foo)的時候,才發現這個接口根本沒定義,所以報錯。
解決這個問題的方法,就是讓b.mjs運行的時候,foo已經有定義了。這能夠經過將foo寫成函數來解決。
// a.mjs
import {bar} from './b';
console.log('a.mjs');
console.log(bar());
function foo() { return 'foo' }
export {foo};
// b.mjs
import {foo} from './a';
console.log('b.mjs');
console.log(foo());
function bar() { return 'bar' }
export {bar};
複製代碼
這時再執行a.mjs就能夠獲得預期結果。
$ node --experimental-modules a.mjs
b.mjs
foo
a.mjs
bar
複製代碼
這是由於函數具備提高做用,在執行import {bar} from './b'時,函數foo就已經有定義了,因此b.mjs加載的時候不會報錯。這也意味着,若是把函數foo改寫成函數表達式,也會報錯。
// a.mjs
import {bar} from './b';
console.log('a.mjs');
console.log(bar());
const foo = () => 'foo';
export {foo};
複製代碼
上面代碼的第四行,改爲了函數表達式,就不具備提高做用,執行就會報錯。
// even.js
import { odd } from './odd'
export var counter = 0;
export function even(n) {
counter++;
return n === 0 || odd(n - 1);
}
// odd.js
import { even } from './even';
export function odd(n) {
return n !== 0 && even(n - 1);
}
複製代碼
上面代碼中,even.js裏面的函數even有一個參數n,只要不等於 0,就會減去 1,傳入加載的odd()。odd.js也會作相似操做。
$ babel-node
> import * as m from './even.js';
> m.even(10);
true
> m.counter
6
> m.even(20)
true
> m.counter
17
複製代碼
上面代碼中,參數n從 10 變爲 0 的過程當中,even()一共會執行 6 次,因此變量counter等於 6。第二次調用even()時,參數n從 20 變爲 0,even()一共會執行 11 次,加上前面的 6 次,因此變量counter等於 17。
// even.js
var odd = require('./odd');
var counter = 0;
exports.counter = counter;
exports.even = function (n) {
counter++;
return n == 0 || odd(n - 1);
}
// odd.js
var even = require('./even').even;
module.exports = function (n) {
return n != 0 && even(n - 1);
}
複製代碼
上面代碼中,even.js加載odd.js,而odd.js又去加載even.js,造成「循環加載」。這時,執行引擎就會輸出even.js已經執行的部分(不存在任何結果),因此在odd.js之中,變量even等於undefined,等到後面調用even(n - 1)就會報錯。
$ node
> var m = require('./even');
> m.even(10)
TypeError: even is not a function 複製代碼
瀏覽器目前還不支持 ES6 模塊,爲了如今就能使用,能夠將其轉爲 ES5 的寫法。除了 Babel 能夠用來轉碼以外,還有如下兩個方法,也能夠用來轉碼。
ES6 module transpiler是 square 公司開源的一個轉碼器,能夠將 ES6 模塊轉爲 CommonJS 模塊或 AMD 模塊的寫法,從而在瀏覽器中使用。
首先,安裝這個轉碼器。
$ npm install -g es6-module-transpiler
複製代碼
而後,使用compile-modules convert命令,將 ES6 模塊文件轉碼。
$ compile-modules convert file1.js file2.js
複製代碼
-o參數能夠指定轉碼後的文件名。
$ compile-modules convert -o out.js file1.js
複製代碼
另外一種解決方法是使用 SystemJS。它是一個墊片庫(polyfill),能夠在瀏覽器內加載 ES6 模塊、AMD 模塊和 CommonJS 模塊,將其轉爲 ES5 格式。它在後臺調用的是 Google 的 Traceur 轉碼器。
使用時,先在網頁內載入system.js文件。
<script src="system.js"></script>
複製代碼
而後,使用System.import方法加載模塊文件。
<script>
System.import('./app.js');
</script>
複製代碼
上面代碼中的./app,指的是當前目錄下的 app.js 文件。它能夠是 ES6 模塊文件,System.import會自動將其轉碼。
須要注意的是,System.import使用異步加載,返回一個 Promise 對象,能夠針對這個對象編程。下面是一個模塊文件。
// app/es6-file.js:
export class q {
constructor() {
this.es6 = 'hello';
}
}
複製代碼
而後,在網頁內加載這個模塊文件。
<script>
System.import('app/es6-file').then(function(m) {
console.log(new m.q().es6); // hello
});
</script>
複製代碼
上面代碼中,System.import方法返回的是一個 Promise 對象,因此能夠用then方法指定回調函數。
首先咱們要明白一個前提,CommonJS模塊規範和ES6模塊規範徹底是兩種不一樣的概念。
Node應用由模塊組成,採用CommonJS模塊規範。
根據這個規範,每一個文件就是一個模塊,有本身的做用域。在一個文件裏面定義的變量、函數、類,都是私有的,對其餘文件不可見。
CommonJS規範規定,每一個模塊內部,module變量表明當前模塊。這個變量是一個對象,它的exports屬性(即module.exports)是對外的接口。加載某個模塊,實際上是加載該模塊的module.exports屬性。
var x = 5;
var addX = function (value) {
return value + x;
};
module.exports.x = x;
module.exports.addX = addX;
複製代碼
上面代碼經過module.exports輸出變量x和函數addX。
require方法用於加載模塊。
var example = require('./example.js');
console.log(example.x); // 5
console.log(example.addX(1)); // 6
複製代碼
不一樣於CommonJS,ES6使用 export 和 import 來導出、導入模塊。
// profile.js
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;
export {firstName, lastName, year};
複製代碼
須要特別注意的是,export命令規定的是對外的接口,必須與模塊內部的變量創建一一對應關係。
// 寫法一
export var m = 1;
// 寫法二
var m = 1;
export {m};
// 寫法三
var n = 1;
export {n as m};
複製代碼
爲了方便,Node爲每一個模塊提供一個exports變量,指向module.exports。這等同在每一個模塊頭部,有一行這樣的命令。
var exports = module.exports;
複製代碼
因而咱們能夠直接在 exports 對象上添加方法,表示對外輸出的接口,如同在module.exports上添加同樣。注意,不能直接將exports變量指向一個值,由於這樣等於切斷了exports與module.exports的聯繫。
使用export default命令,爲模塊指定默認輸出。
require 引入的是 module.exports
import 引入的是module.exports.default
// export-default.js
export default function () {
console.log('foo');
}
複製代碼
若是你以爲個人文章還不錯的話,能夠給個star哦~,GitHub地址