想要深入學習JavaScript這門語言,閉包
這個概念幾乎是繞不開的關鍵,今天就讓我們一起好好盤一盤,閉包
到底是什麼東西。
如果是零基礎的小夥伴,可以先看看前一篇文章,幫助你更好的理解本文的內容:
我們先來看看閉包在MDN中的定義:
一個函數和對其周圍狀態(lexical environment,詞法環境)的引用捆綁在一起(或者說函數被引用包圍),這樣的組合就是閉包(closure)
#閉包的定義非常晦澀難懂,如果通俗的解釋一下的話,就是:
這麼講可能還是很抽象,不過我們先有個概念,以下會詳細解釋這個概念。對於閉包,其實還有另一種解釋:
區別於第一種解釋,第二種解釋把函數本身稱為閉包,這樣的解釋也合理,但更準確的話其實我們可以把這個函數稱為閉包函數
。這樣的話,其實兩種解釋都沒有問題,也可以同時存在了。
看完了閉包的概念,我們透過實際的例子感受一下,閉包長什麼樣子?閉包產生的條件?
function outer(){ var a=0; function inner(){ console.log(++a); } return inner; }var f=outer(); f(); //1f(); //2复制代码
我們先來看下這個例子裡,閉包是如何體現的。我們首先定義了outer
函數,然後在outer
函數的內部宣告了一個inner
函數並傳回。
在外部,我們執行outer
函數,並將返回的inner
函數保存在了f
中,現在的f
就是我們說的閉包函數
了。
在範例中,我們執行了兩次f
函數,分別輸出了1
和2
,說明我們在執行f
函數中的console.log( a);
語句時,根據我們先前對作用域的解釋,會去函數所在的作用域鏈上尋找變量,最終在outer
函數中找到了這個變量,並進行了增值操作。
同時要注意的是,我們輸出的結果並不是兩次1
,表示我們的作用域是共享的,可以看成outer
的作用域延伸到了外部。
outer
作用域本來應該在執行結束後釋放,但根據GC
機制(我們這裡先不展開介紹)會發現:
outer
了,但a
變數好像被引用了,而且外部還保存了起來不知道什麼時候呼叫?好收回,那我就先不釋放了。 閉包就這樣誕生了,在作用域消失之後,外部依然可以存取作用域中的變數。
我們根據剛剛的範例和解釋總結一下,產生一個閉包需要哪些條件:
outer
就是外部函數,inner
就是回傳的內部函數。 inner
函數就引用了變數a
。 outer
需要被執行,同時需要把傳回的內部函數給保存起來,也就是我們範例中的var f=outer()
。 只有同時滿足了這三個條件,才能產生閉包。看似很難達成,事實上,我們實際程式碼中經常不經意就能產生閉包。
透過剛剛的介紹,相信大家應該對閉包有了一個概念,接下來就讓我們出發,深入的了解一下閉包的方方面面。
在剛剛的範例中,我們建立了一個閉包,執行多次閉包函數後,增加的都是同一個作用域中的變數a
,那麼我們試試看建立多個閉包會怎麼樣:
function outer() { var a = 0; function inner() { console.log(++a); } return inner; }var f1 = outer();var f2 = outer(); f1(); //1f2(); //1复制代码
这段代码在刚刚的例子上进行了改动,我们执行了两次外部函数outer
,并分别用不同的变量f1
和f2
保存。当执行f1
和f2
时会发现,输出的结果都是1
,说明f1
和f2
的作用域是独立的,f1
和f2
属于两个不同的闭包,我们用一张图来理解下:
当分别创建f1
和f2
时,调用了两次outer
函数,创建了两个不同的上下文。而当f1
和f2
分别执行时,根据作用域的查找规则,会去对应的作用域中查找变量,并执行增值输出,所以最终两个值均为2
;
我们知道,作用域的外部无法拿到作用域内部的值,而通过闭包,我们可以把作用域我们需要的值或者方法暴露出去,比如:
function outer() { var myNumber = 0; function addNumber() { myNumber++; } function getNumber() { return myNumber; } return { addNumber, getNumber } }var module = outer();module.addNumber();module.addNumber();module.getNumber();复制代码
在这个例子中,我们同样定义了一个外部函数outer
,另外还分别定义了两个函数addNumber
和getNumber
,用于增加和获取变量myNumber
。
当我们执行outer()
语句的时候,会创建一个上下文,同时把内部返回的对象保存在module
变量上,此时我们就创建了一个闭包,以及一个包含方法addNumber
和getNumber
的对象。
由于外部是无法直接访问变量myNumber
的,但由于闭包的原因,addNumber
和getNumber
是可以访问到这个变量的,因此我们成功的把变量myNumber
隐藏了起来,并且对外只提供了增加和获取myNumber
值的方法。
通过刚刚的例子,相信大家应该对闭包有了一定了解,接下来我们试着运用闭包来解决实际问题,先看一下例子:
for (var i = 0; i < 2; i++) { setTimeout(() => { console.log(i); }, 500); }复制代码
这是一个十分容易令人误解的例子。接触过的小伙伴肯定都知道,最后会输出两次2
而不是依次输出0
和1
,我们来看看为什么会这样。
首先,外部是一个for
循环,执行了两次语句。
for (var i = 0; i < 2; i++) { ... // 执行两次}复制代码
在函数的内部,我们调用了setTimeout
函数,关键的地方来了,这个函数是一个异步函数,并不会马上执行,所以实际上等外部的for
循环执行结束了,才会真的执行setTimeout
中的函数。还有第二个关键点在于,在for
循环中,var
定义的变量相当于定义在全局,而不存在块级作用域。那么刚刚的代码就可以近似的看成这样了。
var i=0; i++; //i=1i++; //i=2console.log(i);console.log(i);复制代码
非常直接粗暴,但可以很清晰的看出输出结果为何是两次2
了,因为大家共用了同一个作用域,i
的值被覆盖了。那么知道了问题出在哪里,我们试着用上我们刚刚学习的闭包,来创建不同的作用域:
for (var i = 0; i < 2; i++) { function outer() { var j = i; setTimeout(function inner() { console.log(j); }, 500); } outer(); }复制代码
我们按照闭包的样式对刚刚的代码进行了改造,这里的setTimeout
并不直接就是inner
函数,这是因为它在这里起到了定义inner
函数,并保存执行inner
函数的功能。
我们可以看到,最终结果依次输出了0
和1
,说明我们的闭包是成功了的,但这样的闭包比较臃肿,我们试着提高一下,写的更加优雅一点:
for (var i = 0; i < 2; i++) { (function() { //用自执行函数代替了`outer`函数的定义和执行两个步骤 var j = i; setTimeout(function inner() { console.log(j); }, 500); })(); }复制代码
还可以再简化一下:
for (var i = 0; i < 5; i++) { for (var i = 0; i < 2; i++) { (function(j) {//用自执行函数代替了`outer`函数的定义和执行两个步骤 setTimeout(function inner() { console.log(j); }, 500); })(i);//var j=i的步骤,通过传入i值替换 } }复制代码
这样就大功告成了!
本篇首先介绍了闭包的定义以及不同人对闭包的理解,之后介绍了闭包产生的原因并总结了三个条件,之后举例说明了如何创建多个闭包和通过闭包实现模块,最后讲述了如何通过闭包解决for循环中使用异步函数依次输出值的问题。其实闭包没有想象的那么可怕,只要你愿意静下心去探索去了解,闭包也会对你敞开大门~
都看到这里了,如果觉得对你有帮助的话不妨点个赞支持一下呗~
以后会陆续更新更多文章和知识点,感兴趣的话可以关注一波~
如果哪里有错误的地方或者描述不准确的地方,也欢迎大家指出交流~
相关免费学习推荐:javascript(视频)
以上是JavaScript 來好好盤一盤閉包!的詳細內容。更多資訊請關注PHP中文網其他相關文章!