精讀《你不知道的JavaScript》(上卷)(一)

前言

《你不知道的JavaScript》是前端必讀書(shū)籍系列之一,有上中下三卷,該篇主要記錄在精讀上卷上半部分(作用域和閉包)時(shí)的知識(shí)點(diǎn),記錄一個(gè)大綱,便于日后查看,反復(fù)閱讀會(huì)對(duì)JS有更深入的理解。

后期會(huì)反復(fù)總結(jié),不定期更新!


1. 作用域和閉包

1.1 編譯原理

  • 傳統(tǒng)編譯語(yǔ)言流程

    1. 分詞/詞法分析

      將由字符組成的字符串分解成(對(duì)編程語(yǔ)言來(lái)說(shuō))有意義的代碼塊,var a = 2;,被分解成 var、a、=、2、;

    2. 解析/語(yǔ)法分析

      將詞法單元流(數(shù)組)轉(zhuǎn)換成一個(gè)由元素逐級(jí)嵌套所組成的代表了程序語(yǔ)法結(jié)構(gòu)的樹(shù)。這個(gè)樹(shù)被稱為“抽象語(yǔ)法樹(shù)(AST)”

    3. 代碼生成

  • javaScript 編譯

    javaScript 的編譯過(guò)程不是發(fā)生在構(gòu)建之前的。對(duì)于 JavaScript 來(lái)說(shuō),大部分情況下編譯發(fā)生在代碼執(zhí)行前的幾微秒(甚至更短)的時(shí)間內(nèi)。

1.2 LHS 查詢 和 RHS 查詢

如果查找的目的是對(duì)變量進(jìn)行賦值,那么就會(huì)使用 LHS 查詢;如果目的是獲取變量的值,就會(huì)使用 RHS 查詢。賦值操作符會(huì)導(dǎo)致 LHS 查詢。 = 操作符或調(diào)用函數(shù)時(shí)傳入?yún)?shù)的操作都會(huì)導(dǎo)致關(guān)聯(lián)作用域的賦值操作。

  • 區(qū)別:變量還沒(méi)有聲明(在任何作用域中都無(wú)法找到該變量)的情況下,這兩種查詢的行為是不一樣的。

    function foo(a) {
      console.log(a + b);
      b = a;
    }
    foo(2);
    

    第一次對(duì) b 進(jìn)行 RHS 查詢時(shí)是無(wú)法找到該變量的。也就是說(shuō),這是一個(gè)“未聲明”的變量,因?yàn)樵谌魏蜗嚓P(guān)的作用域中都無(wú)法找到它。

    如果 RHS 查詢?cè)谒星短椎淖饔糜蛑斜閷げ坏剿璧淖兞浚婢蜁?huì)拋出 ReferenceError 異常。值得注意的是, ReferenceError 是非常重要的異常類型。

    相較之下,當(dāng)引擎執(zhí)行 LHS 查詢時(shí),如果在頂層(全局作用域)中也無(wú)法找到目標(biāo)變量,全局作用域中就會(huì)創(chuàng)建一個(gè)具有該名稱的變量,并將其返還給引擎,前提是程序運(yùn)行在非“嚴(yán)格模式”下。

    ES5 中引入了“嚴(yán)格模式”。同正常模式,或者說(shuō)寬松 / 懶惰模式相比,嚴(yán)格模式在行為上有很多不同。其中一個(gè)不同的行為是嚴(yán)格模式禁止自動(dòng)或隱式地創(chuàng)建全局變量。因此,在嚴(yán)格模式中 LHS 查詢失敗時(shí),并不會(huì)創(chuàng)建并返回一個(gè)全局變量,引擎會(huì)拋出同 RHS 查詢失敗時(shí)類似的 ReferenceError 異常。

    接下來(lái),如果 RHS 查詢找到了一個(gè)變量,但是你嘗試對(duì)這個(gè)變量的值進(jìn)行不合理的操作,比如試圖對(duì)一個(gè)非函數(shù)類型的值進(jìn)行函數(shù)調(diào)用,或著引用 null 或 undefined 類型的值中的屬性,那么引擎會(huì)拋出另外一種類型的異常,叫作 TypeError 。

    ReferenceError 同作用域判別失敗相關(guān),而 TypeError 則代表作用域判別成功了,但是對(duì)結(jié)果的操作是非法或不合理的。

