你不知道的JavaScript(五)|作用域和閉包

作用域閉包
當(dāng)函數(shù)可以記住并訪問所在的詞法作用域時(shí)馆蠕,就產(chǎn)生了閉包箕昭,即使函數(shù)式在當(dāng)前詞法作用域之外執(zhí)行。
下面用一些代碼來解釋這個(gè)定義:

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

這段代碼看起來和嵌套作用域中的示例代碼很相似俭令∧窍龋基于詞法作用域的查找規(guī)則,函數(shù)bar()可以訪問外部作用域中的變量a(RHS引用查詢)却音。
這是閉包嗎改抡?
技術(shù)上來講,也許是系瓢。但根據(jù)前面的定義阿纤,確切地說并不是。最準(zhǔn)確地用來解釋bar()對(duì)a的引用的方法是詞法作用域的查找規(guī)則夷陋,而這些規(guī)則只是閉包的一部分欠拾。
下面代碼清晰地展示了閉包:

function foo() {
  var a = 2;
  function bar() {
    console.log( a );
  }
  return bar;
}
var baz = foo();
baz(); // 2 —— 朋友胰锌,這就是閉包的效果。

以上代碼清蚀,函數(shù)bar()的詞法作用域能夠訪問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í)際上只是通過不同的標(biāo)識(shí)符引用調(diào)用了內(nèi)部的函數(shù)bar()。
bar()顯然可以被正常執(zhí)行腹泌。但是在這個(gè)例子中嘶卧,它在自己定義的詞法作用域以外的地方執(zhí)行。
在foo()執(zhí)行后凉袱,通常會(huì)期待foo()的整個(gè)內(nèi)部作用域都被銷毀芥吟,因?yàn)槲覀冎酪嬗欣厥掌饔脕磲尫挪辉偈褂玫膬?nèi)存空間。由于看上去foo()的內(nèi)容不會(huì)再被使用专甩,所以很自然地考慮對(duì)其進(jìn)行回收钟鸵。
而閉包的“神奇”之處正是可以阻止這件事情的發(fā)生。事實(shí)上內(nèi)部作用域依然存在涤躲,因此沒有被回收棺耍。誰在使用這個(gè)內(nèi)部作用域?原來是bar()本身在使用种樱。
拜bar()所聲明的位置所賜蒙袍,它擁有涵蓋foo()內(nèi)部作用域的閉包,使得該作用域能夠一直存活嫩挤,以供bar()在之后任何時(shí)間進(jìn)行引用害幅。
bar()依然持有對(duì)該作用域的引用,而這個(gè)引用就叫做閉包岂昭。
無論使用何種方式對(duì)函數(shù)類型的值進(jìn)行傳遞矫限,當(dāng)函數(shù)在別處被調(diào)用時(shí)都可以觀察到閉包。

function foo() {
  var a = 2;
  function baz() {
    console.log( a ); // 2
  }
  bar( baz );
}
function bar(fn) {
  fn(); // 媽媽快看呀佩抹,這就是閉包叼风!
}

把內(nèi)部函數(shù)baz傳遞給bar,當(dāng)調(diào)用這個(gè)內(nèi)部函數(shù)時(shí)(現(xiàn)在叫fn)棍苹,它涵蓋的foo()內(nèi)部作用域的閉包就可以觀察到了无宿,因?yàn)樗軌蛟L問a。
傳遞函數(shù)也可以是間接的:

var fn;
function foo() {
  var a = 2;
  function baz() {
    console.log( a );
  }
  fn = baz; // 將baz 分配給全局變量
}
function bar() {
  fn(); // 媽媽快看呀枢里,這就是閉包孽鸡!
}
foo();
bar(); // 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巷疼。
我們?cè)噲D假設(shè)循環(huán)中的每個(gè)迭代在運(yùn)行時(shí)都會(huì)給自己“捕獲”一個(gè)i的副本晚胡。但是根據(jù)作用域的工作原理,實(shí)際情況是盡管循環(huán)中的五個(gè)函數(shù)式在各個(gè)迭代中分別定義的嚼沿,但是它們都被封閉在一個(gè)共享的全局作用域中估盘,因此實(shí)際上只有一個(gè)i。
這樣說的話,當(dāng)然所有函數(shù)共享一個(gè)i的引用。循環(huán)結(jié)構(gòu)讓我們誤以為背后還有更復(fù)雜的機(jī)制在起作用疆拘,但實(shí)際上沒有。如果將延遲函數(shù)的回調(diào)重復(fù)定義五次箫踩,完全不使用循環(huán),那它同這段代碼是完全等價(jià)的谭贪。
以上代碼的缺陷是什么班套?我們需要更多的閉包作用域,特別是在循環(huán)的過程中每個(gè)迭代都需要一個(gè)閉包作用域故河。

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

