網(wǎng)上關(guān)于閉包的文章很多姜贡,各種專(zhuān)業(yè)文獻(xiàn)上的“閉包”定義非常抽象功舀,很難看懂。官方對(duì)閉包的解釋是:一個(gè)擁有許多變量和綁定了這些變量的環(huán)境的表達(dá)式毫别,還是看不懂娃弓?
不著急,引用《JavaScript權(quán)威指南》英文原版對(duì)閉包的定義如下:
This combination of a function object and a scope (a set of variable bindings) in which the function’s variables are resolved is called a closure in the computer science literature.
個(gè)人翻譯為:閉包指的是一個(gè)函數(shù)和該函數(shù)能訪問(wèn)和引用的所有變量對(duì)象(也叫作用域?qū)ο螅┑募?/strong>岛宦。
我覺(jué)的也可以這么理解:一個(gè)閉包就是當(dāng)一個(gè)函數(shù)被其包含作用域外部引用時(shí)台丛,一個(gè)沒(méi)有釋放資源的棧區(qū),棧區(qū)包含該函數(shù)本身和函數(shù)所引用的作用域鏈上的變量對(duì)象砾肺。
所以挽霉,在Javascript中防嗡,從不同角度看有兩種形式:
1)從理論角度看,所有的函數(shù)都是閉包侠坎,包括全局函數(shù)蚁趁,因?yàn)樗麄兌加凶约旱淖兞繉?duì)象和作用域鏈,訪問(wèn)全局變量就相當(dāng)于訪問(wèn)最外層的作用域?qū)ο?br>
2)從實(shí)踐角度看硅蹦,閉包函數(shù)一般有以下特點(diǎn)的:創(chuàng)建它們的上下文(比如父函數(shù)執(zhí)行環(huán)境)已經(jīng)銷(xiāo)毀荣德,它們?nèi)匀淮嬖诓⒈灰茫疫€引用了其作用域鏈里其他作用域?qū)ο蟮淖兞俊?br>
最簡(jiǎn)單的就像這樣:
function F_func() {
var num = 42;
var S_func = functio() {
return ++num;
}
return S_func;
}
var add = F_func();
當(dāng)一個(gè)或多個(gè)內(nèi)部函數(shù)被包含函數(shù)返回童芹,而內(nèi)部函數(shù)引用了其他作用域的變量涮瞻,當(dāng)內(nèi)部函數(shù)在包含函數(shù)之外被調(diào)用,我們就可以稱(chēng)它們構(gòu)成了閉包假褪。
一署咽、閉包的原理
1、外部函數(shù)在調(diào)用時(shí)會(huì)創(chuàng)建自身的執(zhí)行環(huán)境生音、變量對(duì)象和作用域鏈宁否,調(diào)用結(jié)束后這些回收,但是函數(shù)的變量對(duì)象若有變量被閉包函數(shù)調(diào)用缀遍,則基于Js的垃圾回收機(jī)制慕匠,函數(shù)調(diào)用結(jié)束后其執(zhí)行環(huán)境、作用域鏈將銷(xiāo)毀域醇,變量對(duì)象卻不會(huì)立即回收台谊,而是在閉包函數(shù)的調(diào)用鏈里;
2譬挚、每調(diào)用一次外部函數(shù)就會(huì)創(chuàng)建自身的執(zhí)行環(huán)境锅铅、變量對(duì)象等操作,便產(chǎn)生一個(gè)新的閉包减宣,以前的閉包依舊存在且互不影響盐须;
3、同一個(gè)閉包會(huì)保留上一次的狀態(tài)漆腌,當(dāng)它被再次調(diào)用時(shí)會(huì)在同一作用域鏈上進(jìn)行贼邓;
還是用上面的例子說(shuō)明
var name = 'scope';
function F_func() {
var num = 42;
function S_func() {
var t = 1;
num += t;
return num;
}
return S_func;
}
var a = F_func();
a(); // 43
a(); // 44
var b = F_func(); // 重新調(diào)用F_func(),產(chǎn)生新的閉包
b(); // 43
a = null; // 通知回收
b(); // 44
大概的執(zhí)行過(guò)程(其中作用鏈以指針關(guān)聯(lián),vo-指代變量對(duì)象):
- 作用域鏈的各個(gè)變量對(duì)象以指針關(guān)聯(lián)
- vo-指代變量對(duì)象
1)進(jìn)入全局上下文闷尿,創(chuàng)建全局執(zhí)行環(huán)境塑径、全局變量對(duì)象和作用域鏈,入棧
//對(duì)應(yīng)作用域鏈
global ---> global-vo {
name: 'scope',
F_func: Function,
a: undefined,
b: undefined
}
2)第一次執(zhí)行F_func函數(shù)悠砚,創(chuàng)建F_func執(zhí)行環(huán)境晓勇、變量對(duì)象和作用域鏈堂飞,入棧
3)執(zhí)行F_func函數(shù)灌旧,初始化上下文绑咱、變量對(duì)象和內(nèi)部函數(shù)的作用域鏈
//對(duì)應(yīng)作用域鏈
F_func ----- F_func-vo { ----- global-vo {
num: 42, name: 'scope',
S_func F_func,
} a: Function,
| b: undefined
| }
|
S_func ----- S_func-vo {
t: 1
}
4)F_func函數(shù)執(zhí)行完畢枢泰,變量a引用返回的內(nèi)部函數(shù)S_func描融,F(xiàn)_func的執(zhí)行環(huán)境和作用域鏈出棧并銷(xiāo)毀
//對(duì)應(yīng)作用域鏈
F_func-vo { ----- global-vo {
num: 42, name: 'scope',
S_func F_func,
} a: Function,
| b: undefined
| }
|
a=S_func ----- S_func-vo {
t: 1
}
5)再次調(diào)用a函數(shù)窿克,后續(xù)每次調(diào)用都在這個(gè)作用域鏈里進(jìn)行,將會(huì)繼承變量對(duì)象里變量的變化
6)第二次執(zhí)行F_func函數(shù)毛甲,重新創(chuàng)建F_func執(zhí)行環(huán)境...重復(fù)上述過(guò)程年叮,b也將獲得一個(gè)自己的執(zhí)行環(huán)境玻募、變量對(duì)象和作用域鏈,同a是一樣的
//對(duì)應(yīng)作用域鏈
F_func-vo { ----- global-vo {
num: 42, name: 'scope',
S_func F_func,
} a: Function,
| b: undefined
| }
|
b=S_func ----- S_func-vo {
t: 1
}
7)最后設(shè)置a為null七咧,解除了a和內(nèi)部函數(shù)的引用跃惫,垃圾回收機(jī)制下一次檢查時(shí)將回收存儲(chǔ)并清除內(nèi)部函數(shù)的作用域鏈和變量對(duì)象
8)b不受影響,依然可以調(diào)用
4艾栋、當(dāng)外部函數(shù)中存在多個(gè)內(nèi)部函數(shù)時(shí),這些函數(shù)構(gòu)成閉包引用的同一份父函數(shù)的變量對(duì)象先较,適合構(gòu)造對(duì)象遥诉;
舉個(gè)例子:
function F_func() {
var num = 42;
return {
fun1: function() { console.log(num); },
fun2: function() { num++; },
fun3: function() { num--; }
};
}
var obj = F_func();
obj.fun2();
obj.fun2();
obj.fun3();
obj.fun1(); // 43
3個(gè)匿名內(nèi)部函數(shù)引用的都是F_func執(zhí)行完生產(chǎn)的同一份變量對(duì)象
二、閉包的用途
閉包可以用在許多地方矮锈,主要有:
- 讀取函數(shù)內(nèi)部的變量
- 讓這些變量的值始終保持在內(nèi)存中。
- 模擬塊級(jí)作用域變量债朵,構(gòu)造變量臨時(shí)性死區(qū)瀑凝,同時(shí)也能創(chuàng)建私有作用域,避免向全局作用域添加過(guò)多變量谚中,減少與其他開(kāi)發(fā)者產(chǎn)生命名沖突
示例:
(function() {
var num= 10;
var add = function(n) {return n + num; }
...
})();
- Js里變量是函數(shù)級(jí)作用域,函數(shù)便是變量的私有作用域宪塔,函數(shù)外不能直接訪問(wèn),只能通過(guò)調(diào)用域鏈比搭,可以封裝私有屬性和私有方法南誊,并開(kāi)發(fā)共有接口,一般用與面向?qū)ο蟮臉?gòu)造
示例:
function Person(name) {
var age = 18;
getAge = function() {
return age;
};
this.getName = function() {
return name + getAge;
};
}
var lucy = new Person('Lucy');
lucy.getName(); // Lucy18
三霉赡、注意事項(xiàng)
1)由閉包會(huì)使得函數(shù)中的變量都被保存在內(nèi)存中幔托,內(nèi)存消耗很大,所以不能濫用閉包迫肖,否則會(huì)造成網(wǎng)頁(yè)的性能問(wèn)題和內(nèi)存泄露攒驰。解決方法是,在退出函數(shù)之前玻粪,將不使用的局部變量全部設(shè)置為null
2)閉包增加的變量查找的作用域鏈的長(zhǎng)度劲室,會(huì)在一定程度上影響查找速度
四、實(shí)例分析
(1)修改前
function buildArr(arr) {
var result = [];
for (var i = 0; i < arr.length; i++) {
var item = 'item' + i;
result.push(function() {console.log(item + ' ' + arr[i])});
}
return result;
}
var fnlist = buildArr([1,2,3]);
fnlist[0](); // item2 undefined
fnlist[1](); // item2 undefined
fnlist[2](); // item2 undefined
外部函數(shù)buildArr執(zhí)行完后幾個(gè)fnlist匿名函數(shù)的作用域鏈為:
fnlist[n] { ----- buildArr-vo { ----- global-vo {
} result:[Function], buildArr: Function,
i: 3, fnlist: undefined
item: item2 }
arr: [1,2,3]
}
(2)修改后
function buildArr(arr) {
var result = [];
for (var i = 0; i < arr.length; i++) {
result.push((function(n) {
var item = 'item' + n;
return function() {
console.log(item + ' ' + arr[n]);
}
})(i));
}
return result;
}
var fnlist = buildArr([1,2,3]);
fnlist[0](); // item0 1
fnlist[1](); // item1 2
fnlist[2](); // item2 3
外部函數(shù)buildArr執(zhí)行完后幾個(gè)fnlist匿名函數(shù)的作用域鏈為:
fnlist[0] { ----- 匿名函數(shù)-vo { ----- buildArr-vo { ----- global-vo {
} n: 0 result: [Function], buildArr: Function,
item: item0 i: 3, fnlist: undefined
} arr: [1,2,3] }
}
|
fnlist[1] { ----- 匿名函數(shù)-vo { --------------
} n: 1 |
item: item1 |
} |
fnlist[2] { ----- 匿名函數(shù)-vo { ------------
} n: 2
item: item2
}
五、實(shí)現(xiàn)閉包的方法
閉包主要依靠外部和內(nèi)部函數(shù)來(lái)構(gòu)造喉磁,而函數(shù)可以又有多種使用方式,比如:一般函數(shù)涝焙、對(duì)象方法等孕暇,所以可以很多實(shí)現(xiàn)閉包的方法赤兴,只要滿(mǎn)足閉包的特點(diǎn)即可隧哮。
(1)作為普通函數(shù)使用
1.1 最簡(jiǎn)單閉包:
var myFunc = (function() {
var num = 10;
return function() {
return num++;
};
})();
1.2 返回對(duì)象或數(shù)組:也可叫模塊模式或增強(qiáng)單例對(duì)象
var myArr = (function() {
var num = 10;
function add() {
return num++;
};
return [add];
})();
var myObj = (function() {
var num = 10;
function add() {
return num++;
};
return {
add1: add,
getNum: function() {return num;} // 公共接口
};
})();
1.3 多級(jí)作用域鏈
var myObj = (function() {
var num = 10;
var arr = [];
for (var i = 0;i < num;i++) {
arr.push((function(n) {
return function() { return n + 1;};
})(i));
}
return {
funs: arr
};
})();
1.4 閉包函數(shù)以其他方式被引用
var $dom = document.querySelector("#id")
function() {
var num = 10;
$dom.onclick = function() {
console.log(num);
};
}
function() {
var num = 10;
setInterval(function() {
console.log(num);
},2000);
}
function() {
var num = 10;
clickMe = function() {
console.log(num);
};
}
(2)作為對(duì)象屬性方法使用
var circle = {
"PI":3.14159,
"area": function(r) {
return this.PI * r * r;
}
};
console.log(circle.area(2));
(3)作為構(gòu)造函數(shù)和面向?qū)ο?/p>
廣泛用于面向?qū)ο缶幊谭绞街?/p>
3.1 實(shí)例方法
function Person(name) {
var age = 18;
getAge = function() {
return age;
};
this.getName = function() {
return name + getAge;
};
}
var lucy = new Person('Lucy');
lucy.getName(); // Lucy18
3.2 靜態(tài)方法和變量近迁,靜態(tài)共享
(function() {
var name = 'lilei';
var age = 18;
Person = function(name) {
name = name;
};
Person.prototype.getName = function() {
return name + age;
};
})();
var lucy = new Person('Lucy');
lucy.getName(); // Lucy18
var lilei = new Person('Lilei');
lucy.getName(); // Lilei18
lilei.getName(); // Lilei18
3.3 原始對(duì)象
var Circle = new Object();
Circle.PI = 3.14159;
Circle.Area = function( r ) {
return this.PI * r * r;
}
alert( Circle.Area( 1.0 ) );
3.3 工廠模式生產(chǎn)對(duì)象
var Circle = function() {
var obj = new Object();
obj.PI = 3.14159;
obj.area = function( r ) {
return this.PI * r * r;
}
return obj;
}
var c = new Circle();
alert( c.area( 1.0 ) );
3.3 原型模式生產(chǎn)對(duì)象
function Circle(r) {
this.r = r;
}
Circle.PI = 3.14159;
Circle.prototype.area = function() {
return Circle.PI * this.r * this.r;
}
var c = new Circle(1.0);
alert(c.area());
其他...