2. 詞法作用域

作用域兩種主要的工作模型: 詞法作用域(最普遍)、動(dòng)態(tài)作用域,js 采用詞法作用域

2.1 詞法階段

作用域查找會(huì)在找到第一個(gè)匹配的標(biāo)識(shí)符時(shí)停止。在多層的嵌套作用域中可以定義同名的標(biāo)識(shí)符,這叫作“遮蔽效應(yīng)”(內(nèi)部的標(biāo)識(shí)符“遮蔽”了外部的標(biāo)識(shí)符)。拋開(kāi)遮蔽效應(yīng),作用域查找始終從運(yùn)行時(shí)所處的最內(nèi)部作用域開(kāi)始,逐級(jí)向外或者說(shuō)向上進(jìn)行,直到遇見(jiàn)第一個(gè)匹配的標(biāo)識(shí)符為止

全局變量會(huì)自動(dòng)成為全局對(duì)象(比如瀏覽器中的 window 對(duì)象)的屬性,因此可以不直接通過(guò)全局對(duì)象的詞法名稱,而是間接地通過(guò)對(duì)全局對(duì)象屬性的引用來(lái)對(duì)其進(jìn)行訪問(wèn)。
window.a
通過(guò)這種技術(shù)可以訪問(wèn)那些被同名變量所遮蔽的全局變量。但非全局的變量如果被遮蔽了,無(wú)論如何都無(wú)法被訪問(wèn)到。

2.2 欺騙詞法

在運(yùn)行時(shí)來(lái)“修改”(也可以說(shuō)欺騙)詞法作用域,JavaScript 中有兩種機(jī)制來(lái)實(shí)現(xiàn)這個(gè)目的。欺騙詞法作用域會(huì)導(dǎo)致性能下降

2.2.1 eval

eval(..) 函數(shù)可以接受一個(gè)字符串為參數(shù),并將其中的內(nèi)容視為好像在書(shū)寫(xiě)時(shí)就存在于程序中這個(gè)位置的代碼

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)行時(shí)有其自己的詞法作用域,意味著其中的聲明無(wú)法修改所在的作用域。

function foo(str) {
  "use strict";
  eval(str);
  console.log(a); // ReferenceError: a is not defined
}
foo("var a = 2");

2.2.2 with

with 可以將一個(gè)沒(méi)有或有多個(gè)屬性的對(duì)象處理為一個(gè)完全隔離的詞法作用域,因此這個(gè)對(duì)象的屬性也會(huì)被處理為定義在這個(gè)作用域中的詞法標(biāo)識(shí)符。

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ù)如果接受了含有一個(gè)或多個(gè)聲明的代碼,就會(huì)修改其所處的詞法作用域,而
with 聲明實(shí)際上是根據(jù)你傳遞給它的對(duì)象憑空創(chuàng)建了一個(gè)全新的詞法作用域。

