閉包的概念
如果一個函數通路了此函數的父級及父級以上的作用域變量,那麼這個函數就是一個閉包。
是以以下寫法都是閉包
var a = 1;
// 匿名的立即執行函數,因通路了全局變量a,是以也是一個閉包
(function test (){
alert(a);
})()
本質上,JS中的每個函數都是一個閉包,因為每個函數都可以通路全局變量。
關于JS中的作用域,可以參考部落格:
實作閉包最常見的方式就是函數嵌套(并不是形成閉包的唯一方式!)
function a() {
var i = '初始值';
i = i + "—_執行a"
// 此處的函數b通路了父級函數a中的局部變量i,成為了一個閉包
function b() {
i = i + "_執行b"
console.log(i)
}
return b;
}
var c = a(); // 此時 i 的值為 :初始值—_執行a
c() // 此時 i 的值為 :初始值—_執行a_執行b
c() // 此時 i 的值為 :初始值—_執行a_執行b_執行b
閉包的執行過程
以上方代碼為例:
- 将函數a指派給全局變量c時,a會執行一次,局部變量 i 的值變為
,最終傳回函數b,此時全局變量c的值為閉包函數b的引用。初始值—_執行a
此時函數a雖然已執行完,但因為内部包含閉包函數b,是以函數 a 的執行期上下文會繼續保留在記憶體中,不會被銷毀,是以局部變量 i 仍是
初始值—_執行a
執行期上下文:當函數執行時,會建立一個執行期上下文的内部對象。每調用一次函數,就會建立一個新的上下文對象,他們之間是互相獨立的。當函數執行完畢,它所産生的執行期上下文會被銷毀
- 第一次執行
時,閉包函數b第一次執行,局部變量 i 的值變為c()
初始值—_執行a_執行b
- 第二次執行
時,閉包函數b第二次執行,局部變量 i 的值變為c()
初始值—_執行a_執行b_執行b
閉包的圖解
var a = "global variable";
var F = function () {
var b = "local variable";
var N = function () {
var c = "inner local";
return b;
};
return N;
};
var d = F()
d()
G 為全局作用域

