一文帶你深入了解JavaScript中的閉包
閉包(closure)是一個函數(shù)以及其捆綁的周邊環(huán)境狀態(tài)(lexical environment,詞法環(huán)境)的引用的組合。換而言之,閉包讓開發(fā)者可以從內部函數(shù)訪問外部函數(shù)的作用域。在 JavaScript 中,閉包會隨著函數(shù)的創(chuàng)建而被同時創(chuàng)建。
簡言之:閉包就是能夠讀取其他函數(shù)內部變量的函數(shù)。
詞法作用域
示例1:
function init() {
var name = "localName"; // name 是一個被 init 創(chuàng)建的局部變量
function closureName() { // closureName() 是內部函數(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一樣。不同之處在于內部函數(shù)closureName()在執(zhí)行前,從外部函數(shù)返回。
JavaScript中的函數(shù)會形成閉包。閉包是由函數(shù)以及聲明該函數(shù)的詞法環(huán)境組合而成的。該環(huán)境包含了這個閉包創(chuàng)建時作用域內的任何局部變量。示例2中,myFn是執(zhí)行fn時創(chuàng)建的closureName函數(shù)實例的引用。closureName的實例維持了一個對它的詞法環(huán)境(變量name存在其中)的引用。
所以,當myFn被調用時,變量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)境)關聯(lián)起來。類似于面向對象編程。比如對象允許我們將某些數(shù)據(jù)(如對象的屬性)與一個或多個方法相關聯(lián)。
- 當使用只有一個方法的對象的地方,都可以使用閉包。
- 在web中,我們大部分寫的javascript代碼都是基于事件的----定義某種行為,然后將其綁定到用戶觸發(fā)的事件之上。(比如按鈕的點擊事件)。這個代碼通常稱為回調:為響應事件而執(zhí)行的函數(shù)。
比如我們想在頁面上添加調整字體大小的按鈕,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ù)體內。這個環(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閉包都是引用自己詞法作用域內的變量。
在一個閉包內對變量的修改,不會影響到另外一個閉包中的變量。
閉包的用途
總結閉包的用途:
可以讀取函數(shù)內部的變量;
讓變量的值始終保存在內存中。
在循環(huán)中創(chuàng)建閉包:一個場景的錯誤
在引入let,關鍵字之前,在循環(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)在宏任務階段就執(zhí)行了,setTimeout在微任務階段執(zhí)行,此時變量i因為是全局變量,i的值已經(jīng)變?yōu)?了,所以最后打印出來是5個5。
如何解決呢?
1.使用閉包
將setTimeout的內容放置閉包中,循環(huán)執(zhí)行時每個閉包中都有對應的作用域i
for(var i = 0; i < 5; i++) {
(function(i){
setTimeout(() => {
console.log(i)
}, 0)
})(i)
} // 0 1 2 3 42.使用let聲明變量i
for(let i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i)
}, 0)
} // 0 1 2 3 43.使用forEach來遍歷數(shù)組
[0, 1, 2, 3, 4].forEach((i) => {
setTimeout(() => {
console.log(i)
}, 0)
}) // 0 1 2 3 4性能考量
如果不是某些特定任務需要使用閉包,在其他函數(shù)中創(chuàng)建函數(shù)是不明智的,因為閉包在處理速度和內存消耗方面對腳本性能具有負面影響。
例如:在創(chuàng)建新的對象或者類時,方法通常應該關聯(lián)到對象的原型上,而不是定義到對象的構造器中。因為每次構造器被調用時,方法都會被重新賦值一次。(即每創(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;
},
};但是,我們不建議重新定義對應的原型,而是在原型的基礎上去添加方法,修改后如下:
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ù)中的變量都被保存在內存中,內存消耗很大,所以不能濫用閉包,否則會造成網(wǎng)頁的性能問題,在IE中可能導致內存泄露。解決方法是,在退出函數(shù)之前,將不使用的局部變量全部刪除。
閉包會在父函數(shù)外部,改變父函數(shù)內部變量的值。所以,如果你把父函數(shù)當作對象(object)使用,把閉包當作它的公用方法(Public Method),把內部變量當作它的私有屬性(private value),這時一定要小心,不要隨便改變父函數(shù)內部變量的值。
到此這篇關于一文帶你深入了解JavaScript中的閉包的文章就介紹到這了,更多相關JavaScript閉包內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
this,this,再次討論javascript中的this,超全面(經(jīng)典)
在JavaScript中,this 的概念比較復雜。除了在面向對象編程中,this 還是隨處可用的。這篇文章介紹了javascript中的this相關知識,對javascript this相關知識感興趣的朋友一起學習吧2016-01-01
javascript 中的console.log和彈出窗口alert
這篇文章主要介紹了javascript 中的console.log和彈出窗口alert 的相關資料,非常不錯,具有參考借鑒價值,感興趣的朋友參考下吧2016-08-08
基于BootStrap Metronic開發(fā)框架經(jīng)驗小結【三】下拉列表Select2插件的使用
本文主要給大家介紹在編輯頁面中常用到的控件Select2,這個控件可以更加豐富傳統(tǒng)的Select下拉列表控件,提供更多的功能和更好的用戶體驗,2016-05-05