總結(jié)
使用這其中任何一個(gè)機(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

問(wèn)題: 首先,必須聲明一個(gè)具名函數(shù) foo() ,意味著 foo 這個(gè)名稱本身“污染”了所在作用域(在這個(gè)例子中是全局作用域)。其次,必須顯式地通過(guò)函數(shù)名( foo() )調(diào)用這個(gè)函數(shù)才能運(yùn)行其中的代碼。

var a = 2;
(function foo() {
  // <-- 添加這一行
  var a = 3;
  console.log(a); // 3
})(); // <-- 以及這一行
console.log(a); // 2

函數(shù)會(huì)被當(dāng)作函數(shù)表達(dá)式而不是一個(gè)標(biāo)準(zhǔn)的函數(shù)聲明來(lái)處理。

總結(jié): 區(qū)分函數(shù)聲明和表達(dá)式最簡(jiǎn)單的方法是看 function 關(guān)鍵字出現(xiàn)在聲明中的位置(不僅僅是一行代碼,而是整個(gè)聲明中的位置)。如果 function 是聲明中的第一個(gè)詞,那么就是一個(gè)函數(shù)聲明,否則就是一個(gè)函數(shù)表達(dá)式。

函數(shù)聲明和函數(shù)表達(dá)式之間最重要的區(qū)別是它們的名稱標(biāo)識(shí)符將會(huì)綁定在何處。比較一下前面兩個(gè)代碼片段。第一個(gè)片段中 foo 被綁定在所在作用域中,可以直接通過(guò) foo() 來(lái)調(diào)用它。第二個(gè)片段中 foo 被綁定在函數(shù)表達(dá)式自身的函數(shù)中而不是所在作用域中。

換句話說(shuō), (function foo(){ .. }) 作為函數(shù)表達(dá)式意味著 foo 只能在 .. 所代表的位置中被訪問(wèn),外部作用域則不行。 foo 變量名被隱藏在自身中意味著不會(huì)非必要地污染外部作用域。

3.1.1 匿名和具名

  1. 匿名函數(shù)缺點(diǎn)
  • 匿名函數(shù)在棧追蹤中不會(huì)顯示出有意義的函數(shù)名,使得調(diào)試很困難。
  • 如果沒(méi)有函數(shù)名,當(dāng)函數(shù)需要引用自身時(shí)只能使用已經(jīng)過(guò)期的 arguments.callee 引用,比如在遞歸中。另一個(gè)函數(shù)需要引用自身的例子,是在事件觸發(fā)后事件監(jiān)聽(tīng)器需要解綁自身。
  • 匿名函數(shù)省略了對(duì)于代碼可讀性 / 可理解性很重要的函數(shù)名。一個(gè)描述性的名稱可以讓代碼不言自明。

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ù)作用域是最常見(jiàn)的作用域單元,當(dāng)然也是現(xiàn)行大多數(shù) JavaScript 中最普遍的設(shè)計(jì)
方法, 除 JavaScript 外的很多編程語(yǔ)言都支持塊作用域. 塊作用域是一個(gè)用來(lái)對(duì)之前的最小授權(quán)原則進(jìn)行擴(kuò)展的工具,將代碼從在函數(shù)中隱藏信息擴(kuò)展為在塊中隱藏信息。

3.2.1 with

塊作用域的一種形式

3.2.2 try/catch

try {
  undefined(); // 執(zhí)行一個(gè)非法操作來(lái)強(qiáng)制制造一個(gè)異常
} 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)行的聲明不會(huì)在塊作用域中進(jìn)行提升。聲明的代碼被運(yùn)行之前,聲明并不“存在”。

1. 垃圾收集

另一個(gè)塊作用域非常有用的原因和閉包及回收內(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ù)形成
了一個(gè)覆蓋整個(gè)作用域的閉包,JavaScript 引擎極有可能依然保存著這個(gè)結(jié)構(gòu)(取決于具體
實(shí)現(xiàn))。

  • 使用塊作用域

塊作用域可以打消這種顧慮,可以讓引擎清楚地知道沒(méi)有必要繼續(xù)保存 someReallyBigData 了.

function process(data) {
// 在這里做點(diǎn)有趣的事情
}
// 在這個(gè)塊中定義的內(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)

一個(gè) let 可以發(fā)揮優(yōu)勢(shì)的典型例子就是之前討論的 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)
的每一個(gè)迭代中,確保使用上一個(gè)循環(huán)迭代結(jié)束時(shí)的值重新進(jìn)行賦值。

