一文帶你深入了解JavaScript中的閉包
閉包(closure)是一個函數(shù)以及其捆綁的周邊環(huán)境狀態(tài)(lexical environment,詞法環(huán)境)的引用的組合。換而言之,閉包讓開發(fā)者可以從內(nèi)部函數(shù)訪問外部函數(shù)的作用域。在 JavaScript 中,閉包會隨著函數(shù)的創(chuàng)建而被同時創(chuàng)建。
簡言之:閉包就是能夠讀取其他函數(shù)內(nèi)部變量的函數(shù)。
詞法作用域
示例1:
function init() { var name = "localName"; // name 是一個被 init 創(chuàng)建的局部變量 function closureName() { // closureName() 是內(nèi)部函數(shù),一個閉包 alert(name); // 使用了父函數(shù)中聲明的變量 } closureName(); } init();
上面詞法作用域的例子描述了分析器如何在函數(shù)嵌套的情況下解析變量名。詞法指的是,詞法作用域根據(jù)源代碼中聲明變量的位置來確定該變量在何處可用。嵌套函數(shù)可訪問聲明于它們外部作用域的變量。
示例2:
function fn() { var name = "localName"; function closureName() { alert(name); } return closureName; } var myFn = fn() myFn();
示例2的運行效果和示例1一樣。不同之處在于內(nèi)部函數(shù)closureName()
在執(zhí)行前,從外部函數(shù)返回。
JavaScript中的函數(shù)會形成閉包。閉包是由函數(shù)以及聲明該函數(shù)的詞法環(huán)境組合而成的。該環(huán)境包含了這個閉包創(chuàng)建時作用域內(nèi)的任何局部變量。示例2中,myFn
是執(zhí)行fn
時創(chuàng)建的closureName
函數(shù)實例的引用。closureName
的實例維持了一個對它的詞法環(huán)境(變量name
存在其中)的引用。
所以,當(dāng)myFn
被調(diào)用時,變量name
仍然可以使用。
示例3:
function fn(x) { return function(y) { return x + y; } } var addOne = fn(1) var addTwo = fn(2) console.log(addOne(3)) // 4 console.log(addTwo(3)) // 5
示例3中,我們可以把fn
看作一個函數(shù)工廠,它創(chuàng)建了將指定的值和他的參數(shù)相加求和的函數(shù)。我們使用這個函數(shù)工廠創(chuàng)建了兩個新函數(shù)。一個將其參數(shù)和1求和,一個將其參數(shù)和2求和。
addOne
和addTwo
都是閉包,他們共享相同的函數(shù)定義,但是保存了不同的詞法環(huán)境。在addOne
的詞法環(huán)境中,x
是1。addTwo
中,x
是2。
使用閉包場景
閉包允許將函數(shù)與其所操作的某些數(shù)據(jù)(環(huán)境)關(guān)聯(lián)起來。類似于面向?qū)ο缶幊?。比如對象允許我們將某些數(shù)據(jù)(如對象的屬性)與一個或多個方法相關(guān)聯(lián)。
- 當(dāng)使用只有一個方法的對象的地方,都可以使用閉包。
- 在web中,我們大部分寫的javascript代碼都是基于事件的----定義某種行為,然后將其綁定到用戶觸發(fā)的事件之上。(比如按鈕的點擊事件)。這個代碼通常稱為回調(diào):為響應(yīng)事件而執(zhí)行的函數(shù)。
比如我們想在頁面上添加調(diào)整字體大小的按鈕,size1
和size2
就是閉包。
示例4:
<button id="btn1">size12</button> <button id="btn2">size24</button> <p>hello</p> <script> function setFontSize(num) { return function() { return num + 'px'; } } var size1 = setFontSize(12) var size2 = setFontSize(24) document.getElementById("btn1").onclick = () => { let pdom = document.getElementsByTagName("p") pdom[0].style.fontSize = size1() } document.getElementById("btn2").onclick = () => { let pdom = document.getElementsByTagName("p") pdom[0].style.fontSize = size2() } </script>
用閉包模擬私有方法
使用閉包來模擬私有方法,私有方法不僅僅有利于限制對代碼的訪問,還提供了管理全局命名空間的強大能力,避免非核心的方法弄亂了代碼的公共接口部分。
示例5:使用閉包定義公共函數(shù),并令其可以訪問私有函數(shù)和變量
var Counter = (function () { var privateCounter = 0 function operateCounter(val) { privateCounter += val } return { increment: function () { operateCounter(1); }, decrement: function () { operateCounter(-1); }, value: function () { return privateCounter; } } })() console.log(Counter.value()) Counter.increment() Counter.increment() console.log(Counter.value()) Counter.decrement() console.log(Counter.value())
之前的示例中,每個閉包都有自己的詞法環(huán)境。示例5只創(chuàng)建了一個詞法環(huán)境,為3個函數(shù)共享:Counter.increment
,Counter.decrement
和 Counter.value
。
示例5的共享環(huán)境創(chuàng)建于一個立即執(zhí)行的匿名函數(shù)體內(nèi)。這個環(huán)境中包含兩個私有項:名為 privateCounter
的變量和名為 operateCounter
的函數(shù)。這兩項都無法在這個匿名函數(shù)外部直接訪問。必須通過匿名函數(shù)返回的三個公共函數(shù)訪問。
這三個公共函數(shù)共享同一個環(huán)境的閉包。多虧 JavaScript 的詞法作用域,它們都可以訪問 privateCounter
變量和 operateCounter
函數(shù)。
注意:示例5中,我們定義了一個立即執(zhí)行的匿名函數(shù),并賦值給Counter
,那么我們也可以把這個匿名函數(shù)不立即執(zhí)行,賦值給變量makeCounter
,這樣就能創(chuàng)建多個計數(shù)器。
var makeCounter = (function () { var privateCounter = 0 function operateCounter(val) { privateCounter += val } return { increment: function () { operateCounter(1); }, decrement: function () { operateCounter(-1); }, value: function () { return privateCounter; } } }) var Counter1 = makeCounter(); var Counter2 = makeCounter();
Counter1
和Counter2
閉包都是引用自己詞法作用域內(nèi)的變量。
在一個閉包內(nèi)對變量的修改,不會影響到另外一個閉包中的變量。
閉包的用途
總結(jié)閉包的用途:
可以讀取函數(shù)內(nèi)部的變量;
讓變量的值始終保存在內(nèi)存中。
在循環(huán)中創(chuàng)建閉包:一個場景的錯誤
在引入let
,關(guān)鍵字之前,在循環(huán)中創(chuàng)建閉包有一個常見的問題。
for(var i = 0; i < 5; i++) { setTimeout(() => { console.log(i) }, 0) } // 5 5 5 5 5
我們想輸出0 1 2 3 4,確發(fā)現(xiàn)打印了5個5出來,為什么?
for循環(huán)在宏任務(wù)階段就執(zhí)行了,setTimeout
在微任務(wù)階段執(zhí)行,此時變量i
因為是全局變量,i
的值已經(jīng)變?yōu)?了,所以最后打印出來是5個5。
如何解決呢?
1.使用閉包
將setTimeout
的內(nèi)容放置閉包中,循環(huán)執(zhí)行時每個閉包中都有對應(yīng)的作用域i
for(var i = 0; i < 5; i++) { (function(i){ setTimeout(() => { console.log(i) }, 0) })(i) } // 0 1 2 3 4
2.使用let聲明變量i
for(let i = 0; i < 5; i++) { setTimeout(() => { console.log(i) }, 0) } // 0 1 2 3 4
3.使用forEach
來遍歷數(shù)組
[0, 1, 2, 3, 4].forEach((i) => { setTimeout(() => { console.log(i) }, 0) }) // 0 1 2 3 4
性能考量
如果不是某些特定任務(wù)需要使用閉包,在其他函數(shù)中創(chuàng)建函數(shù)是不明智的,因為閉包在處理速度和內(nèi)存消耗方面對腳本性能具有負(fù)面影響。
例如:在創(chuàng)建新的對象或者類時,方法通常應(yīng)該關(guān)聯(lián)到對象的原型上,而不是定義到對象的構(gòu)造器中。因為每次構(gòu)造器被調(diào)用時,方法都會被重新賦值一次。(即每創(chuàng)建一個對象,方法都會被重新賦值)
比如:
function CreateObject(name, message) { this.name = name.toString(); this.message = message.toString(); this.getName = function () { return this.name; }; this.getMessage = function () { return this.message; }; }
上面代碼中,并沒有利用到閉包的好處。因此可以避免使用閉包。修改后如下:
function CreateObject(name, message) { this.name = name.toString(); this.message = message.toString(); } CreateObject.prototype = { getName() { return this.name; }, getMessage() { return this.message; }, };
但是,我們不建議重新定義對應(yīng)的原型,而是在原型的基礎(chǔ)上去添加方法,修改后如下:
function CreateObject(name, message) { this.name = name.toString(); this.message = message.toString(); } CreateObject.prototype.getName = function () { return this.name; }; CreateObject.prototype.getMessage = function () { return this.message; };
上面示例中,繼承的原型可以被所有對象共享,而不比在每一次創(chuàng)建對象時去定義方法。
使用閉包注意點
由于閉包會使得函數(shù)中的變量都被保存在內(nèi)存中,內(nèi)存消耗很大,所以不能濫用閉包,否則會造成網(wǎng)頁的性能問題,在IE中可能導(dǎo)致內(nèi)存泄露。解決方法是,在退出函數(shù)之前,將不使用的局部變量全部刪除。
閉包會在父函數(shù)外部,改變父函數(shù)內(nèi)部變量的值。所以,如果你把父函數(shù)當(dāng)作對象(object)使用,把閉包當(dāng)作它的公用方法(Public Method),把內(nèi)部變量當(dāng)作它的私有屬性(private value),這時一定要小心,不要隨便改變父函數(shù)內(nèi)部變量的值。
到此這篇關(guān)于一文帶你深入了解JavaScript中的閉包的文章就介紹到這了,更多相關(guān)JavaScript閉包內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
axios?POST提交數(shù)據(jù)的三種請求方式寫法示例
這篇文章主要介紹了axios?POST提交數(shù)據(jù)的三種請求方式寫法示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-09-09Bootstrap3 內(nèi)聯(lián)單選和多選框
這篇文章主要介紹了Bootstrap3 內(nèi)聯(lián)單選和多選框的相關(guān)資料,非常不錯,具有參考借鑒價值,需要的朋友可以參考下2016-12-12關(guān)于ckeditor在bootstrap中modal中彈框無法輸入的解決方法
今天小編就為大家分享一篇關(guān)于ckeditor在bootstrap中modal中彈框無法輸入的解決方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2019-09-09TypeScript里string和String的區(qū)別
這篇文章主要介紹了TypeScript里string和String的區(qū)別,真的不止是大小寫的區(qū)別,string表示原生類型,而String表示對象,下文更多詳細(xì)內(nèi)容需要的小伙伴可以參考一下2022-03-03