深入淺出Javascript閉包

來源 |?http://www.fly63.com/article/detial/10081
一、引子
閉包(closure)是 JavaScript 語言的一個(gè)難點(diǎn),面試時(shí)常被問及,也是它的特色,很多高級(jí)應(yīng)用都要依靠閉包實(shí)現(xiàn)。本文盡可能用簡單易懂的話,講清楚閉包的概念、形成條件及其常見的面試題。
我們先來看一個(gè)例子:
var n = 999;function f1() {console.log(n);}f1()?//?999
上面代碼中,函數(shù)f1可以讀取全局變量n。但是,函數(shù)外部無法讀取函數(shù)內(nèi)部聲明的變量。
function f1() {var n = 999;}console.log(n)// Uncaught ReferenceError: n is not defined
function f1() {var n = 999;function f2() {console.log(n); // 999}}
上面代碼中,函數(shù)f2就在函數(shù)f1內(nèi)部,這時(shí)f1內(nèi)部的所有局部變量,對(duì)f2都是可見的。既然f2可以讀取f1的局部變量,那么只要把f2作為返回值,我們不就可以在f1外部讀取它的內(nèi)部變量了嗎!
二、閉包是什么
我們可以對(duì)上面代碼進(jìn)行如下修改:
function f1(){var a = 999;function f2(){console.log(a);}return f2; // f1返回了f2的引用}var result = f1(); // result就是f2函數(shù)了result(); // 執(zhí)行result,全局作用域下沒有a的定義,//但是函數(shù)閉包,能夠把定義函數(shù)的時(shí)候的作用域一起記住,輸出999
上面代碼中,函數(shù)f1的返回值就是函數(shù)f2,由于f2可以讀取f1的內(nèi)部變量,所以就可以在外部獲得f1的內(nèi)部變量了。
閉包就是函數(shù)f2,即能夠讀取其他函數(shù)內(nèi)部變量的函數(shù)。由于在JavaScript語言中,只有函數(shù)內(nèi)部的子函數(shù)才能讀取內(nèi)部變量,因此可以把閉包簡單理解成“定義在一個(gè)函數(shù)內(nèi)部的函數(shù)”。
閉包最大的特點(diǎn),就是它可以“記住”誕生的環(huán)境,比如f2記住了它誕生的環(huán)境f1,所以從f2可以得到f1的內(nèi)部變量。在本質(zhì)上,閉包就是將函數(shù)內(nèi)部和函數(shù)外部連接起來的一座橋梁。
那到底什么是閉包呢?
當(dāng)函數(shù)可以記住并訪問所在的詞法作用域,即使函數(shù)是在當(dāng)前詞法作用域之外執(zhí)行,這就產(chǎn)生了閉包。?----《你不知道的Javascript上卷》
我個(gè)人理解,閉包就是函數(shù)中的函數(shù)(其他語言不能函數(shù)再套函數(shù)),里面的函數(shù)可以訪問外面函數(shù)的變量,外面的變量的是這個(gè)內(nèi)部函數(shù)的一部分。
閉包形成的條件
函數(shù)嵌套
內(nèi)部函數(shù)引用外部函數(shù)的局部變量
三、閉包的特性
每個(gè)函數(shù)都是閉包,每個(gè)函數(shù)天生都能夠記憶自己定義時(shí)所處的作用域環(huán)境。把一個(gè)函數(shù)從它定義的那個(gè)作用域,挪走,運(yùn)行。這個(gè)函數(shù)居然能夠記憶住定義時(shí)的那個(gè)作用域。不管函數(shù)走到哪里,定義時(shí)的作用域就帶到了哪里。接下來我們用兩個(gè)例子來說明這個(gè)問題:
//例題1var inner;function outer(){var a=250;inner=function(){alert(a);//這個(gè)函數(shù)雖然在外面執(zhí)行,但能夠記憶住定義時(shí)的那個(gè)作用域,a是250}}outer();var a=300;inner();//一個(gè)函數(shù)在執(zhí)行的時(shí)候,找閉包里面的變量,不會(huì)理會(huì)當(dāng)前作用域。
//例題2function outer(x){function inner(y){console.log(x+y);}return inner;}var inn=outer(3);//數(shù)字3傳入outer函數(shù)后,inner函數(shù)中x便會(huì)記住這個(gè)值inn(5);//當(dāng)inner函數(shù)再傳入5的時(shí)候,只會(huì)對(duì)y賦值,所以最后彈出8
四、閉包的內(nèi)存泄漏
棧內(nèi)存提供一個(gè)執(zhí)行環(huán)境,即作用域,包括全局作用域和私有作用域,那他們什么時(shí)候釋放內(nèi)存的?
全局作用域----只有當(dāng)頁面關(guān)閉的時(shí)候全局作用域才會(huì)銷毀
私有的作用域----只有函數(shù)執(zhí)行才會(huì)產(chǎn)生
一般情況下,函數(shù)執(zhí)行會(huì)形成一個(gè)新的私有的作用域,當(dāng)私有作用域中的代碼執(zhí)行完成后,我們當(dāng)前作用域都會(huì)主動(dòng)的進(jìn)行釋放和銷毀。但當(dāng)遇到函數(shù)執(zhí)行返回了一個(gè)引用數(shù)據(jù)類型的值,并且在函數(shù)的外面被一個(gè)其他的東西給接收了,這種情況下一般形成的私有作用域都不會(huì)銷毀。
如下面這種情況:
function fn(){var num=100;return function(){}}var?f=fn();//fn執(zhí)行形成的這個(gè)私有的作用域就不能再銷毀了
也就是像上面這段代碼,fn函數(shù)內(nèi)部的私有作用域會(huì)被一直占用的,發(fā)生了內(nèi)存泄漏。所謂內(nèi)存泄漏指任何對(duì)象在您不再擁有或需要它之后仍然存在。閉包不能濫用,否則會(huì)導(dǎo)致內(nèi)存泄露,影響網(wǎng)頁的性能。閉包使用完了后,要立即釋放資源,將引用變量指向null。
接下來我們看下有關(guān)于內(nèi)存泄漏的一道經(jīng)典面試題:
function outer(){var num=0;//內(nèi)部變量return function add(){//通過return返回add函數(shù),就可以在outer函數(shù)外訪問了num++;//內(nèi)部函數(shù)有引用,作為add函數(shù)的一部分了console.log(num);};}var func1=outer();func1();//實(shí)際上是調(diào)用add函數(shù), 輸出1func1();//輸出2 因?yàn)閛uter函數(shù)內(nèi)部的私有作用域會(huì)一直被占用var func2=outer();func2();// 輸出1 每次重新引用函數(shù)的時(shí)候,閉包是全新的。func2();// 輸出2
五、閉包的作用
1.可以讀取函數(shù)內(nèi)部的變量。
2.可以使變量的值長期保存在內(nèi)存中,生命周期比較長。因此不能濫用閉包,否則會(huì)造成網(wǎng)頁的性能問題
3.可以用來實(shí)現(xiàn)js模塊。
js模塊:具有特定功能的js文件,將所有的數(shù)據(jù)和功能都封裝在一個(gè)函數(shù)內(nèi)部(私有的),只向外暴露一個(gè)包信n個(gè)方法的對(duì)象或函數(shù),模塊的使用者,只需要通過模塊暴露的對(duì)象調(diào)用方法來實(shí)現(xiàn)對(duì)應(yīng)的功能。
具體請(qǐng)看下面的例子:
//index.html文件
//myModule.js文件(function () {var msg = 'Beijing'//私有數(shù)據(jù)//操作數(shù)據(jù)的函數(shù)function doSomething() {console.log('doSomething() '+msg.toUpperCase())}function doOtherthing () {console.log('doOtherthing() '+msg.toLowerCase())}//向外暴露對(duì)象(給外部使用的兩個(gè)方法)window.myModule2 = {doSomething: doSomething,doOtherthing: doOtherthing}})()
六、閉包的運(yùn)用
我們要實(shí)現(xiàn)這樣的一個(gè)需求: 點(diǎn)擊某個(gè)按鈕, 提示"點(diǎn)擊的是第n個(gè)按鈕",此處我們先不用事件代理:
.....
萬萬沒想到,點(diǎn)擊任意一個(gè)按鈕,后臺(tái)都是彈出“第四個(gè)”,這是因?yàn)閕是全局變量,執(zhí)行到點(diǎn)擊事件時(shí),此時(shí)i的值為3。那該如何修改,最簡單的是用let聲明i
for (let i = 0; i < btns.length; i++) {btns[i].onclick = function () {console.log('第' + (i + 1) + '個(gè)')}????}
另外我們可以通過閉包的方式來修改:
for (var i = 0; i < btns.length; i++) {(function (j) {btns[j].onclick = function () {console.log('第' + (j + 1) + '個(gè)')}})(i)}