3.2.4 const

除了 let 以外,ES6 還引入了 const ,同樣可以用來(lái)創(chuàng)建塊作用域變量,但其值是固定的(常量)。之后任何試圖修改值的操作都會(huì)引起錯(cuò)誤。

4. 提升

4.1 變量聲明和函數(shù)聲明的提升

a = 2;
var a;
console.log(a); // 2
console.log(a); // undefined
var a = 2;

引擎會(huì)在解釋 JavaScript 代碼之前首先對(duì)其進(jìn)行編譯。編譯階段中的一部分工作就是找到所有的聲明,并用合適的作用域?qū)⑺鼈冴P(guān)聯(lián)起來(lái)。所以,包括變量和函數(shù)在內(nèi)的所有聲明都會(huì)在任何代碼被執(zhí)行前首先被處理。

分析: var a = 2, JavaScript 實(shí)際上會(huì)將其看成兩個(gè)聲明: var a; 和 a = 2; 。第一個(gè)定義聲明是在編譯階段進(jìn)行的。第二個(gè)賦值聲明會(huì)被留在原地等待執(zhí)行階段。這個(gè)過(guò)程就好像變量和函數(shù)聲明從它們?cè)诖a中出現(xiàn)的位置被“移動(dòng)”到了最上面。這個(gè)過(guò)程就叫作提升。先聲明,后賦值。

foo();
function foo() {
  console.log(a); // undefined
  var a = 2;
}

另外值得注意的是,每個(gè)作用域都會(huì)進(jìn)行提升操作。盡管前面大部分的代碼片段已經(jīng)簡(jiǎn)化了(因?yàn)樗鼈冎话肿饔糜颍?,而我們正在討論?foo(..) 函數(shù)自身也會(huì)在內(nèi)部對(duì) var a 進(jìn)行提升(顯然并不是提升到了整個(gè)程序的最上方)。因此上面的代碼實(shí)際上會(huì)被理解為下面的形式:

function foo() {
  var a;
  console.log(a); // undefined
  a = 2;
}
foo();

可以看到,函數(shù)聲明會(huì)被提升,但是函數(shù)表達(dá)式卻不會(huì)被提升。

foo(); // 不是 ReferenceError, 而是 TypeError!
var foo = function bar() {
  // ...
};

這段程序中的變量標(biāo)識(shí)符 foo() 被提升并分配給所在作用域(在這里是全局作用域),因此 foo() 不會(huì)導(dǎo)致 ReferenceError 。但是 foo 此時(shí)并沒(méi)有賦值(如果它是一個(gè)函數(shù)聲明而不是函數(shù)表達(dá)式,那么就會(huì)賦值)。 foo() 由于對(duì) undefined 值進(jìn)行函數(shù)調(diào)用而導(dǎo)致非法操作,因此拋出 TypeError 異常。

4.2 函數(shù)優(yōu)先

函數(shù)聲明和變量聲明都會(huì)被提升。但是一個(gè)值得注意的細(xì)節(jié)是函數(shù)會(huì)首先被提升,然后才是變量。

foo(); // 1
var foo;
function foo() {
  console.log(1);
}
foo = function () {
  console.log(2);
};

這個(gè)代碼片段會(huì)被引擎理解為如下形式:

function foo() {
  console.log(1);
}
foo(); // 1
foo = function () {
  console.log(2);
};

5. 作用域閉包

當(dāng)函數(shù)可以記住并訪問(wèn)所在的詞法作用域時(shí),就產(chǎn)生了閉包,即使函數(shù)是在當(dāng)前詞法作用域之外執(zhí)行。

5.1 實(shí)質(zhì)問(wèn)題