-
全局作用域 G 中有:
—— 函數 F
—— 全局變量 a
—— 全局變量 d (存有對閉包函數 N 的引用)
-
函數 F 中有: (傳回閉包函數N)
—— 函數 F 作用域中的局部變量 b
—— 閉包函數 N
-
閉包函數 N 中有: (傳回局部變量b)
—— 函數 N 作用域中的局部變量 c
閉包的特點
1.被閉包函數通路的父級及以上的函數的局部變量(如範例中的局部變量 i )會一直存在于記憶體中,不會被JS的垃圾回收機制回收。
2.閉包函數實作了對其他函數内部變量的通路。(函數内部的變量對外是無法通路的,閉包通過這種變通的方法,實作了通路。)
Javascript的垃圾回收機制
- 如果一個對象不再被引用,那麼這個對象就會被GC回收。
- 如果兩個對象互相引用,而不再被第三者所引用,那麼這兩個對象都會被回收。
閉包的用途
- 通路函數内部的變量
- 讓變量始終保持在記憶體中
閉包的應用場景
模拟面向對象的代碼風格
比如模拟兩人對話
function person(name) {
function say(content) {
console.log(name + ':' + content)
}
return say
}
a = person("張三")
b = person("李四")
a("在幹啥?")
b("沒幹啥。")
a("出去玩嗎?")
b("去哪啊?")
控制台列印結果為:
張三:在幹啥?
李四:沒幹啥。
張三:出去玩嗎?
李四:去哪啊?
使setTimeout支援傳參
通過閉包實作setTimeout第一個函數傳參(預設不支援傳參)
function func(param){
return function(){
alert(param)
}
}
var f1 = func(1);
setTimeout(f1,1000);
封裝私有變量
//用閉包定義能通路私有函數和私有變量的公有函數。
var counter = (function () {
var privateCounter = 0; //私有變量
function change(val) {
privateCounter += val;
}
return {
increment: function () {
change(1);
},
decrement: function () {
change(-1);
},
value: function () {
return privateCounter;
}
};
})();
console.log(counter.value());//0
counter.increment();
console.log(counter.value());//1
counter.increment();
console.log(counter.value());//2
模拟塊作用域
依次點選 4 個 li 标簽,結果都彈出 4
解析:onclick綁定的function中沒有變量 i,解析引擎會尋找父級作用域,最終找到了全局變量 i,for循環結束時,i 的值已變成了4,是以onclick事件執行時,全都彈出 4。
下面使用閉包來解決這個問題:
var elements = document.getElementsByTagName('li');
var length = elements.length;
for (var i = 0; i < length; i++) {
elements[i].onclick = function (num) {
return function () {
alert(num);
};
}(i);
}
通過匿名閉包,把每次的 i 都儲存到一個變量中,實作了預期效果。
當然,通過 ES6 的 let 可以輕松解決這個問題:
var elements = document.getElementsByTagName('li');
var length = elements.length;
for (let i = 0; i < length; i++) {
elements[i].onclick = function () {
alert(i);
};
}
實作疊代器
function setup(x) {
var i = 0;
return function(){
return x[i++];
};
}
var next = setup(['a', 'b', 'c']);
控制台中的執行效果:
> next();
"a"
> next();
"b"
> next();
"c"
閉包的優點
- 可以減少全局變量的定義,避免全局變量的污染
- 能夠讀取函數内部的變量
- 在記憶體中維護一個變量,可以用做緩存
閉包的缺點
1)造成記憶體洩露
閉包會使函數中的變量一直儲存在記憶體中,記憶體消耗很大,是以不能濫用閉包,否則會造成網頁的性能問題,在IE中可能導緻記憶體洩露。
解決方法——使用完變量後,手動将它指派為null;
2)閉包可能在父函數外部,改變父函數内部變量的值。
3)造成性能損失
由于閉包涉及跨作用域的通路,是以會導緻性能損失。
解決方法——通過把跨作用域變量存儲在局部變量中,然後直接通路局部變量,來減輕對執行速度的影響
閉包的範例
傳回匿名閉包
function funA(){
var a = 10; // funA的活動對象之中;
return function(){ //匿名函數的活動對象;
alert(a);
}
}
var b = funA();
b(); //10
各自獨立的閉包
function outerFn(){
var i = 0;
function innerFn(){
i++;
console.log(i);
}
return innerFn;
}
var inner = outerFn(); //每次外部函數執行的時候,都會開辟一塊記憶體空間,外部函數的位址不同,都會重新建立一個新的位址
inner();
inner();
inner();
var inner2 = outerFn();
inner2();
inner2();
inner2(); //1 2 3 1 2 3
function fn(){
var a = 3;
return function(){
return ++a;
}
}
alert(fn()()); //4
alert(fn()()); //4
通路全局變量的閉包
var i = 0;
function outerFn(){
function innnerFn(){
i++;
console.log(i);
}
return innnerFn;
}
var inner1 = outerFn();
var inner2 = outerFn();
inner1();
inner2();
inner1();
inner2(); //1 2 3 4
寫法有點繞的閉包
(function() {
var m = 0;
function getM() { return m; }
function seta(val) { m = val; }
window.g = getM;
window.f = seta;
})();
f(100);
console.info(g()); //100 閉包找到的是同一位址中父級函數中對應變量最終的值
閉包的鍊式調用
var add = function (x) {
var sum = 1;
var tmp = function (x) {
console.log('執行tmp')
sum = sum + x;
return tmp;
}
tmp.toString = function () {
return sum;
}
return tmp;
}
console.log(add(1)(2)(3).toString())
執行tmp
執行tmp
6
console.log(add(8)(2)(3).toString()) // 最終結果還是 6
留意父函數執行過一次!
function love1(){
var num = 223;
var me1 = function() {
console.log(num);
}
num++;
return me1;
}
var loveme1 = love1();
loveme1(); //輸出224
列印每次鍊式調用的上一次傳參
function fun(n,o) {
console.log(o);
return {
fun:function(m) {
return fun(m,n);
}
};
}
var a = fun(0); //undefined
a.fun(1); //0
a.fun(2); //0
a.fun(3); //0
var b = fun(0).fun(1).fun(2).fun(3); //undefined 0 1 2
var c = fun(0).fun(1);
c.fun(2);
c.fun(3); //undefined 0 1 1