前言
《你不知道的JavaScript》是前端必讀書籍系列之一锦爵,有上中下三卷含鳞,該篇主要記錄在精讀上卷上半部分(作用域和閉包)時的知識點(diǎn),記錄一個大綱计福,便于日后查看,反復(fù)閱讀會對JS有更深入的理解徽职。
后期會反復(fù)總結(jié)象颖,不定期更新!
1. 作用域和閉包
1.1 編譯原理
-
傳統(tǒng)編譯語言流程
-
分詞/詞法分析
將由字符組成的字符串分解成(對編程語言來說)有意義的代碼塊姆钉,var a = 2;说订,被分解成 var、a潮瓶、=陶冷、2、;
-
解析/語法分析
將詞法單元流(數(shù)組)轉(zhuǎn)換成一個由元素逐級嵌套所組成的代表了程序語法結(jié)構(gòu)的樹筋讨。這個樹被稱為“抽象語法樹(AST)”
代碼生成
-
-
javaScript 編譯
javaScript 的編譯過程不是發(fā)生在構(gòu)建之前的埃叭。對于 JavaScript 來說摸恍,大部分情況下編譯發(fā)生在代碼執(zhí)行前的幾微秒(甚至更短)的時間內(nèi)悉罕。
1.2 LHS 查詢 和 RHS 查詢
如果查找的目的是對變量進(jìn)行賦值,那么就會使用 LHS 查詢立镶;如果目的是獲取變量的值壁袄,就會使用 RHS 查詢。賦值操作符會導(dǎo)致 LHS 查詢媚媒。 = 操作符或調(diào)用函數(shù)時傳入?yún)?shù)的操作都會導(dǎo)致關(guān)聯(lián)作用域的賦值操作嗜逻。
-
區(qū)別:變量還沒有聲明(在任何作用域中都無法找到該變量)的情況下,這兩種查詢的行為是不一樣的缭召。
function foo(a) { console.log(a + b); b = a; } foo(2);
第一次對 b 進(jìn)行 RHS 查詢時是無法找到該變量的栈顷。也就是說逆日,這是一個“未聲明”的變量,因?yàn)樵谌魏蜗嚓P(guān)的作用域中都無法找到它萄凤。
如果 RHS 查詢在所有嵌套的作用域中遍尋不到所需的變量室抽,引擎就會拋出 ReferenceError 異常。值得注意的是靡努, ReferenceError 是非常重要的異常類型坪圾。
相較之下,當(dāng)引擎執(zhí)行 LHS 查詢時惑朦,如果在頂層(全局作用域)中也無法找到目標(biāo)變量兽泄,全局作用域中就會創(chuàng)建一個具有該名稱的變量,并將其返還給引擎漾月,前提是程序運(yùn)行在非“嚴(yán)格模式”下病梢。
ES5 中引入了“嚴(yán)格模式”。同正常模式梁肿,或者說寬松 / 懶惰模式相比飘千,嚴(yán)格模式在行為上有很多不同。其中一個不同的行為是嚴(yán)格模式禁止自動或隱式地創(chuàng)建全局變量栈雳。因此护奈,在嚴(yán)格模式中 LHS 查詢失敗時,并不會創(chuàng)建并返回一個全局變量哥纫,引擎會拋出同 RHS 查詢失敗時類似的 ReferenceError 異常霉旗。
接下來,如果 RHS 查詢找到了一個變量蛀骇,但是你嘗試對這個變量的值進(jìn)行不合理的操作厌秒,比如試圖對一個非函數(shù)類型的值進(jìn)行函數(shù)調(diào)用,或著引用 null 或 undefined 類型的值中的屬性擅憔,那么引擎會拋出另外一種類型的異常鸵闪,叫作 TypeError 。
ReferenceError 同作用域判別失敗相關(guān)暑诸,而 TypeError 則代表作用域判別成功了蚌讼,但是對結(jié)果的操作是非法或不合理的。
2. 詞法作用域
作用域兩種主要的工作模型: 詞法作用域(最普遍)个榕、動態(tài)作用域篡石,js 采用詞法作用域
2.1 詞法階段
作用域查找會在找到第一個匹配的標(biāo)識符時停止。在多層的嵌套作用域中可以定義同名的標(biāo)識符西采,這叫作“遮蔽效應(yīng)”(內(nèi)部的標(biāo)識符“遮蔽”了外部的標(biāo)識符)凰萨。拋開遮蔽效應(yīng),作用域查找始終從運(yùn)行時所處的最內(nèi)部作用域開始,逐級向外或者說向上進(jìn)行胖眷,直到遇見第一個匹配的標(biāo)識符為止
全局變量會自動成為全局對象(比如瀏覽器中的 window 對象)的屬性武通,因此可以不直接通過全局對象的詞法名稱,而是間接地通過對全局對象屬性的引用來對其進(jìn)行訪問珊搀。
window.a
通過這種技術(shù)可以訪問那些被同名變量所遮蔽的全局變量厅须。但非全局的變量如果被遮蔽了,無論如何都無法被訪問到食棕。
2.2 欺騙詞法
在運(yùn)行時來“修改”(也可以說欺騙)詞法作用域朗和,JavaScript 中有兩種機(jī)制來實(shí)現(xiàn)這個目的。欺騙詞法作用域會導(dǎo)致性能下降
2.2.1 eval
eval(..) 函數(shù)可以接受一個字符串為參數(shù)簿晓,并將其中的內(nèi)容視為好像在書寫時就存在于程序中這個位置的代碼
function foo(str, a) {
eval(str); // 欺騙眶拉!
console.log(a, b);
}
var b = 2;
foo("var b = 3;", 1); // 1, 3
在嚴(yán)格模式的程序中, eval(..) 在運(yùn)行時有其自己的詞法作用域憔儿,意味著其中的聲明無法修改所在的作用域忆植。
function foo(str) {
"use strict";
eval(str);
console.log(a); // ReferenceError: a is not defined
}
foo("var a = 2");
2.2.2 with
with 可以將一個沒有或有多個屬性的對象處理為一個完全隔離的詞法作用域,因此這個對象的屬性也會被處理為定義在這個作用域中的詞法標(biāo)識符谒臼。
function foo(obj) {
with (obj) {
a = 2;
}
}
var o1 = {
a: 3,
};
var o2 = {
b: 3,
};
foo(o1);
console.log(o1.a); // 2
foo(o2);
console.log(o2.a); // undefined
console.log(a); // 2——不好朝刊,a 被泄漏到全局作用域上了!
eval(..) 函數(shù)如果接受了含有一個或多個聲明的代碼蜈缤,就會修改其所處的詞法作用域拾氓,而
with 聲明實(shí)際上是根據(jù)你傳遞給它的對象憑空創(chuàng)建了一個全新的詞法作用域。
總結(jié)
使用這其中任何一個機(jī)制都將導(dǎo)致代碼運(yùn)行變慢底哥。不要使用它們咙鞍。
3. 函數(shù)作用域和塊作用域
3.1 函數(shù)作用域
var a = 2;
function foo() {
// <-- 添加這一行
var a = 3;
console.log(a); // 3
} // <-- 以及這一行
foo(); // <-- 以及這一行
console.log(a); // 2
問題: 首先,必須聲明一個具名函數(shù) foo() 趾徽,意味著 foo 這個名稱本身“污染”了所在作用域(在這個例子中是全局作用域)续滋。其次,必須顯式地通過函數(shù)名( foo() )調(diào)用這個函數(shù)才能運(yùn)行其中的代碼孵奶。
var a = 2;
(function foo() {
// <-- 添加這一行
var a = 3;
console.log(a); // 3
})(); // <-- 以及這一行
console.log(a); // 2
函數(shù)會被當(dāng)作函數(shù)表達(dá)式而不是一個標(biāo)準(zhǔn)的函數(shù)聲明來處理疲酌。
總結(jié): 區(qū)分函數(shù)聲明和表達(dá)式最簡單的方法是看 function 關(guān)鍵字出現(xiàn)在聲明中的位置(不僅僅是一行代碼,而是整個聲明中的位置)了袁。如果 function 是聲明中的第一個詞朗恳,那么就是一個函數(shù)聲明,否則就是一個函數(shù)表達(dá)式早像。
函數(shù)聲明和函數(shù)表達(dá)式之間最重要的區(qū)別是它們的名稱標(biāo)識符將會綁定在何處僻肖。比較一下前面兩個代碼片段。第一個片段中 foo 被綁定在所在作用域中卢鹦,可以直接通過 foo() 來調(diào)用它。第二個片段中 foo 被綁定在函數(shù)表達(dá)式自身的函數(shù)中而不是所在作用域中。
換句話說冀自, (function foo(){ .. }) 作為函數(shù)表達(dá)式意味著 foo 只能在 .. 所代表的位置中被訪問揉稚,外部作用域則不行。 foo 變量名被隱藏在自身中意味著不會非必要地污染外部作用域熬粗。
3.1.1 匿名和具名
- 匿名函數(shù)缺點(diǎn)
- 匿名函數(shù)在棧追蹤中不會顯示出有意義的函數(shù)名搀玖,使得調(diào)試很困難。
- 如果沒有函數(shù)名驻呐,當(dāng)函數(shù)需要引用自身時只能使用已經(jīng)過期的 arguments.callee 引用灌诅,比如在遞歸中。另一個函數(shù)需要引用自身的例子含末,是在事件觸發(fā)后事件監(jiān)聽器需要解綁自身猜拾。
- 匿名函數(shù)省略了對于代碼可讀性 / 可理解性很重要的函數(shù)名。一個描述性的名稱可以讓代碼不言自明佣盒。
3.1.2 立即執(zhí)行函數(shù)表達(dá)式
IIFE挎袜,代表立即執(zhí)行函數(shù)表達(dá)式(Immediately Invoked Function Expression)
(function foo(){ .. })() & (function(){ .. }()) 兩種形式
- 傳遞參數(shù)
var a = 2;
(function IIFE(global) {
var a = 3;
console.log(a); // 3
console.log(global.a); // 2
})(window);
console.log(a); // 2
- IIFE 還有一種變化的用途是倒置代碼的運(yùn)行順序,將需要運(yùn)行的函數(shù)放在第二位肥惭,在 IIFE
執(zhí)行之后當(dāng)作參數(shù)傳遞進(jìn)去盯仪。
var a = 2;
(function IIFE(def) {
def(window);
})(function def(global) {
var a = 3;
console.log(a); // 3
console.log(global.a); // 2
});
3.2 塊作用域
函數(shù)作用域是最常見的作用域單元,當(dāng)然也是現(xiàn)行大多數(shù) JavaScript 中最普遍的設(shè)計(jì)
方法, 除 JavaScript 外的很多編程語言都支持塊作用域. 塊作用域是一個用來對之前的最小授權(quán)原則進(jìn)行擴(kuò)展的工具蜜葱,將代碼從在函數(shù)中隱藏信息擴(kuò)展為在塊中隱藏信息全景。
3.2.1 with
塊作用域的一種形式
3.2.2 try/catch
try {
undefined(); // 執(zhí)行一個非法操作來強(qiáng)制制造一個異常
} catch (err) {
console.log(err); // 能夠正常執(zhí)行!
}
console.log(err); // ReferenceError: err not found
3.2.3 let
ES6 改變了現(xiàn)狀牵囤,引入了新的 let 關(guān)鍵字蚪燕,提供了除 var 以外的另一種變量聲明方式。
let 關(guān)鍵字可以將變量綁定到所在的任意作用域中(通常是 { .. } 內(nèi)部)奔浅。
但是使用 let 進(jìn)行的聲明不會在塊作用域中進(jìn)行提升馆纳。聲明的代碼被運(yùn)行之前,聲明并不“存在”汹桦。
1. 垃圾收集
另一個塊作用域非常有用的原因和閉包及回收內(nèi)存垃圾的回收機(jī)制相關(guān)鲁驶。
- 不使用塊作用域
function process(data) {
// 在這里做點(diǎn)有趣的事情
}
var someReallyBigData = { .. };
process( someReallyBigData );
var btn = document.getElementById( "my_button" );
btn.addEventListener( "click", function click(evt) {
console.log("button clicked");
}, /*capturingPhase=*/false );
click 函數(shù)的點(diǎn)擊回調(diào)并不需要 someReallyBigData 變量。理論上這意味著當(dāng) process(..) 執(zhí)
行后舞骆,在內(nèi)存中占用大量空間的數(shù)據(jù)結(jié)構(gòu)就可以被垃圾回收了钥弯。但是,由于 click 函數(shù)形成
了一個覆蓋整個作用域的閉包督禽,JavaScript 引擎極有可能依然保存著這個結(jié)構(gòu)(取決于具體
實(shí)現(xiàn))脆霎。
- 使用塊作用域
塊作用域可以打消這種顧慮,可以讓引擎清楚地知道沒有必要繼續(xù)保存 someReallyBigData 了.
function process(data) {
// 在這里做點(diǎn)有趣的事情
}
// 在這個塊中定義的內(nèi)容可以銷毀了狈惫!
{
let someReallyBigData = { .. };
process( someReallyBigData );
}
var btn = document.getElementById( "my_button" );
btn.addEventListener( "click", function click(evt){
console.log("button clicked");
}, /*capturingPhase=*/false );
2.let 循環(huán)
一個 let 可以發(fā)揮優(yōu)勢的典型例子就是之前討論的 for 循環(huán)睛蛛。
for (let i = 0; i < 10; i++) {
console.log(i);
}
console.log(i); // ReferenceError
for 循環(huán)頭部的 let 不僅將 i 綁定到了 for 循環(huán)的塊中,事實(shí)上它將其重新綁定到了循環(huán)
的每一個迭代中,確保使用上一個循環(huán)迭代結(jié)束時的值重新進(jìn)行賦值忆肾。
3.2.4 const
除了 let 以外荸频,ES6 還引入了 const ,同樣可以用來創(chuàng)建塊作用域變量客冈,但其值是固定的(常量)旭从。之后任何試圖修改值的操作都會引起錯誤。
4. 提升
4.1 變量聲明和函數(shù)聲明的提升
a = 2;
var a;
console.log(a); // 2
console.log(a); // undefined
var a = 2;
引擎會在解釋 JavaScript 代碼之前首先對其進(jìn)行編譯场仲。編譯階段中的一部分工作就是找到所有的聲明和悦,并用合適的作用域?qū)⑺鼈冴P(guān)聯(lián)起來。所以渠缕,包括變量和函數(shù)在內(nèi)的所有聲明都會在任何代碼被執(zhí)行前首先被處理鸽素。
分析: var a = 2, JavaScript 實(shí)際上會將其看成兩個聲明: var a; 和 a = 2; 褐健。第一個定義聲明是在編譯階段進(jìn)行的付鹿。第二個賦值聲明會被留在原地等待執(zhí)行階段。這個過程就好像變量和函數(shù)聲明從它們在代碼中出現(xiàn)的位置被“移動”到了最上面蚜迅。這個過程就叫作提升舵匾。先聲明,后賦值谁不。
foo();
function foo() {
console.log(a); // undefined
var a = 2;
}
另外值得注意的是坐梯,每個作用域都會進(jìn)行提升操作。盡管前面大部分的代碼片段已經(jīng)簡化了(因?yàn)樗鼈冎话肿饔糜颍┥才粒覀冋谟懻摰?foo(..) 函數(shù)自身也會在內(nèi)部對 var a 進(jìn)行提升(顯然并不是提升到了整個程序的最上方)吵血。因此上面的代碼實(shí)際上會被理解為下面的形式:
function foo() {
var a;
console.log(a); // undefined
a = 2;
}
foo();
可以看到,函數(shù)聲明會被提升偷溺,但是函數(shù)表達(dá)式卻不會被提升蹋辅。
foo(); // 不是 ReferenceError, 而是 TypeError!
var foo = function bar() {
// ...
};
這段程序中的變量標(biāo)識符 foo() 被提升并分配給所在作用域(在這里是全局作用域),因此 foo() 不會導(dǎo)致 ReferenceError 挫掏。但是 foo 此時并沒有賦值(如果它是一個函數(shù)聲明而不是函數(shù)表達(dá)式侦另,那么就會賦值)。 foo() 由于對 undefined 值進(jìn)行函數(shù)調(diào)用而導(dǎo)致非法操作尉共,因此拋出 TypeError 異常褒傅。
4.2 函數(shù)優(yōu)先
函數(shù)聲明和變量聲明都會被提升。但是一個值得注意的細(xì)節(jié)是函數(shù)會首先被提升袄友,然后才是變量殿托。
foo(); // 1
var foo;
function foo() {
console.log(1);
}
foo = function () {
console.log(2);
};
這個代碼片段會被引擎理解為如下形式:
function foo() {
console.log(1);
}
foo(); // 1
foo = function () {
console.log(2);
};
5. 作用域閉包
當(dāng)函數(shù)可以記住并訪問所在的詞法作用域時,就產(chǎn)生了閉包剧蚣,即使函數(shù)是在當(dāng)前詞法作用域之外執(zhí)行支竹。
5.1 實(shí)質(zhì)問題
當(dāng)函數(shù)可以記住并訪問所在的詞法作用域時旋廷,就產(chǎn)生了閉包,即使函數(shù)是在當(dāng)前詞法作用域之外執(zhí)行唾戚。
function foo() {
var a = 2;
function bar() {
console.log(a);
}
return bar;
}
var baz = foo();
baz(); // 2 ——這就是閉包的效果柳洋。 foo()();
函數(shù) bar() 的詞法作用域能夠訪問 foo() 的內(nèi)部作用域待诅。然后我們將 bar() 函數(shù)本身當(dāng)作一個值類型進(jìn)行傳遞叹坦。在這個例子中,我們將 bar 所引用的函數(shù)對象本身當(dāng)作返回值卑雁。
在 foo() 執(zhí)行后募书,其返回值(也就是內(nèi)部的 bar() 函數(shù))賦值給變量 baz 并調(diào)用 baz() ,實(shí)際上只是通過不同的標(biāo)識符引用調(diào)用了內(nèi)部的函數(shù) bar() 测蹲。
bar() 顯然可以被正常執(zhí)行莹捡。但是在這個例子中,它在自己定義的詞法作用域以外的地方執(zhí)行扣甲。
在 foo() 執(zhí)行后篮赢,通常會期待 foo() 的整個內(nèi)部作用域都被銷毀,因?yàn)槲覀冎酪嬗欣厥掌饔脕磲尫挪辉偈褂玫膬?nèi)存空間琉挖。由于看上去 foo() 的內(nèi)容不會再被使用启泣,所以很自然地會考慮對其進(jìn)行回收。
而閉包的“神奇”之處正是可以阻止這件事情的發(fā)生示辈。事實(shí)上內(nèi)部作用域依然存在寥茫,因此沒有被回收。誰在使用這個內(nèi)部作用域矾麻?原來是 bar() 本身在使用纱耻。
拜 bar() 所聲明的位置所賜,它擁有涵蓋 foo() 內(nèi)部作用域的閉包险耀,使得該作用域能夠一直存活弄喘,以供 bar() 在之后任何時間進(jìn)行引用。
bar() 依然持有對該作用域的引用甩牺,而這個引用就叫作閉包蘑志。
var fn;
function foo() {
var a = 2;
function baz() {
console.log(a);
}
fn = baz; // 將 baz 分配給全局變量
}
function bar() {
fn(); // 這就是閉包!
}
foo();
bar(); // 2
無論通過何種手段將內(nèi)部函數(shù)傳遞到所在的詞法作用域以外柴灯,它都會持有對原始定義作用域的引用卖漫,無論在何處執(zhí)行這個函數(shù)都會使用閉包。
再來一個例子:
function wait(message) {
setTimeout(function timer() {
console.log(message);
}, 1000);
}
wait("Hello, closure!");
將一個內(nèi)部函數(shù)(名為 timer )傳遞給 setTimeout(..) 赠群。 timer 具有涵蓋 wait(..) 作用域的閉包羊始,因此還保有對變量 message 的引用。
wait(..) 執(zhí)行 1000 毫秒后查描,它的內(nèi)部作用域并不會消失突委, timer 函數(shù)依然保有 wait(..) 作用域的閉包柏卤。
深入到引擎的內(nèi)部原理中,內(nèi)置的工具函數(shù) setTimeout(..) 持有對一個參數(shù)的引用匀油,這個參數(shù)也許叫作 fn 或者 func 缘缚,或者其他類似的名字。引擎會調(diào)用這個函數(shù)敌蚜,在例子中就是內(nèi)部的 timer 函數(shù)桥滨,而詞法作用域在這個過程中保持完整。
這就是閉包弛车。
5.2 循環(huán)和閉包
for (var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
}
正常情況下齐媒,我們對這段代碼行為的預(yù)期是分別輸出數(shù)字 1~5,每秒一次纷跛,每次一個喻括。
但實(shí)際上,這段代碼在運(yùn)行時會以每秒一次的頻率輸出五次 6贫奠。
這是為什么唬血?
首先解釋 6 是從哪里來的。這個循環(huán)的終止條件是 i 不再 <=5 唤崭。條件首次成立時 i 的值是 6拷恨。因此,輸出顯示的是循環(huán)結(jié)束時 i 的最終值浩姥。
延遲函數(shù)的回調(diào)會在循環(huán)結(jié)束時才執(zhí)行挑随。事實(shí)上,當(dāng)定時器運(yùn)行時即使每個迭代中執(zhí)行的是 setTimeout(.., 0) 勒叠,所有的回調(diào)函數(shù)依然是在循環(huán)結(jié)束后才會被執(zhí)行兜挨,因此會每次輸出一個 6 出來。
根據(jù)作用域的工作原理眯分,實(shí)際情況是盡管循環(huán)中的五個函數(shù)是在各個迭代中分別定義的拌汇,但是它們都被封閉在一個共享的全局作用域中,因此實(shí)際上只有一個 i 弊决。
這樣可以達(dá)到目的:
for (var i = 1; i <= 5; i++) {
(function (j) {
setTimeout(function timer() {
console.log(j);
}, j * 1000);
})(i);
}
IIFE(立即執(zhí)行函數(shù)表達(dá)式) 會通過聲明并立即執(zhí)行一個函數(shù)來創(chuàng)建作用域噪舀。在迭代內(nèi)使用 IIFE 會為每個迭代都生成一個新的作用域,使得延遲函數(shù)的回調(diào)可以將新的作用域封閉在每個迭代內(nèi)部飘诗,每個迭代中都會含有一個具有正確值的變量供我們訪問与倡。
塊作用域與閉包的結(jié)合
IIFE 在每次迭代時都創(chuàng)建一個新的作用域。換句話說昆稿,每次迭代我們都需要一個塊作用域纺座。 let 聲明,可以用來劫持塊作用域溉潭,并且在這個塊作用域中聲明一個變量净响。本質(zhì)上這是將一個塊轉(zhuǎn)換成一個可以被關(guān)閉的作用域少欺。 因此,下面這段代碼就可正常運(yùn)行了:
for (let i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
}
for 循環(huán)頭部的 let 聲明還會有一個特殊的行為馋贤。這個行為指出變量在循環(huán)過程中不止被聲明一次赞别,每次迭代都會聲明。隨后的每個迭代都會使用上一個迭代結(jié)束時的值來初始化這個變量配乓。
5.3 模塊
模塊模式需要具備兩個必要條件:
必須有外部的封閉函數(shù)仿滔,該函數(shù)必須至少被調(diào)用一次(每次調(diào)用都會創(chuàng)建一個新的模塊實(shí)例)。
封閉函數(shù)必須返回至少一個內(nèi)部函數(shù)扰付,這樣內(nèi)部函數(shù)才能在私有作用域中形成閉包堤撵,并且可以訪問或者修改私有的狀態(tài)仁讨。
5.3.1 現(xiàn)代模塊機(jī)制
var MyModules = (function Manager() {
var modules = {};
function define(name, deps, impl) {
for (var i = 0; i < deps.length; i++) {
deps[i] = modules[deps[i]];
}
modules[name] = impl.apply(impl, deps);
}
function get(name) {
return modules[name];
}
return {
define: define,
get: get,
};
})();
apply 方法能劫持另外一個對象的方法羽莺,繼承另外一個對象的屬性。
使用上面的代碼來定義模塊:
MyModules.define("bar", [], function () {
function hello(who) {
return "Let me introduce: " + who;
}
return {
hello: hello,
};
});
MyModules.define("foo", ["bar"], function (bar) {
var hungry = "hippo";
function awesome() {
console.log(bar.hello(hungry).toUpperCase());
}
return {
awesome: awesome,
};
});
var bar = MyModules.get("bar");
var foo = MyModules.get("foo");
console.log(bar.hello("ctystal"));
foo.awesome();
import 可以將一個模塊中的一個或多個 API 導(dǎo)入到當(dāng)前作用域中洞豁,并分別綁定在一個變量上(在我們的例子里是 hello )盐固。 module 會將整個模塊的 API 導(dǎo)入并綁定到一個變量上(在我們的例子里是 foo 和 bar )。 export 會將當(dāng)前模塊的一個標(biāo)識符(變量丈挟、函數(shù))導(dǎo)出為公共 API刁卜。