當(dāng)函數(shù)可以記住并訪問(wèn)所在的詞法作用域時(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() 的詞法作用域能夠訪問(wèn) foo() 的內(nèi)部作用域。然后我們將 bar() 函數(shù)本身當(dāng)作一個(gè)值類型進(jìn)行傳遞。在這個(gè)例子中,我們將 bar 所引用的函數(shù)對(duì)象本身當(dāng)作返回值。

在 foo() 執(zhí)行后,其返回值(也就是內(nèi)部的 bar() 函數(shù))賦值給變量 baz 并調(diào)用 baz() ,實(shí)際上只是通過(guò)不同的標(biāo)識(shí)符引用調(diào)用了內(nèi)部的函數(shù) bar() 。

bar() 顯然可以被正常執(zhí)行。但是在這個(gè)例子中,它在自己定義的詞法作用域以外的地方執(zhí)行。

在 foo() 執(zhí)行后,通常會(huì)期待 foo() 的整個(gè)內(nèi)部作用域都被銷毀,因?yàn)槲覀冎酪嬗欣厥掌饔脕?lái)釋放不再使用的內(nèi)存空間。由于看上去 foo() 的內(nèi)容不會(huì)再被使用,所以很自然地會(huì)考慮對(duì)其進(jìn)行回收。

而閉包的“神奇”之處正是可以阻止這件事情的發(fā)生。事實(shí)上內(nèi)部作用域依然存在,因此沒(méi)有被回收。誰(shuí)在使用這個(gè)內(nèi)部作用域?原來(lái)是 bar() 本身在使用。

拜 bar() 所聲明的位置所賜,它擁有涵蓋 foo() 內(nèi)部作用域的閉包,使得該作用域能夠一直存活,以供 bar() 在之后任何時(shí)間進(jìn)行引用。

bar() 依然持有對(duì)該作用域的引用,而這個(gè)引用就叫作閉包。

var fn;
function foo() {
  var a = 2;
  function baz() {
    console.log(a);
  }
  fn = baz; // 將 baz 分配給全局變量
}
function bar() {
  fn(); // 這就是閉包!
}
foo();
bar(); // 2

無(wú)論通過(guò)何種手段將內(nèi)部函數(shù)傳遞到所在的詞法作用域以外,它都會(huì)持有對(duì)原始定義作用域的引用,無(wú)論在何處執(zhí)行這個(gè)函數(shù)都會(huì)使用閉包。

再來(lái)一個(gè)例子:

function wait(message) {
  setTimeout(function timer() {
    console.log(message);
  }, 1000);
}
wait("Hello, closure!");

將一個(gè)內(nèi)部函數(shù)(名為 timer )傳遞給 setTimeout(..) 。 timer 具有涵蓋 wait(..) 作用域的閉包,因此還保有對(duì)變量 message 的引用。

wait(..) 執(zhí)行 1000 毫秒后,它的內(nèi)部作用域并不會(huì)消失, timer 函數(shù)依然保有 wait(..) 作用域的閉包。

深入到引擎的內(nèi)部原理中,內(nèi)置的工具函數(shù) setTimeout(..) 持有對(duì)一個(gè)參數(shù)的引用,這個(gè)參數(shù)也許叫作 fn 或者 func ,或者其他類似的名字。引擎會(huì)調(diào)用這個(gè)函數(shù),在例子中就是內(nèi)部的 timer 函數(shù),而詞法作用域在這個(gè)過(guò)程中保持完整。

這就是閉包。

5.2 循環(huán)和閉包

for (var i = 1; i <= 5; i++) {
  setTimeout(function timer() {
    console.log(i);
  }, i * 1000);
}

正常情況下,我們對(duì)這段代碼行為的預(yù)期是分別輸出數(shù)字 1~5,每秒一次,每次一個(gè)。

但實(shí)際上,這段代碼在運(yùn)行時(shí)會(huì)以每秒一次的頻率輸出五次 6。

這是為什么?

首先解釋 6 是從哪里來(lái)的。這個(gè)循環(huán)的終止條件是 i 不再 <=5 。條件首次成立時(shí) i 的值是 6。因此,輸出顯示的是循環(huán)結(jié)束時(shí) i 的最終值。