如果改成以上代碼吱韭,也是不行的。的確我們現(xiàn)在擁有了更多的詞法作用域鱼的,每個(gè)延遲函數(shù)都會(huì)將IIFE在每次迭代中創(chuàng)建的作用域封閉起來理盆。但如果作用域是空的,那么僅僅將它們進(jìn)行封閉是不夠的凑阶。仔細(xì)看一下猿规,IIFE只是一個(gè)什么都沒有的空作用域。它需要有自己的變量宙橱,用來在每個(gè)迭代中存儲(chǔ)i的值姨俩,如下:

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

或是:

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

重返塊作用域
仔細(xì)思考我們對(duì)前面的解決方案的分析。我們使用IIFE在每次迭代時(shí)都創(chuàng)建一個(gè)新的作用域师郑。換句話說环葵,每次迭代我們都需要一個(gè)塊作用域。

for (var i=1; i<=5; i++) {
  let j = i; // 是的宝冕,閉包的塊作用域张遭!
  setTimeout( function timer() {
    console.log( j );
  }, j*1000 );
}

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

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

這就是塊作用域和閉包的結(jié)合使用洁闰。

模塊
還有其他的代碼模式利用閉包的強(qiáng)大威力歉甚,但從表面上看,它們似乎與回調(diào)無關(guān)扑眉。

function CoolModule() {
  var something = "cool";
  var another = [1, 2, 3];
  function doSomething() {
    console.log( something );
  }
  function doAnother() {
    console.log( another.join( " ! " ) );
  }
  return {
    doSomething: doSomething,
    doAnother: doAnother
  };
}
var foo = CoolModule();
foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3

這個(gè)模式在JavaScript中被稱為模塊纸泄。最常見的實(shí)現(xiàn)模塊模式的方法通常被稱為模塊暴露,這里展示的是其變體襟雷。
從模塊中返回一個(gè)實(shí)際的對(duì)象并不是必須的刃滓,也可以直接返回一個(gè)內(nèi)部函數(shù)仁烹。JQuery就是一個(gè)很好的例子耸弄。JQuery和$標(biāo)識(shí)符就是JQuery模塊的公共API,但它們本身都是函數(shù)(由于函數(shù)也是對(duì)象卓缰,它們本身也可以擁有屬性)计呈。
如果要更簡單的描述,模塊模式需要具備兩個(gè)必要條件:
1征唬、必須有外部的封裝函數(shù)捌显,該函數(shù)必須至少被調(diào)用一次(每次調(diào)用都會(huì)創(chuàng)建一個(gè)新的模塊實(shí)例)。
2总寒、封裝函數(shù)必須返回至少一個(gè)內(nèi)部函數(shù)扶歪,這樣內(nèi)部函數(shù)才能在私有作用域中形成閉包,并且可以訪問或者修改私有的狀態(tài)摄闸。
一個(gè)具有函數(shù)屬性的對(duì)象本身并不是真正的模塊善镰。從方便觀察的角度看,一個(gè)從函數(shù)調(diào)用所返回的年枕,只有數(shù)據(jù)屬性而沒有閉包函數(shù)的對(duì)象并不是真正的模塊炫欺。
上一個(gè)示例代碼中有一個(gè)叫做CoolModule()的獨(dú)立的模塊創(chuàng)建器,可以被調(diào)用任意多次熏兄,每次調(diào)用都會(huì)創(chuàng)建一個(gè)新的模塊實(shí)例品洛。當(dāng)只需要一個(gè)實(shí)例時(shí),可以對(duì)這個(gè)模式進(jìn)行簡單的改進(jìn)來實(shí)現(xiàn)單例模式:

var foo = (function CoolModule() {
  var something = "cool";
  var another = [1, 2, 3];
  function doSomething() {
    console.log( something );
  }
  function doAnother() {
    console.log( another.join( " ! " ) );
  }
  return {
    doSomething: doSomething,
    doAnother: doAnother
  };
})();
foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3

