import
陳述式中使用變數嗎?import
陳述式中使用解構嗎?eval()
模組的程式碼嗎?JavaScript 很久以前就有模組了。然而,它們是透過函式庫實作,而不是內建在語言中。ES6 是 JavaScript 第一次有內建模組。
ES6 模組儲存在檔案中。每個檔案只有一個模組,每個模組只有一個檔案。您可以使用兩種方式從模組中匯出內容。 這兩種方式可以混合使用,但通常最好分開使用。
可以有多個命名匯出
//------ lib.js ------
export
const
sqrt
=
Math
.
sqrt
;
export
function
square
(
x
)
{
return
x
*
x
;
}
export
function
diag
(
x
,
y
)
{
return
sqrt
(
square
(
x
)
+
square
(
y
));
}
//------ main.js ------
import
{
square
,
diag
}
from
'lib'
;
console
.
log
(
square
(
11
));
// 121
console
.
log
(
diag
(
4
,
3
));
// 5
您也可以匯入完整的模組
//------ main.js ------
import
*
as
lib
from
'lib'
;
console
.
log
(
lib
.
square
(
11
));
// 121
console
.
log
(
lib
.
diag
(
4
,
3
));
// 5
可以有一個預設匯出。例如,一個函式
//------ myFunc.js ------
export
default
function
()
{
···
}
// no semicolon!
//------ main1.js ------
import
myFunc
from
'myFunc'
;
myFunc
();
或一個類別
//------ MyClass.js ------
export
default
class
{
···
}
// no semicolon!
//------ main2.js ------
import
MyClass
from
'MyClass'
;
const
inst
=
new
MyClass
();
請注意,如果您預設匯出函式或類別(它們是匿名宣告),則結尾沒有分號。
腳本 | 模組 | |
---|---|---|
HTML 元素 | <script> |
<script type="module"> |
預設模式 | 非嚴格 | 嚴格 |
頂層變數是 | 全域 | 模組的區域 |
頂層的 this 值 |
window |
未定義 |
執行 | 同步 | 非同步 |
宣告式匯入(import 陳述式) |
否 | 是 |
程式化匯入(基於 Promise 的 API) | 是 | 是 |
檔案副檔名 | .js |
.js |
儘管 JavaScript 從未有內建模組,但社群已收斂成一種簡單的模組樣式,這在 ES5 及更早版本的函式庫中受到支援。ES6 也採用了這種樣式
'../model/user'
):這些路徑會根據匯入模組的位置來解釋。通常可以省略檔案副檔名 .js
。'/lib/js/helpers'
):直接指向要匯入的模組檔案。'util'
):模組名稱所參照的內容必須設定。這種模組方法避免了全域變數,唯一全域的項目是模組規格說明。
ES5 模組系統在沒有語言明確支援的情況下,能運作得很好,這令人印象深刻。最重要的兩個(且不幸地不相容)標準是
以上只是對 ES5 模組的簡化說明。如果您想要更深入的資料,請參閱 Addy Osmani 的「使用 AMD、CommonJS 和 ES Harmony 編寫模組化 JavaScript」。
ECMAScript 6 模組的目標是建立一個讓 CommonJS 和 AMD 使用者都滿意的格式
由於內建在語言中,讓 ES6 模組能超越 CommonJS 和 AMD(詳細資訊會在稍後說明)
ES6 模組標準分為兩個部分
匯出有兩種:具名匯出(每個模組數個)和預設匯出(每個模組一個)。如後文所述,可以同時使用這兩種匯出,但通常最好將它們分開。
模組可以透過在宣告前加上關鍵字 export
來匯出多個項目。這些匯出以其名稱區分,稱為具名匯出。
//------ lib.js ------
export
const
sqrt
=
Math
.
sqrt
;
export
function
square
(
x
)
{
return
x
*
x
;
}
export
function
diag
(
x
,
y
)
{
return
sqrt
(
square
(
x
)
+
square
(
y
));
}
//------ main.js ------
import
{
square
,
diag
}
from
'lib'
;
console
.
log
(
square
(
11
));
// 121
console
.
log
(
diag
(
4
,
3
));
// 5
還有其他方法可以指定具名匯出(後文會說明),但我覺得這個方法相當方便:只要像沒有外部世界一樣撰寫程式碼,然後使用關鍵字標記要匯出的所有內容。
如果您願意,也可以匯入整個模組,並透過屬性表示法來參考其具名匯出
//------ main.js ------
import
*
as
lib
from
'lib'
;
console
.
log
(
lib
.
square
(
11
));
// 121
console
.
log
(
lib
.
diag
(
4
,
3
));
// 5
使用 CommonJS 語法撰寫的相同程式碼:一段時間以來,我嘗試了幾種聰明的策略,以減少 Node.js 中模組匯出的重複性。現在我比較喜歡以下簡單但略為冗長的風格,它讓人想起揭露模組模式
//------ lib.js ------
var
sqrt
=
Math
.
sqrt
;
function
square
(
x
)
{
return
x
*
x
;
}
function
diag
(
x
,
y
)
{
return
sqrt
(
square
(
x
)
+
square
(
y
));
}
module
.
exports
=
{
sqrt
:
sqrt
,
square
:
square
,
diag
:
diag
,
};
//------ main.js ------
var
square
=
require
(
'lib'
).
square
;
var
diag
=
require
(
'lib'
).
diag
;
console
.
log
(
square
(
11
));
// 121
console
.
log
(
diag
(
4
,
3
));
// 5
只匯出單一值的模組在 Node.js 社群中非常受歡迎。但它們在前端開發中也很常見,在前端開發中,您通常會為模型和元件建立類別,每個模組一個類別。ES6 模組可以選擇一個預設匯出,也就是主要的匯出值。預設匯出特別容易匯入。
以下 ECMAScript 6 模組「是」一個單一函式
//------ myFunc.js ------
export
default
function
()
{}
// no semicolon!
//------ main1.js ------
import
myFunc
from
'myFunc'
;
myFunc
();
預設匯出為類別的 ECMAScript 6 模組如下所示
//------ MyClass.js ------
export
default
class
{}
// no semicolon!
//------ main2.js ------
import
MyClass
from
'MyClass'
;
const
inst
=
new
MyClass
();
預設匯出有兩種樣式
您可以使用關鍵字 export default
為任何函式宣告(或產生器函式宣告)或類別宣告加上前綴,使其成為預設匯出
export
default
function
foo
()
{}
// no semicolon!
export
default
class
Bar
{}
// no semicolon!
您也可以在此情況下省略名稱。這使得預設匯出成為 JavaScript 中唯一具有匿名函式宣告和匿名類別宣告的地方
export
default
function
()
{}
// no semicolon!
export
default
class
{}
// no semicolon!
當您查看前兩行程式碼時,您會預期 export default
的運算元為表達式。它們僅出於一致性的原因而成為宣告:運算元可以是命名宣告,將其匿名版本解釋為表達式會令人困惑(甚至比引入新類型的宣告更令人困惑)。
如果您希望將運算元解釋為表達式,則需要使用括號
export
default
(
function
()
{});
export
default
(
class
{});
值是透過表達式產生的
export
default
'abc'
;
export
default
foo
();
export
default
/^xyz$/
;
export
default
5
*
7
;
export
default
{
no
:
false
,
yes
:
true
};
每個預設匯出都具有以下結構。
export
default
«
expression
»
;
這等於
const
__default__
=
«
expression
»
;
export
{
__default__
as
default
};
// (A)
A 行中的陳述句是一個匯出子句(在後面的章節中說明)。
引入了第二種預設匯出樣式,因為如果變數宣告宣告多個變數,則無法將其有意義地轉換為預設匯出
export
default
const
foo
=
1
,
bar
=
2
,
baz
=
3
;
// not legal JavaScript!
foo
、bar
和 baz
三個變數中哪一個會是預設匯出?
正如稍後更詳細地說明的那樣,ES6 模組的結構是靜態的,您不能有條件地匯入或匯出內容。這帶來了許多好處。
此限制透過僅允許在模組的頂層匯入和匯出,在語法上強制執行
if
(
Math
.
random
())
{
import
'foo'
;
// SyntaxError
}
// You can’t even nest `import` and `export`
// inside a simple block:
{
import
'foo'
;
// SyntaxError
}
模組匯入會提升(在內部移至目前範圍的開頭)。因此,您在模組中提到它們的位置並不重要,而且以下程式碼可以在沒有任何問題的情況下執行
foo
();
import
{
foo
}
from
'my_module'
;
ES6 模組的匯入是匯出實體的唯讀檢視。這表示連接到模組主體內宣告的變數會保持運作,如下列程式碼所示。
//------ lib.js ------
export
let
counter
=
3
;
export
function
incCounter
()
{
counter
++
;
}
//------ main.js ------
import
{
counter
,
incCounter
}
from
'./lib'
;
// The imported value `counter` is live
console
.
log
(
counter
);
// 3
incCounter
();
console
.
log
(
counter
);
// 4
這在底層是如何運作的,將在 後面的章節 中說明。
匯入作為檢視具有下列優點
如果 A 和 B 兩個模組彼此 循環依賴,表示 A(可能間接/遞移)匯入 B,而 B 匯入 A。如果可能,應避免循環依賴,它們會導致 A 和 B 緊密耦合 – 它們只能一起使用和演進。
那麼,為什麼要支援循環依賴?偶爾,你無法避開它們,這就是為什麼支援它們是一項重要的功能。後面的章節 有更多資訊。
讓我們看看 CommonJS 和 ECMAScript 6 如何處理循環依賴。
下列 CommonJS 程式碼正確處理兩個模組 a
和 b
彼此循環依賴。
//------ a.js ------
var
b
=
require
(
'b'
);
function
foo
()
{
b
.
bar
();
}
exports
.
foo
=
foo
;
//------ b.js ------
var
a
=
require
(
'a'
);
// (i)
function
bar
()
{
if
(
Math
.
random
())
{
a
.
foo
();
// (ii)
}
}
exports
.
bar
=
bar
;
如果模組 a
首先被匯入,則在第 i 行,模組 b
會在匯出新增到其中之前取得 a
的匯出物件。因此,b
無法在頂層存取 a.foo
,但該屬性會在 a
的執行完成後存在。如果之後呼叫 bar()
,則第 ii 行中的方法呼叫會運作。
作為一般規則,請記住,對於循環依賴,你無法在模組的主體中存取匯入。這是現象的本質,而且不會隨著 ECMAScript 6 模組而改變。
CommonJS 方法的限制為
module
.
exports
=
function
()
{
···
};
如果模組 a
這樣做,則在進行指定後,模組 b
的變數 a
就不會更新。它會繼續參考原始的匯出物件。
b
無法像這樣匯入 foo
var
foo
=
require
(
'a'
).
foo
;
foo
只會是 undefined
。換句話說,你只能透過 a.foo
來參照 foo
。
這些限制表示匯出者和匯入者都必須知道循環依賴並明確支援它們。
ES6 模組自動支援循環依賴。也就是說,它們沒有前一節提到的 CommonJS 模組的兩個限制:預設匯出有效,不合格的名稱匯入也一樣(以下範例中的第 i 和 iii 行)。因此,你可以實作循環依賴彼此的模組,如下所示。
//------ a.js ------
import
{
bar
}
from
'b'
;
// (i)
export
function
foo
()
{
bar
();
// (ii)
}
//------ b.js ------
import
{
foo
}
from
'a'
;
// (iii)
export
function
bar
()
{
if
(
Math
.
random
())
{
foo
();
// (iv)
}
}
這段程式碼有效,因為如前一節所述,匯入是匯出的檢視。這表示即使是不合格的匯入(例如第 ii 行的 bar
和第 iv 行的 foo
)也是指派給原始資料的間接參照。因此,在面對循環依賴時,你透過不合格的匯入或透過模組存取名稱匯出並無差別:這兩種情況都涉及間接參照,而且總是有效。
ECMAScript 6 提供多種匯入樣式2
import
localName
from
'src/my_lib'
;
import
*
as
my_lib
from
'src/my_lib'
;
import
{
name1
,
name2
}
from
'src/my_lib'
;
你可以重新命名名稱匯入
// Renaming: import `name1` as `localName1`
import
{
name1
as
localName1
,
name2
}
from
'src/my_lib'
;
// Renaming: import the default export as `foo`
import
{
default
as
foo
}
from
'src/my_lib'
;
import
'src/my_lib'
;
只有兩種方法可以結合這些樣式,而且它們出現的順序是固定的;預設匯出總是排在第一位。
import
theDefault
,
*
as
my_lib
from
'src/my_lib'
;
import
theDefault
,
{
name1
,
name2
}
from
'src/my_lib'
;
在模組中,您可以使用兩種方式來匯出命名項目。
一方面,您可以使用關鍵字 export
來標記宣告。
export
var
myVar1
=
···
;
export
let
myVar2
=
···
;
export
const
MY_CONST
=
···
;
export
function
myFunc
()
{
···
}
export
function
*
myGeneratorFunc
()
{
···
}
export
class
MyClass
{
···
}
另一方面,您可以將所有想要匯出的項目列在模組的結尾(其樣式類似於揭露模組模式)。
const
MY_CONST
=
···
;
function
myFunc
()
{
···
}
export
{
MY_CONST
,
myFunc
};
您也可以使用不同的名稱來匯出項目
export
{
MY_CONST
as
FOO
,
myFunc
};
重新匯出表示將另一個模組的匯出項目新增到目前模組的匯出項目中。您可以新增所有其他模組的匯出項目
export
*
from
'src/other_module'
;
預設匯出項目會被 export *
忽略3。
或者,您可以更具選擇性(在重新命名時為選用)
export
{
foo
,
bar
}
from
'src/other_module'
;
// Renaming: export other_module’s foo as myFoo
export
{
foo
as
myFoo
,
bar
}
from
'src/other_module'
;
下列陳述會將另一個模組 foo
的預設匯出項目設為目前模組的預設匯出項目
export
{
default
}
from
'foo'
;
下列陳述會將模組 foo
的命名匯出項目 myFunc
設為目前模組的預設匯出項目
export
{
myFunc
as
default
}
from
'foo'
;
ECMAScript 6 提供了多種匯出樣式4
export
*
from
'src/other_module'
;
export
{
foo
as
myFoo
,
bar
}
from
'src/other_module'
;
export
{
default
}
from
'src/other_module'
;
export
{
default
as
foo
}
from
'src/other_module'
;
export
{
foo
as
default
}
from
'src/other_module'
;
export
{
MY_CONST
as
FOO
,
myFunc
};
export
{
foo
as
default
};
export
var
foo
;
export
let
foo
;
export
const
foo
;
export
function
myFunc
()
{}
export
function
*
myGenFunc
()
{}
export
class
MyClass
{}
export
default
function
myFunc
()
{}
export
default
function
()
{}
export
default
function
*
myGenFunc
()
{}
export
default
function
*
()
{}
export
default
class
MyClass
{}
export
default
class
{}
export
default
foo
;
export
default
'Hello world!'
;
export
default
3
*
7
;
export
default
(
function
()
{});
下列模式在 JavaScript 中非常常見:函式庫是一個單一函式,但透過該函式的屬性提供其他服務。範例包括 jQuery 和 Underscore.js。以下是 Underscore 作為 CommonJS 模組的草圖
//------ underscore.js ------
var
_
=
function
(
obj
)
{
···
};
var
each
=
_
.
each
=
_
.
forEach
=
function
(
obj
,
iterator
,
context
)
{
···
};
module
.
exports
=
_
;
//------ main.js ------
var
_
=
require
(
'underscore'
);
var
each
=
_
.
each
;
···
使用 ES6 範例,函式 _
是預設匯出,而 each
和 forEach
是命名匯出。事實證明,您實際上可以同時具有命名匯出和預設匯出。例如,先前的 CommonJS 模組重新寫為 ES6 模組,如下所示
//------ underscore.js ------
export
default
function
(
obj
)
{
···
}
export
function
each
(
obj
,
iterator
,
context
)
{
···
}
export
{
each
as
forEach
};
//------ main.js ------
import
_
,
{
each
}
from
'underscore'
;
···
請注意,CommonJS 版本和 ECMAScript 6 版本僅大致相似。後者具有扁平結構,而前者是巢狀的。
我通常建議將兩種匯出類型分開:每個模組,只有一個預設匯出或只有一個命名匯出。
不過,這並不是一個非常強烈的建議;偶爾混合使用這兩種方式是有道理的。一個範例是一個預設匯出的實體模組。對於單元測試,可以透過命名匯出額外提供一些內部元件。
預設匯出實際上只是一個命名匯出,其特殊名稱為 default
。也就是說,下列兩個陳述式是等效的
import
{
default
as
foo
}
from
'lib'
;
import
foo
from
'lib'
;
類似地,下列兩個模組具有相同的預設匯出
//------ module1.js ------
export
default
function
foo
()
{}
// function declaration!
//------ module2.js ------
function
foo
()
{}
export
{
foo
as
default
};
default
:作為匯出名稱可以,但作為變數名稱不行 你不能使用保留字(例如 default
和 new
)作為變數名稱,但你可以使用它們作為匯出名稱(你也可以在 ECMAScript 5 中使用它們作為屬性名稱)。如果你想要直接匯入此類命名匯出,你必須將它們重新命名為適當的變數名稱。
這表示 default
只能出現在重新命名匯入的左側
import
{
default
as
foo
}
from
'some_module'
;
而且它只能出現在重新命名匯出的右側
export
{
foo
as
default
};
在重新匯出時,as
的兩側都是匯出名稱
export
{
myFunc
as
default
}
from
'foo'
;
export
{
default
as
otherFunc
}
from
'foo'
;
// The following two statements are equivalent:
export
{
default
}
from
'foo'
;
export
{
default
as
default
}
from
'foo'
;
除了用於處理模組的宣告式語法之外,還有一個程式化 API。它允許你
載入器處理解析模組規格符(import-from
結尾的字串 ID)、載入模組等。其建構函式為 Reflect.Loader
。每個平台都在全域變數 System
(系統載入器)中保留一個預設執行個體,它實作其特定類型的模組載入。
您可以透過基於 Promises 的 API 以程式方式匯入模組
System
.
import
(
'some_module'
)
.
then
(
some_module
=>
{
// Use some_module
})
.
catch
(
error
=>
{
···
});
System.import()
讓您可以
<script>
元素中使用模組(在不支援模組語法的部分,請參閱 模組與腳本 的部分以取得詳細資訊)。System.import()
會擷取單一模組,您可以使用 Promise.all()
來匯入多個模組
Promise
.
all
(
[
'module1'
,
'module2'
,
'module3'
]
.
map
(
x
=>
System
.
import
(
x
)))
.
then
(([
module1
,
module2
,
module3
])
=>
{
// Use module1, module2, module3
});
載入器有更多方法。三個重要的方法為
System.module(source, options?)
source
中的 JavaScript 程式碼評估為一個模組(透過 Promise 非同步傳送)。System.set(name, module)
System.module()
建立的模組)。System.define(name, source, options?)
source
中的模組程式碼並註冊結果。模組載入器 API 將有各種掛鉤來設定載入程序。使用案例包括
可設定的模組載入是 Node.js 和 CommonJS 受限的領域。
讓我們看看 ES6 模組如何在瀏覽器中受到支援。
在瀏覽器中,有兩種不同類型的實體:腳本和模組。它們的語法略有不同,且工作方式也不同。
以下是差異的概觀,詳細資訊將在稍後說明
腳本 | 模組 | |
---|---|---|
HTML 元素 | <script> |
<script type="module"> |
預設模式 | 非嚴格 | 嚴格 |
頂層變數是 | 全域 | 模組的區域 |
頂層的 this 值 |
window |
未定義 |
執行 | 同步 | 非同步 |
宣告式匯入(import 陳述式) |
否 | 是 |
程式化匯入(基於 Promise 的 API) | 是 | 是 |
檔案副檔名 | .js |
.js |
腳本是傳統的瀏覽器方式,用於嵌入 JavaScript 並參考外部 JavaScript 檔案。腳本具有 網際網路媒體類型,用作
<script>
元素的 type
屬性的值。請注意,對於 HTML5,建議在 <script>
元素中省略 type
屬性(如果它們包含或參考 JavaScript)。以下是最重要的值
text/javascript
:是一個舊值,如果在腳本標籤中省略 type
屬性,則會用作預設值。對於 Internet Explorer 8 及更早版本,它是 最安全的選擇。application/javascript
:建議用於目前的瀏覽器。腳本通常會同步載入或執行。JavaScript 執行緒會停止,直到程式碼載入或執行完畢。
為了符合 JavaScript 常見的執行至完成語意,模組的主體必須不中斷地執行。這會為匯入模組留下兩個選項
ECMAScript 6 提供了兩全其美的方案:Node.js 的同步語法加上 AMD 的非同步載入。為了讓兩者都可行,ES6 模組在語法上比 Node.js 模組不靈活:匯入和匯出必須在頂層進行。這表示它們也不能有條件。此限制允許 ES6 模組載入器靜態分析模組匯入的模組,並在執行其主體之前載入這些模組。
腳本的同步性質會阻止它們成為模組。腳本甚至無法宣告性地匯入模組(如果您想這樣做,則必須使用程式化的模組載入器 API)。
模組可以使用 <script>
元素的新變體從瀏覽器中使用,該變體完全是非同步的
<
script
type
=
"module"
>
import
$
from
'lib/jquery'
;
var
x
=
123
;
// The current scope is not global
console
.
log
(
'$'
in
window
);
// false
console
.
log
(
'x'
in
window
);
// false
// `this` is undefined
console
.
log
(
this
===
undefined
);
// true
</
script
>
如你所見,元素有其自己的範圍,而「內部」的變數是屬於該範圍的。請注意,模組程式碼隱含地處於嚴格模式。這是一個好消息,再也不需要 'use strict'
了。
類似於一般的 <script>
元素,<script type="module">
也可用於載入外部模組。例如,下列標籤透過 main
模組啟動一個網路應用程式(屬性名稱 import
是我的發明,目前還不清楚將採用哪個名稱)。
<
script
type
=
"module"
import
=
"impl/main"
></
script
>
透過自訂 <script>
類型在 HTML 中支援模組的優點,在於可以透過多載(一個函式庫)輕鬆地將該支援帶到舊引擎。最終可能會或可能不會有專門的模組元素(例如 <module>
)。
檔案是模組還是指令碼,僅由其如何被匯入或載入決定。大多數模組都有匯入或匯出,因此可以被偵測到。但如果模組既沒有匯入也沒有匯出,那麼它就與指令碼沒有區別。例如
var
x
=
123
;
這段程式碼的語意會根據它是被解釋為模組還是指令碼而有所不同
x
在模組範圍內建立。x
會變成一個全域變數,以及全域物件(在瀏覽器中為 window
)的屬性。更實際的範例是一個安裝某些東西的模組,例如全域變數中的多載或全域事件監聽器。這樣的模組既不匯入也不匯出任何東西,並透過空的匯入來啟動
import
'./my_module'
;
匯入在 CommonJS 和 ES6 中的工作方式不同
以下各節說明了這表示什麼意思。
使用 CommonJS(Node.js)模組時,事情會以相對熟悉的方式運作。
如果你將一個值匯入到變數中,則該值會被複製兩次:一次是在匯出時(A 行),另一次是在匯入時(B 行)。
//------ lib.js ------
var
counter
=
3
;
function
incCounter
()
{
counter
++
;
}
module
.
exports
=
{
counter
:
counter
,
// (A)
incCounter
:
incCounter
,
};
//------ main1.js ------
var
counter
=
require
(
'./lib'
).
counter
;
// (B)
var
incCounter
=
require
(
'./lib'
).
incCounter
;
// The imported value is a (disconnected) copy of a copy
console
.
log
(
counter
);
// 3
incCounter
();
console
.
log
(
counter
);
// 3
// The imported value can be changed
counter
++
;
console
.
log
(
counter
);
// 4
如果您透過 exports 物件存取值,它仍會在匯出時複製一次
//------ main2.js ------
var
lib
=
require
(
'./lib'
);
// The imported value is a (disconnected) copy
console
.
log
(
lib
.
counter
);
// 3
lib
.
incCounter
();
console
.
log
(
lib
.
counter
);
// 3
// The imported value can be changed
lib
.
counter
++
;
console
.
log
(
lib
.
counter
);
// 4
與 CommonJS 相比,匯入是匯出值的檢視。換句話說,每個匯入都是與匯出資料的即時連線。匯入是唯讀的
import x from 'foo'
)就像 const
宣告的變數。foo
的屬性(import * as foo from 'foo'
)就像 凍結物件 的屬性。下列程式碼示範匯入如何像檢視
//------ lib.js ------
export
let
counter
=
3
;
export
function
incCounter
()
{
counter
++
;
}
//------ main1.js ------
import
{
counter
,
incCounter
}
from
'./lib'
;
// The imported value `counter` is live
console
.
log
(
counter
);
// 3
incCounter
();
console
.
log
(
counter
);
// 4
// The imported value can’t be changed
counter
++
;
// TypeError
如果您透過星號(*
)匯入模組物件,您會得到相同的結果
//------ main2.js ------
import
*
as
lib
from
'./lib'
;
// The imported value `counter` is live
console
.
log
(
lib
.
counter
);
// 3
lib
.
incCounter
();
console
.
log
(
lib
.
counter
);
// 4
// The imported value can’t be changed
lib
.
counter
++
;
// TypeError
請注意,雖然您無法變更匯入的值,但您可以變更它們所參照的物件。例如
//------ lib.js ------
export
let
obj
=
{};
//------ main.js ------
import
{
obj
}
from
'./lib'
;
obj
.
prop
=
123
;
// OK
obj
=
{};
// TypeError
為何要引入如此複雜的匯入機制,而且偏離既定的做法?
根據我的經驗,ES6 匯入運作良好,您很少需要思考底層發生了什麼事。
在底層,匯入如何作為匯出的檢視運作?匯出透過資料結構匯出項目管理。所有匯出項目(重新匯出的項目除外)都有下列兩個名稱
匯入實體後,該實體總是透過指標存取,指標有兩個組成部分模組和本機名稱。換句話說,該指標參照模組中的繫結(變數的儲存空間)。
讓我們檢視各種匯出所建立的匯出名稱和區域名稱。下表(改編自 ES6 規格)提供概觀,後續章節有更多詳細資訊。
陳述式 | 區域名稱 | 匯出名稱 |
---|---|---|
export {v}; |
'v' |
'v' |
export {v as x}; |
'v' |
'x' |
export const v = 123; |
'v' |
'v' |
export function f() {} |
'f' |
'f' |
export default function f() {} |
'f' |
'default' |
export default function () {} |
'*default*' |
'default' |
export default 123; |
'*default*' |
'default' |
function
foo
()
{}
export
{
foo
};
foo
foo
function
foo
()
{}
export
{
foo
as
bar
};
foo
bar
這是內嵌匯出
export
function
foo
()
{}
它等同於以下程式碼
function
foo
()
{}
export
{
foo
};
因此,我們有以下名稱
foo
foo
有兩種預設匯出
以下程式碼預設匯出表達式 123
的結果
export
default
123
;
它等同於
const
*
default
*
=
123
;
// *not* legal JavaScript
export
{
*
default
*
as
default
};
如果您預設匯出表達式,您會得到
*default*
default
區域名稱的選擇是為了避免與任何其他區域名稱衝突。
請注意,預設匯出仍會建立繫結。但是,由於 *default*
不是合法的識別碼,您無法從模組內部存取該繫結。
以下程式碼預設匯出函式宣告
export
default
function
foo
()
{}
它等同於
function
foo
()
{}
export
{
foo
as
default
};
名稱為
foo
default
這表示你可以透過將不同的值指定給 foo
,來變更模組中預設輸出的值。
(僅限於) 預設輸出,你也可以省略函式宣告的名稱
export
default
function
()
{}
這等於
function
*
default
*
()
{}
// *not* legal JavaScript
export
{
*
default
*
as
default
};
名稱為
*default*
default
預設輸出的產生器宣告和類別宣告,其運作方式與預設輸出的函式宣告類似。
此部分提供 ECMAScript 2015 (ES6) 語言規範的指標。
管理匯入
各種輸出的匯出名稱和本機名稱顯示在「表 42」的「原始碼文字模組記錄」部分。而「靜態語意:ExportEntries」部分有更詳細的說明。你可以看到匯出項目是靜態設定的 (在評估模組之前),評估匯出陳述的說明在「執行時期語意:評估」部分。
如果你想了解 ECMAScript 6 模組,了解哪些目標影響了其設計會有所幫助。主要的目標有
以下小節說明這些目標。
建議預設輸出「就是」模組的模組語法可能看起來有點奇怪,但如果你考慮到一個主要的設計目標是讓預設輸出盡可能方便,這就說得通了。引用 David Herman
ECMAScript 6 偏好單一/預設匯出樣式,並提供最簡潔的語法來匯入預設值。匯入命名匯出可以,甚至應該更簡潔。
當前的 JavaScript 模組格式具有動態結構:匯入和匯出的內容會在執行階段變更。ES6 引入其模組格式的原因之一,是為了啟用靜態結構,這有幾個好處。但在我們深入探討之前,讓我們先了解靜態結構的意義。
這表示您可以在編譯階段(靜態地)決定匯入和匯出,您只需要查看原始碼,而不需要執行它。ES6 在語法上強制執行這一點:您只能在頂層匯入和匯出(絕不能巢狀在條件式陳述式中)。而且匯入和匯出陳述式沒有動態部分(不允許變數等)。
以下是兩個沒有靜態結構的 CommonJS 模組範例。在第一個範例中,您必須執行程式碼才能找出它匯入了什麼
var
my_lib
;
if
(
Math
.
random
())
{
my_lib
=
require
(
'foo'
);
}
else
{
my_lib
=
require
(
'bar'
);
}
在第二個範例中,您必須執行程式碼才能找出它匯出了什麼
if
(
Math
.
random
())
{
exports
.
baz
=
···
;
}
ECMAScript 6 模組較不靈活,並強制您使用靜態結構。因此,您會獲得幾個好處,如下所述。
在前端開發中,模組通常會處理如下
捆綁的原因是
原因 1 對 HTTP/1 很重要,因為請求檔案的成本相對較高。這將隨著 HTTP/2 而改變,因此這個原因在那裡並不重要。
原因 3 仍會令人信服。這只能透過具有靜態結構的模組格式來達成。
模組捆綁器 Rollup 證明 ES6 模組可以有效地結合,因為它們都符合單一範圍(在變更變數名稱以消除名稱衝突後)。這要歸功於 ES6 模組的兩個特性
舉例來說,考慮以下兩個 ES6 模組。
// lib.js
export
function
foo
()
{}
export
function
bar
()
{}
// main.js
import
{
foo
}
from
'./lib.js'
;
console
.
log
(
foo
());
Rollup 可以將這兩個 ES6 模組套件成以下單一 ES6 模組(請注意已移除未使用的匯出 bar
)
function
foo
()
{}
console
.
log
(
foo
());
Rollup 方法的另一個好處是套件沒有自訂格式,它只是一個 ES6 模組。
如果您需要 CommonJS 中的函式庫,您會取得一個物件
var
lib
=
require
(
'lib'
);
lib
.
someFunc
();
// property lookup
因此,透過 lib.someFunc
存取命名匯出表示您必須執行屬性查詢,這很慢,因為它是動態的。
相反地,如果您在 ES6 中匯入函式庫,您會靜態知道其內容,並可以最佳化存取
import
*
as
lib
from
'lib'
;
lib
.
someFunc
();
// statically resolved
使用靜態模組結構,您會永遠靜態知道模組內部任何位置可見哪些變數
這對於檢查給定的識別碼是否拼寫正確有很大的幫助。這種類型的檢查是 JSLint 和 JSHint 等 linter 的熱門功能;在 ECMAScript 6 中,大部分都可以由 JavaScript 引擎執行。
此外,任何命名匯入(例如 lib.foo
)的存取也可以靜態檢查。
巨集仍然在 JavaScript 未來的藍圖中。如果 JavaScript 引擎支援巨集,您可以透過函式庫為其新增新的語法。 Sweet.js 是 JavaScript 的實驗性巨集系統。以下是 Sweet.js 網站的範例:類別的巨集。
// Define the macro
macro
class
{
rule
{
$className
{
constructor
$cparams
$cbody
$
(
$mname
$mparams
$mbody
)
...
}
}
=>
{
function
$className
$cparams
$cbody
$
(
$className
.
prototype
.
$mname
=
function
$mname
$mparams
$mbody
;
)
...
}
}
// Use the macro
class
Person
{
constructor
(
name
)
{
this
.
name
=
name
;
}
say
(
msg
)
{
console
.
log
(
this
.
name
+
" says: "
+
msg
);
}
}
var
bob
=
new
Person
(
"Bob"
);
bob
.
say
(
"Macros are sweet!"
);
對於巨集,JavaScript 引擎在編譯前會執行預處理步驟:如果剖析器產生的符號串流中的符號序列與巨集的模式部分相符,則會以巨集主體產生的符號取代。預處理步驟僅在您能夠靜態找出巨集定義時才有效。因此,如果您想透過模組匯入巨集,則它們必須有靜態結構。
靜態類型檢查會施加類似巨集的限制:它只能在靜態找到類型定義時執行。同樣地,類型只能從具有靜態結構的模組匯入。
類型之所以有吸引力,是因為它們可以在 JavaScript 中啟用靜態類型化的快速方言,其中可以撰寫對效能至關重要的程式碼。其中一種方言是 低階 JavaScript (LLJS)。
如果您想支援將具有巨集和靜態類型的語言編譯成 JavaScript,則 JavaScript 的模組應具有靜態結構,原因如前兩節所述。
ECMAScript 6 模組必須獨立於引擎是否同步載入模組(例如在伺服器上)或非同步載入(例如在瀏覽器中)。其語法非常適合同步載入,非同步載入則由其靜態結構啟用:由於您可以靜態地確定所有匯入,因此可以在評估模組主體之前載入它們(類似於 AMD 模組)。
支援循環相依性是 ES6 模組的主要目標。原因如下
循環相依性並非本質上邪惡。特別是對於物件,有時你甚至會想要這種相依性。例如,在某些樹狀結構(例如 DOM 文件)中,父層會參考子層,而子層會參考回父層。在函式庫中,你通常可以透過謹慎的設計來避免循環相依性。然而,在大型系統中,它們可能會發生,特別是在重構期間。如果模組系統支援循環相依性,這時就會非常有用,因為在重構時系統不會中斷。
Node.js 文件承認循環相依性的重要性,而 Rob Sayre 提供了額外的證據
資料點:我曾經為 Firefox 實作一個類似 [ECMAScript 6 模組] 的系統。我在發布後 3 週就被要求支援循環相依性。
Alex Fritze 發明且我參與開發的系統並不完美,而且語法也不太美觀。但 它在 7 年後仍然被使用,所以它一定做對了某些事。
import
陳述式完全是靜態的:其模組指定符始終是固定的。如果你想要動態地決定要載入哪個模組,你需要使用 程式化載入器 API
const
moduleSpecifier
=
'module_'
+
Math
.
random
();
System
.
import
(
moduleSpecifier
)
.
then
(
the_module
=>
{
// Use the_module
})
匯入陳述式必須始終位於模組的最上層。這表示你無法將它們巢狀在 if
陳述式、函式等內部。因此,如果你想要條件式或依需求載入模組,你必須使用 程式化載入器 API
if
(
Math
.
random
())
{
System
.
import
(
'some_module'
)
.
then
(
some_module
=>
{
// Use some_module
})
}
import
陳述式中使用變數嗎? 不行,你不能。請記住,匯入的內容不能依賴於任何在執行時期計算的內容。因此
// Illegal syntax:
import
foo
from
'some_module'
+
SUFFIX
;
import
陳述式中使用解構嗎? 不行,你不能。 import
陳述式只看起來像解構,但完全不同(靜態、匯入是檢視等)。
因此,你不能在 ES6 中執行類似這樣的操作
// Illegal syntax:
import
{
foo
:
{
bar
}
}
from
'some_module'
;
你可能會想 - 如果我們可以簡單地預設匯出物件(就像在 CommonJS 中),為什麼我們需要命名匯出?答案是,你無法透過物件強制靜態結構,並失去所有相關的優點(這些優點已在 本章節 中說明)。
eval()
模組的程式碼嗎? 不行,你不能。模組對於 eval()
來說是太高階的建構。 模組載入器 API 提供了從字串建立模組的方法。在語法上,eval()
接受指令碼(不允許 import
和 export
),而不是模組。
乍看之下,將模組建置到 ECMAScript 6 中似乎是一個無聊的功能 - 畢竟,我們已經有幾個好的模組系統。但 ECMAScript 6 模組有幾個新功能
ES6 模組也將 - 希望 - 結束當前主流標準 CommonJS 和 AMD 之間的分歧。擁有單一的、原生的模組標準意味著
navigator
的全域變數或屬性。Math
和 JSON
等物件在 ECMAScript 5 中作為函式的命名空間。在未來,此類功能可透過模組提供。