闭包详解一
在正式學習閉包之前,請各位同學一定要確保自己對詞法作用域已經(jīng)非常的熟悉了,如果對詞法作用域還不夠熟悉的話,可以先看:
- 深入理解閉包之前置知識---作用域與詞法作用域
前言
現(xiàn)在去面試前端開發(fā)的崗位,如果你對面試官也是個前端,并且不是太水的話,你有很大的概率會被問到JavaScript中的閉包。因為這個閉包這個知識點真的很重要,還非常難掌握。
什么是閉包
什么是閉包,你可能會搜出很多答案....
《JavaScript高級程序設計》這樣描述:
閉包是指有權訪問另一個函數(shù)作用域中的變量的函數(shù);
《JavaScript權威指南》這樣描述:
從技術的角度講,所有的JavaScript函數(shù)都是閉包:它們都是對象,它們都關聯(lián)到作用域鏈。
《你不知道的JavaScript》這樣描述:
當函數(shù)可以記住并訪問所在的詞法作用域時,就產(chǎn)生了閉包,即使函數(shù)是在當前詞法作用域之外執(zhí)行。
我最認同的是《你不知道的JavaScript》中的描述,雖然前面的兩種說法都沒有錯,但閉包應該是基于詞法作用域書寫代碼時產(chǎn)生的自然結果,是一種現(xiàn)象!你也不用為了利用閉包而特意的創(chuàng)建,因為閉包的在你的代碼中隨處可見,只是你還不知道當時你寫的那一段代碼其實就產(chǎn)生了閉包。
講解閉包
上面已經(jīng)說到,當函數(shù)可以記住并訪問所在的詞法作用域時,就產(chǎn)生了閉包,即使函數(shù)是在當前詞法作用域之外執(zhí)行。
看一段代碼
function fn1() {var name = 'iceman';function fn2() {console.log(name);}fn2(); } fn1(); 復制代碼如果是根據(jù)《JavaScript高級程序設計》和《JavaScript權威指南》來說,上面的代碼已經(jīng)產(chǎn)生閉包了。fn2訪問到了fn1的變量,滿足了條件“有權訪問另一個函數(shù)作用域中的變量的函數(shù)”,fn2本身是個函數(shù),所以滿足了條件“所有的JavaScript函數(shù)都是閉包”。
這的確是閉包,但是這種方式定義的閉包不太好觀察。
再看一段代碼:
function fn1() {var name = 'iceman';function fn2() {console.log(name);}return fn2; } var fn3 = fn1(); fn3(); 復制代碼這樣就清晰地展示了閉包:
-
fn2的詞法作用域能訪問fn1的作用域
-
將fn2當做一個值返回
-
fn1執(zhí)行后,將fn2的引用賦值給fn3
-
執(zhí)行fn3,輸出了變量name
我們知道通過引用的關系,fn3就是fn2函數(shù)本身。執(zhí)行fn3能正常輸出name,這不就是fn2能記住并訪問它所在的詞法作用域,而且fn2函數(shù)的運行還是在當前詞法作用域之外了。
正常來說,當fn1函數(shù)執(zhí)行完畢之后,其作用域是會被銷毀的,然后垃圾回收器會釋放那段內(nèi)存空間。而閉包卻很神奇的將fn1的作用域存活了下來,fn2依然持有該作用域的引用,這個引用就是閉包。
總結:某個函數(shù)在定義時的詞法作用域之外的地方被調用,閉包可以使該函數(shù)極限訪問定義時的詞法作用域。
注意:對函數(shù)值的傳遞可以通過其他的方式,并不一定值有返回該函數(shù)這一條路,比如可以用回調函數(shù):
function fn1() {var name = 'iceman';function fn2() {console.log(name);}fn3(fn2); } function fn3(fn) {fn(); } fn1(); 復制代碼本例中,將內(nèi)部函數(shù)fn2傳遞給fn3,當它在fn3中被運行時,它是可以訪問到name變量的。
所以無論通過哪種方式將內(nèi)部的函數(shù)傳遞到所在的詞法作用域以外,它都回持有對原始作用域的引用,無論在何處執(zhí)行這個函數(shù)都會使用閉包。
再次解釋閉包
以上的例子會讓人覺得有點學院派了,但是閉包絕不僅僅是一個無用的概念,你寫過的代碼當中肯定有閉包的身影,比如類似如下的代碼:
function waitSomeTime(msg, time) {setTimeout(function () {console.log(msg)}, time); } waitSomeTime('hello', 1000); 復制代碼定時器中有一個匿名函數(shù),該匿名函數(shù)就有涵蓋waitSomeTime函數(shù)作用域的閉包,因此當1秒之后,該匿名函數(shù)能輸出msg。
另一個很經(jīng)典的例子就是for循環(huán)中使用定時器延遲打印的問題:
for (var i = 1; i <= 10; i++) {setTimeout(function () {console.log(i);}, 1000); } 復制代碼在這段代碼中,我們對其的預期是輸出1~10,但卻輸出10次11。這是因為setTimeout中的匿名函數(shù)執(zhí)行的時候,for循環(huán)都已經(jīng)結束了,for循環(huán)結束的條件是i大于10,所以當然是輸出10次11咯。
究其原因:i是聲明在全局作用中的,定時器中的匿名函數(shù)也是執(zhí)行在全局作用域中,那當然是每次都輸出11了。
原因知道了,解決起來就簡單了,我們可以讓i在每次迭代的時候,都產(chǎn)生一個私有的作用域,在這個私有的作用域中保存當前i的值。
for (var i = 1; i <= 10; i++) {(function () {var j = i;setTimeout(function () {console.log(j);}, 1000);})(); } 復制代碼這樣就達到我們的預期了呀,讓我們用一種比較優(yōu)雅的寫法改造一些,將每次迭代的i作為實參傳遞給自執(zhí)行函數(shù),自執(zhí)行函數(shù)中用變量去接收:
for (var i = 1; i <= 10; i++) {(function (j) {setTimeout(function () {console.log(j);}, 1000);})(i); } 復制代碼閉包的應用
閉包的應用比較典型是定義模塊,我們將操作函數(shù)暴露給外部,而細節(jié)隱藏在模塊內(nèi)部:
function module() {var arr = [];function add(val) {if (typeof val == 'number') {arr.push(val);}}function get(index) {if (index < arr.length) {return arr[index]} else {return null;}}return {add: add,get: get} } var mod1 = module(); mod1.add(1); mod1.add(2); mod1.add('xxx'); console.log(mod1.get(2)); 復制代碼注
關于閉包還有很多要講,這里先講解比較基礎的概念,接下來還會有更精彩的內(nèi)容。
特別注意
可以關注我的公眾號:icemanFE,接下來持續(xù)更新技術文章!
總結
- 上一篇: 进程通信之 Binder 机制浅析
- 下一篇: 【Unity】Protobuf的使用与常