現(xiàn)代的模塊機(jī)制
大多數(shù)模塊依賴加載器/管理器本質(zhì)上都是將這種模塊定義封裝進(jìn)一個(gè)友好的API摩桶。

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
    };
})();
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("hippo"),
); // Let me introduce: hippo
foo.awesome(); // LET ME INTRODUCE: HIPPO

未來的模塊機(jī)制
ES6中為模塊增加了一級(jí)語法支持桥状。但通過模塊系統(tǒng)進(jìn)行加載時(shí),ES6會(huì)將文件當(dāng)做獨(dú)立的模塊來處理硝清。每個(gè)模塊都可以導(dǎo)入其他模塊或特定的API成員岛宦,同樣也可以導(dǎo)出自己的API成員。


// bar.js
function hello(who) {
    return "Let me introduce: " + who;
}
export hello;
// foo.js
// 僅從"bar" 模塊導(dǎo)入hello()
import hello from "bar";
var hungry = "hippo";
function awesome() {
    console.log(
        hello(hungry).toUpperCase()
    );
}
export awesome;
baz.js
// 導(dǎo)入完整的"foo" 和"bar" 模塊
module foo from "foo";
module bar from "bar";
console.log(
    bar.hello("rhino")
); // Let me introduce: rhino
foo.awesome(); // LET ME INTRODUCE: HIPPO

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变汪。這些操作可以在模塊定義中根據(jù)需要使用任意多次侠坎。

閉上眼睛就是天黑
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市裙盾,隨后出現(xiàn)的幾起案子实胸,更是在濱河造成了極大的恐慌,老刑警劉巖番官,帶你破解...
    沈念sama閱讀 216,496評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件庐完,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡徘熔,警方通過查閱死者的電腦和手機(jī)门躯,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來酷师,“玉大人讶凉,你說我怎么就攤上這事∩娇祝” “怎么了懂讯?”我有些...
    開封第一講書人閱讀 162,632評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長台颠。 經(jīng)常有香客問我褐望,道長,這世上最難降的妖魔是什么串前? 我笑而不...
    開封第一講書人閱讀 58,180評(píng)論 1 292
  • 正文 為了忘掉前任瘫里,我火速辦了婚禮,結(jié)果婚禮上酪呻,老公的妹妹穿的比我還像新娘减宣。我一直安慰自己,他們只是感情好玩荠,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,198評(píng)論 6 388
  • 文/花漫 我一把揭開白布漆腌。 她就那樣靜靜地躺著,像睡著了一般阶冈。 火紅的嫁衣襯著肌膚如雪闷尿。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,165評(píng)論 1 299
  • 那天女坑,我揣著相機(jī)與錄音填具,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛劳景,可吹牛的內(nèi)容都是我干的誉简。 我是一名探鬼主播,決...
    沈念sama閱讀 40,052評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼盟广,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼闷串!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起筋量,我...
    開封第一講書人閱讀 38,910評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤烹吵,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后桨武,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體肋拔,經(jīng)...
    沈念sama閱讀 45,324評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,542評(píng)論 2 332
  • 正文 我和宋清朗相戀三年呀酸,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了凉蜂。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,711評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡七咧,死狀恐怖跃惫,靈堂內(nèi)的尸體忽然破棺而出叮叹,到底是詐尸還是另有隱情艾栋,我是刑警寧澤,帶...
    沈念sama閱讀 35,424評(píng)論 5 343
  • 正文 年R本政府宣布蛉顽,位于F島的核電站蝗砾,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏携冤。R本人自食惡果不足惜悼粮,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,017評(píng)論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望曾棕。 院中可真熱鬧扣猫,春花似錦、人聲如沸翘地。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,668評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽衙耕。三九已至昧穿,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間橙喘,已是汗流浹背时鸵。 一陣腳步聲響...
    開封第一講書人閱讀 32,823評(píng)論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留厅瞎,地道東北人饰潜。 一個(gè)月前我還...
    沈念sama閱讀 47,722評(píng)論 2 368
  • 正文 我出身青樓初坠,卻偏偏與公主長得像,于是被迫代替她去往敵國和親彭雾。 傳聞我的和親對(duì)象是個(gè)殘疾皇子某筐,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,611評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容