延遲函數(shù)的回調(diào)會(huì)在循環(huán)結(jié)束時(shí)才執(zhí)行。事實(shí)上,當(dāng)定時(shí)器運(yùn)行時(shí)即使每個(gè)迭代中執(zhí)行的是 setTimeout(.., 0) ,所有的回調(diào)函數(shù)依然是在循環(huán)結(jié)束后才會(huì)被執(zhí)行,因此會(huì)每次輸出一個(gè) 6 出來(lái)。

根據(jù)作用域的工作原理,實(shí)際情況是盡管循環(huán)中的五個(gè)函數(shù)是在各個(gè)迭代中分別定義的,但是它們都被封閉在一個(gè)共享的全局作用域中,因此實(shí)際上只有一個(gè) i 。


這樣可以達(dá)到目的:

for (var i = 1; i <= 5; i++) {
  (function (j) {
    setTimeout(function timer() {
      console.log(j);
    }, j * 1000);
  })(i);
}

IIFE(立即執(zhí)行函數(shù)表達(dá)式) 會(huì)通過(guò)聲明并立即執(zhí)行一個(gè)函數(shù)來(lái)創(chuàng)建作用域。在迭代內(nèi)使用 IIFE 會(huì)為每個(gè)迭代都生成一個(gè)新的作用域,使得延遲函數(shù)的回調(diào)可以將新的作用域封閉在每個(gè)迭代內(nèi)部,每個(gè)迭代中都會(huì)含有一個(gè)具有正確值的變量供我們?cè)L問(wèn)。

塊作用域與閉包的結(jié)合

IIFE 在每次迭代時(shí)都創(chuàng)建一個(gè)新的作用域。換句話說(shuō),每次迭代我們都需要一個(gè)塊作用域。 let 聲明,可以用來(lái)劫持塊作用域,并且在這個(gè)塊作用域中聲明一個(gè)變量。本質(zhì)上這是將一個(gè)塊轉(zhuǎn)換成一個(gè)可以被關(guān)閉的作用域。 因此,下面這段代碼就可正常運(yùn)行了:

for (let i = 1; i <= 5; i++) {
  setTimeout(function timer() {
    console.log(i);
  }, i * 1000);
}

for 循環(huán)頭部的 let 聲明還會(huì)有一個(gè)特殊的行為。這個(gè)行為指出變量在循環(huán)過(guò)程中不止被聲明一次,每次迭代都會(huì)聲明。隨后的每個(gè)迭代都會(huì)使用上一個(gè)迭代結(jié)束時(shí)的值來(lái)初始化這個(gè)變量。

5.3 模塊

模塊模式需要具備兩個(gè)必要條件:

  1. 必須有外部的封閉函數(shù),該函數(shù)必須至少被調(diào)用一次(每次調(diào)用都會(huì)創(chuàng)建一個(gè)新的模塊實(shí)例)。

  2. 封閉函數(shù)必須返回至少一個(gè)內(nèi)部函數(shù),這樣內(nèi)部函數(shù)才能在私有作用域中形成閉包,并且可以訪問(wèn)或者修改私有的狀態(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 方法能劫持另外一個(gè)對(duì)象的方法,繼承另外一個(gè)對(duì)象的屬性。
使用上面的代碼來(lái)定義模塊:

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 可以將一個(gè)模塊中的一個(gè)或多個(gè) API 導(dǎo)入到當(dāng)前作用域中,并分別綁定在一個(gè)變量上(在我們的例子里是 hello )。 module 會(huì)將整個(gè)模塊的 API 導(dǎo)入并綁定到一個(gè)變量上(在我們的例子里是 foo 和 bar )。 export 會(huì)將當(dāng)前模塊的一個(gè)標(biāo)識(shí)符(變量、函數(shù))導(dǎo)出為公共 API。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容