一、閉包的定義
閉包是指有權訪問另一個函數作用域中的變量的函數 --《JavaScript高級程序設計》
函數對象可以通過作用域關聯起來效斑,函數體內的變量都可以保存在函數作用域內非春,這在計算機科學文獻中稱為“閉包”,所有的javascirpt函數都是閉包 --《Javascript權威指南》
相關概念學習:
- 作用域
- 執(zhí)行上下文
- 執(zhí)行上下文堆棧
- 變量對象
- 活動對象
- 作用域鏈
1、作用域 Scope
作用域是一套規(guī)則,用于確定在何處以及如何查找變量(標識符)
作用域共有兩種主要的工作模型:
- 詞法作用域:作用域是在編寫代碼的時候確定的
- 動態(tài)作用域:作用域是在代碼運行的時候確定的
javascript使用的是詞法作用域
2奇昙、執(zhí)行上下文 Execution Contexts
Javascript中代碼的執(zhí)行上下文分為以下三種:
- 全局級別的代碼 – 這個是默認的代碼運行環(huán)境护侮,一旦代碼被載入,引擎最先進入的就是這個環(huán)境储耐。
- 函數級別的代碼 – 當執(zhí)行一個函數時羊初,運行函數體中的代碼。
- Eval的代碼 – 在Eval函數內運行的代碼什湘。
一個執(zhí)行的上下文可以抽象的理解為一個對象长赞。每一個執(zhí)行的上下文都有一系列的屬性(變量對象(variable object),this指針(this value)闽撤,作用域鏈(scope chain) )
Execution Contexts = {
variable object:變量對象;
this value: this指針;
scope chain:作用域鏈;
}
3得哆、執(zhí)行上下文堆棧 Execution Contexts Stack
活動的執(zhí)行上下文在邏輯上組成一個堆棧。堆棧底部永遠都是全局上下文(globalContext)腹尖,而頂部就是當前(活動的)執(zhí)行上下文。
<script>
function add(num){
var sum = 5;
return sum + num;
}
var sum = add(4);
</script>
當add函數被調用時伐脖,add函數執(zhí)行上下文被壓入執(zhí)行上下文堆棧的頂端热幔,此時執(zhí)行上下文堆棧可表示為:
EC Stack = [
<add> functionContext
globalContext
];
add函數執(zhí)行完畢后讼庇,其執(zhí)行上下文將會從執(zhí)行上下文堆棧頂端彈出并被銷毀绎巨。全局執(zhí)行上下文只有在瀏覽器關閉時才會從執(zhí)行上下文堆棧中銷毀
4、變量對象 Variable Object
如果變量與執(zhí)行上下文相關蠕啄,那變量自己應該知道它的數據存儲在哪里场勤,并且知道如何訪問。這種機制稱為變量對象(variable object)歼跟。
可以說變量對象是與執(zhí)行上下文相關的數據作用域(scope of data) 和媳。它是與執(zhí)行上下文關聯的特殊對象,用于存儲被定義在執(zhí)行上下文中的變量(variables)哈街、函數聲明(function declarations) 留瞳。
當進入全局上下文時,全局上下文的變量對象可表示為:
VO = {
add: <reference to function>,
sum: undefined,
Math: <...>,
String: <...>
...
window: global //引用自身
}
5骚秦、活動對象 Activation Object
當函數被調用者激活時她倘,這個特殊的活動對象(activation object) 就被創(chuàng)建了。它包含普通參數(formal parameters) 與特殊參數(arguments)對象(具有索引屬性的參數映射表)作箍∮擦海活動對象在函數上下文中作為變量對象使用。
當add函數被調用時胞得,add函數執(zhí)行上下文被壓入執(zhí)行上下文堆棧的頂端荧止,add函數執(zhí)行上下文中活動對象可表示為
AO = {
num: 4,
sum :5,
arguments:{0:4}
}
6、作用域鏈 Scope Chain
函數上下文的作用域鏈在函數調用時創(chuàng)建的,包含活動對象AO和這個函數內部的[[scope]]屬性罩息。
var x = 10;
function foo() {
var y = 20;
function bar() {
var z = 30;
alert(x + y + z);
}
bar();
}
foo();
在這段代碼中我們看到變量"y"在函數"foo"中定義(意味著它在foo上下文的AO中)"z"在函數"bar"中定義嗤详,但是變量"x"并未在"bar"上下文中定義,相應地瓷炮,它也不會添加到"bar"的AO中葱色。乍一看,變量"x"相對于函數"bar"根本就不存在娘香;
函數"bar"如何訪問到變量"x"苍狰?理論上函數應該能訪問一個更高一層上下文的變量對象。實際上它正是這樣烘绽,這種機制是通過函數內部的[[scope]]屬性來實現的淋昭。
[[scope]]是所有父級變量對象的層級鏈,處于當前函數上下文之上安接,在函數創(chuàng)建時存于其中翔忽。
注意: [[scope]]在函數創(chuàng)建時被存儲是靜態(tài)的(不變的),直至函數銷毀盏檐。即:函數可以永不調用歇式,但[[scope]]屬性已經寫入,并存儲在函數對象中胡野。
在這里我們逐步分析下
全局上下文的變量對象是:
globalContext.VO === Global = {
x: 10
foo: <reference to function>
};
在"foo"創(chuàng)建時材失,"foo"的[[scope]]屬性是:
foo.[[Scope]] = [
globalContext.VO
];
在"foo"激活時(進入上下文),"foo"上下文的活動對象是:
fooContext.AO = {
y: 20,
bar: <reference to function>
};
"foo"上下文的作用域鏈為:
fooContext.Scope = [
fooContext.AO,
globalContext.VO
];
內部函數"bar"創(chuàng)建時硫豆,其[[scope]]為:
bar.[[Scope]] = [
fooContext.AO,
globalContext.VO
];
在"bar"激活時龙巨,"bar"上下文的活動對象為:
barContext.AO = {
z: 30
};
"bar"上下文的作用域鏈為:
bar.Scope= [
barContext.AO,
fooContext.AO,
globalContext.VO
];
二熊响、閉包的原理
我們通過一個閉包的例子來分析一下閉包的形成原理
function add(){
var sum =5;
var func = function () {
console.log(sum);
}
return func;
}
var addFunc = add();
addFunc(); //5
js執(zhí)行流進入全局執(zhí)行上下文環(huán)境時,全局執(zhí)行上下文可表示為:
globalContext = {
VO: {
add: <reference to function>,
addFunc: undefined
},
this: window,
scope chain: window
}
當add函數被調用時旨别,add函數執(zhí)行上下文可表示為:
addContext = {
AO: {
sum: undefined //代碼進入執(zhí)行階段時此處被賦值為5
func: undefined //代碼進入執(zhí)行階段時此處被賦值為function (){console.log(sum);}
},
this: window,
scope chain: addContext.AO + globalContext.VO
}
add函數執(zhí)行完畢后,js執(zhí)行流回到全局上下文環(huán)境中汗茄,將add函數的返回值賦值給addFunc昼榛。
由于addFunc仍保存著func函數的引用,所以add函數執(zhí)行上下文從執(zhí)行上下文堆棧頂端彈出后并未被銷毀而是保存在內存中剔难。
當addFunc()執(zhí)行時胆屿,func函數被調用,此時func函數執(zhí)行上下文可表示為:
funcContext = {
this: window,
scope chain: addContext.AO + globalContext.VO
}
當要訪問變量sum時偶宫,func的活動對象中未能找到非迹,則會沿著作用域鏈查找,由于js遵循詞法作用域纯趋,作用域在函數創(chuàng)建階段就被確定憎兽,在add函數的活動對象中找到sum = 5;
形成閉包的原因:
Javascript允許使用內部函數---即函數定義和函數表達式位于另一個函數的函數體內冷离。而且,這些內部函數可以訪問它們所在的外部函數中聲明的所有局部變量纯命、參數和聲明的其他內部函數西剥。當其中一個這樣的內部函數在包含它們的外部函數之外被調用時,就會形成閉包亿汞。
三瞭空、閉包的用途
閉包可以用在許多地方。它的最大用處有兩個:
- 讀取函數內部的變量
- 讓這些變量的值始終保持在內存中
1疗我、保護變量的安全實現JS私有屬性和私有方法
利用閉包可以讀取函數內部的變量咆畏,變量在函數外部不能直接讀取到,從而達到保護變量安全的作用吴裤。因為私有方法在函數內部都能被訪問到旧找,從而實現了私有屬性和方法的共享。
常見的模塊模式就是利用閉包的這種特性建立的
var Counter = (function() {
//私有屬性
var privateCounter = 0;
//私有方法
function changeBy(val) {
privateCounter += val;
}
return {
increment: function() {
changeBy(1);
},
decrement: function() {
changeBy(-1);
},
value: function() {
return privateCounter;
}
}
})();
console.log(privateCounter); //privateCounter is not defined
console.log(Counter.value()); // 0
Counter.increment();
Counter.increment();
console.log(Counter.value()); // 2
Counter.decrement();
console.log(Counter.value()); // 1
在jQuery框架的私有方法和變量也是這么設計的
var $ = jQuery = function(){
return jQuery.fn.init();
}
jQuery.fn = jQuery.prototype = {
init:function(){
return this; //this指向jQuery.prototype
},
length: 1,
size: function(){
return this.length;
}
}
console.log($().size()); // 1
2麦牺、將處理結果緩存
var mult = (function(){
var cache = {};
var calculate = function(){
var a = 1;
for(vari=0,l=arguments.length;i<l;i++){
a = a*arguments[i];
}
return a;
};
return function(){
var args = Array.prototype.join.call(arguments,',');
if(args in cache){
return cache[args];
}
return cache[args] = calculate.apply(null,arguments);
}
})();
這樣我們在第二次調用的時候拂苹,就會從緩存中讀取到該對象赫粥。
3禁炒、模塊化代碼
var mod = (function(){ //mod是一個模塊脉漏,私有變量是a榴芳,私有函數是func1和func2
var a = 1;
function func1(){
a++;
alert(a);
}
function func2(){
a++;
alert(a);
}
return {
b:func1,
c:func2
}
})() //函數表達式自執(zhí)行羞酗,結果是return后面的對象
mod.b(); //2
mod.c(); //3
4殴俱、在循環(huán)中找到索引
<body>
<ul>
<li>111111</li>
<li>111111</li>
<li>111111</li>
</ul>
<script>
window.onload = function(){
var aLi = document.getElementsByTagName('li');
for(var i=0;i<aLi.length;i++){
aLi[i].onclick = function(){
alert(i); //順序點擊三個li养交,分別彈出3 3 3易结。因為點擊的時候for循環(huán)已經執(zhí)行完畢
}
}
}
</script>
</body>
可使用閉包改為:
<body>
<ul>
<li>111111</li>
<li>111111</li>
<li>111111</li>
</ul>
<script>
window.onload = function(){
var aLi = document.getElementsByTagName('li');
for(var i=0;i<aLi.length;i++){
(function(i){
aLi[i].onclick = function(){
alert(i); //0 1 2
}
})(i) //將i作為參數傳遞給內部函數枕荞,i 在for循環(huán)執(zhí)行完畢不會被釋放
}
}
</script>
</body>
或者另一種寫法:
<body>
<ul>
<li>111111</li>
<li>111111</li>
<li>111111</li>
</ul>
<script>
window.onload = function(){
var aLi = document.getElementsByTagName('li');
for(var i=0;i<aLi.length;i++){
aLi[i].onclick = (function(i){
return function(){
alert(i);
}
})(i) //循環(huán)的時候,這個函數表達式自執(zhí)行搞动,i也是駐扎在內存當中躏精。
}
}
</script>
</body>
理解了閉包的原理我們發(fā)現閉包的這些用途都是利用了閉包保存了當前函數的活動對象的特點,這樣閉包函數在作用域之外被調用時依然能夠訪問其創(chuàng)建時的作用域
四鹦肿、閉包的缺點
- 閉包將函數的活動對象維持在內存中矗烛,過度使用閉包會導致內存占用過多,所以在使用完后需要將保存在內存中的活動對象解除引用箩溃;
- 閉包只能取得外部函數中任何變量的最后一個值瞭吃,在使用循環(huán)且返回的函數中帶有循環(huán)變量時會得到錯誤結果;
- 當返回的函數為匿名函數時涣旨,注意匿名函數中的this指的是window對象歪架。
五、閉包面試題解
1霹陡、用閉包實現計數器
function addCount() {
var count = 0;
return function() {
count = count + 1;
console.log(count);
};
}
addCount() 執(zhí)行的時候, 返回一個函數, 函數是可以創(chuàng)建自己的作用域的, 但是此時返回的這個函數內部需要引用 addCount() 作用域下的變量 count, 因此這個 count 是不能被銷毀的.接下來需要幾個計數器我們就定義幾個變量就可以,并且他們都不會互相影響,每個函數作用域中還會保存 count 變量不被銷毀,進行不斷的累加和蚪。
var fun1 = addCount();
fun1(); //1
fun1(); //2
var fun2 = addCount();
fun2(); //1
fun2(); //2
2止状、for 循環(huán)中打印
for (var i = 0; i < 4; i++) {
setTimeout(function() {
console.log(i);
}, 300);
}
上邊打印出來的都是 4, 可能部分人會認為打印的是 0,1,2,3
原因:js 執(zhí)行的時候首先會先執(zhí)行主線程,異步相關的會存到異步隊列里,當主線程執(zhí)行完畢開始執(zhí)行異步隊列, 主線程執(zhí)行完畢后,此時 i 的值為 4,所以在執(zhí)行異步隊列的時候,打印出來的都是 4(這里需要大家對 event loop 有所了解(js 的事件循環(huán)機制))
如何修改使其正常打印:采用閉包使其正常打印
//方法一:
for (var i = 0; i < 4; i++) {
setTimeout(
(function(i) {
return function() {
console.log(i);
};
})(i),
300
);
}
// 或者
for (var i = 0; i < 4; i++) {
setTimeout(
(function() {
var temp = i;
return function() {
console.log(temp);
};
})(),
300
);
}
// 這個是通過自執(zhí)行函數返回一個函數,然后在調用返回的函數去獲取自執(zhí)行函數內部的變量,此為閉包
//方法二:
for (var i = 0; i < 4; i++) {
(function(i) {
setTimeout(function() {
console.log(i);
}, 300);
})(i);
}
// 方法二是通過創(chuàng)建一個自執(zhí)行函數,使變量存在這個自執(zhí)行函數的作用域里
3、真實的獲取多個元素并添加點擊事件
var op = document.querySelectorAll("p");
for (var j = 0; j < op.length; j++) {
op[j].onclick = function() {
alert(j);
};
}
//alert出來的值是一樣的
// 解決辦法一:
for (var j = 0; j < op.length; j++) {
(function(j) {
op[j].onclick = function() {
alert(j);
};
})(j);
}
// 解決辦法二:
for (var j = 0; j < op.length; j++) {
op[j].onclick = (function(j) {
return function() {
alert(j);
};
})(j);
}
//解決方法三
for (var j = 0; j < op.length; j++) {
op[j].onclick = (function(j) {
var temp = j;
return function() {
alert(j);
};
})(j);
}