在前端開發(fā)中閉包是一個很重要的知識點,是面試中一定會被問到的內(nèi)容。之前我對閉包的理解主要是"通過閉包可以在函數(shù)外部能訪問到函數(shù)內(nèi)部的變量",對閉包運用的也很少,甚至自己寫過閉包自己都不太清楚,只知道這樣寫可以解決問題。最近在梳理自己的
js知識點,發(fā)現(xiàn)自己對js閉包理解的很不透徹,于是想全面的分析一下閉包,特別是閉包的形成原因和閉包的使用場景。
閉包的定義
閉包是指有權(quán)訪問另一個函數(shù)作用域中的變量的函數(shù) --《JavaScript高級程序設(shè)計》
函數(shù)對象可以通過作用域關(guān)聯(lián)起來,函數(shù)體內(nèi)的變量都可以保存在函數(shù)作用域內(nèi),這在計算機科學(xué)文獻中稱為“閉包”,所有的
javascirpt函數(shù)都是閉包 --《Javascript權(quán)威指南》
在認識閉包原理之前我們先必須對作用域、執(zhí)行上下文、執(zhí)行上下文堆棧、變量對象、活動對象、作用域鏈有著全面的認識
作用域 Scope
作用域是一套規(guī)則,用于確定在何處以及如何查找變量(標識符)
作用域共有兩種主要的工作模型:
- 詞法作用域:作用域是在編寫代碼的時候確定的
- 動態(tài)作用域:作用域是在代碼運行的時候確定的
我們知道javascript使用的是詞法作用域
執(zhí)行上下文 Execution Contexts
Javascript中代碼的執(zhí)行上下文分為以下三種:
- 全局級別的代碼 – 這個是默認的代碼運行環(huán)境,一旦代碼被載入,引擎最先進入的就是這個環(huán)境。
- 函數(shù)級別的代碼 – 當(dāng)執(zhí)行一個函數(shù)時,運行函數(shù)體中的代碼。
Eval的代碼 – 在Eval函數(shù)內(nèi)運行的代碼。
一個執(zhí)行的上下文可以抽象的理解為一個對象。每一個執(zhí)行的上下文都有一系列的屬性(變量對象(variable object),this指針(this value),作用域鏈(scope chain) )
Execution Contexts = {
variable object:變量對象;
this value: this指針;
scope chain:作用域鏈;
}
執(zhí)行上下文堆棧 Execution Contexts Stack
活動的執(zhí)行上下文在邏輯上組成一個堆棧。堆棧底部永遠都是全局上下文(globalContext),而頂部就是當(dāng)前(活動的)執(zhí)行上下文。
<script>
function add(num){
var sum = 5;
return sum + num;
}
var sum = add(4);
</script>
當(dāng)add函數(shù)被調(diào)用時,add函數(shù)執(zhí)行上下文被壓入執(zhí)行上下文堆棧的頂端,此時執(zhí)行上下文堆棧可表示為:
EC Stack = [
<add> functionContext
globalContext
];
add函數(shù)執(zhí)行完畢后,其執(zhí)行上下文將會從執(zhí)行上下文堆棧頂端彈出并被銷毀。全局執(zhí)行上下文只有在瀏覽器關(guān)閉時才會從執(zhí)行上下文堆棧中銷毀
變量對象 Variable Object
如果變量與執(zhí)行上下文相關(guān),那變量自己應(yīng)該知道它的數(shù)據(jù)存儲在哪里,并且知道如何訪問。這種機制稱為變量對象(variable object)。
可以說變量對象是與執(zhí)行上下文相關(guān)的數(shù)據(jù)作用域(scope of data) 。它是與執(zhí)行上下文關(guān)聯(lián)的特殊對象,用于存儲被定義在執(zhí)行上下文中的變量(variables)、函數(shù)聲明(function declarations)。
當(dāng)進入全局上下文時,全局上下文的變量對象可表示為:
VO = {
add: <reference to function>,
sum: undefined,
Math: <...>,
String: <...>
...
window: global //引用自身
}
活動對象 Activation Object
當(dāng)函數(shù)被調(diào)用者激活時,這個特殊的活動對象(activation object) 就被創(chuàng)建了。它包含普通參數(shù)(formal parameters) 與特殊參數(shù)(arguments)對象(具有索引屬性的參數(shù)映射表)?;顒訉ο笤诤瘮?shù)上下文中作為變量對象使用。
當(dāng)add函數(shù)被調(diào)用時,add函數(shù)執(zhí)行上下文被壓入執(zhí)行上下文堆棧的頂端,add函數(shù)執(zhí)行上下文中活動對象可表示為
AO = {
num: 4,
sum :5,
arguments:{0:4}
}
作用域鏈 Scope Chain
函數(shù)上下文的作用域鏈在函數(shù)調(diào)用時創(chuàng)建的,包含活動對象
AO和這個函數(shù)內(nèi)部的[[scope]]屬性。
var x = 10;
function foo() {
var y = 20;
function bar() {
var z = 30;
alert(x + y + z);
}
bar();
}
foo();
在這段代碼中我們看到變量"y"在函數(shù)"foo"中定義(意味著它在foo上下文的AO中)"z"在函數(shù)"bar"中定義,但是變量"x"并未在"bar"上下文中定義,相應(yīng)地,它也不會添加到"bar"的AO中。乍一看,變量"x"相對于函數(shù)"bar"根本就不存在
函數(shù)"bar"如何訪問到變量"x"?理論上函數(shù)應(yīng)該能訪問一個更高一層上下文的變量對象。實際上它正是這樣,這種機制是通過函數(shù)內(nèi)部的[[scope]]屬性來實現(xiàn)的。
[[scope]]是所有父級變量對象的層級鏈,處于當(dāng)前函數(shù)上下文之上,在函數(shù)創(chuàng)建時存于其中。
注意: [[scope]]在函數(shù)創(chuàng)建時被存儲是靜態(tài)的(不變的),直至函數(shù)銷毀。即:函數(shù)可以永不調(diào)用,但[[scope]]屬性已經(jīng)寫入,并存儲在函數(shù)對象中。
在這里我們逐步分析下
全局上下文的變量對象是:
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
];
內(nèi)部函數(shù)"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
}
當(dāng)add函數(shù)被調(diào)用時,add函數(shù)執(zhí)行上下文可表示為:
addContext = {
AO: {
sum: undefined //代碼進入執(zhí)行階段時此處被賦值為5
func: undefined //代碼進入執(zhí)行階段時此處被賦值為function (){console.log(sum);}
},
this: window,
scope chain: addContext.AO + globalContext.VO
}
add函數(shù)執(zhí)行完畢后,js執(zhí)行流回到全局上下文環(huán)境中,將add函數(shù)的返回值賦值給addFunc。
由于addFunc仍保存著func函數(shù)的引用,所以add函數(shù)執(zhí)行上下文從執(zhí)行上下文堆棧頂端彈出后并未被銷毀而是保存在內(nèi)存中。
當(dāng)addFunc()執(zhí)行時,func函數(shù)被調(diào)用,此時func函數(shù)執(zhí)行上下文可表示為:
funcContext = {
this: window,
scope chain: addContext.AO + globalContext.VO
}
當(dāng)要訪問變量sum時,func的活動對象中未能找到,則會沿著作用域鏈查找,由于js遵循詞法作用域,作用域在函數(shù)創(chuàng)建階段就被確定,在add函數(shù)的活動對象中找到sum = 5;
Javascript允許使用內(nèi)部函數(shù)---即函數(shù)定義和函數(shù)表達式位于另一個函數(shù)的函數(shù)體內(nèi)。而且,這些內(nèi)部函數(shù)可以訪問它們所在的外部函數(shù)中聲明的所有局部變量、參數(shù)和聲明的其他內(nèi)部函數(shù)。當(dāng)其中一個這樣的內(nèi)部函數(shù)在包含它們的外部函數(shù)之外被調(diào)用時,就會形成閉包。
閉包的用途
閉包可以用在許多地方。它的最大用處有兩個,一個是前面提到的可以讀取函數(shù)內(nèi)部的變量,另一個就是讓這些變量的值始終保持在內(nèi)存中。
1. 保護變量的安全實現(xiàn)JS私有屬性和私有方法
利用閉包可以讀取函數(shù)內(nèi)部的變量,變量在函數(shù)外部不能直接讀取到,從而達到保護變量安全的作用。因為私有方法在函數(shù)內(nèi)部都能被訪問到,從而實現(xiàn)了私有屬性和方法的共享。
常見的模塊模式就是利用閉包的這種特性建立的
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框架的私有方法和變量也是這么設(shè)計的
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. 將處理結(jié)果緩存
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);
}
})();
這樣我們在第二次調(diào)用的時候,就會從緩存中讀取到該對象。
理解了閉包的原理我們發(fā)現(xiàn)閉包的這些用途都是利用了閉包保存了當(dāng)前函數(shù)的活動對象的特點,這樣閉包函數(shù)在作用域之外被調(diào)用時依然能夠訪問其創(chuàng)建時的作用域
閉包的缺點
- 閉包將函數(shù)的活動對象維持在內(nèi)存中,過度使用閉包會導(dǎo)致內(nèi)存占用過多,所以在使用完后需要將保存在內(nèi)存中的活動對象解除引用;
- 閉包只能取得外部函數(shù)中任何變量的最后一個值,在使用循環(huán)且返回的函數(shù)中帶有循環(huán)變量時會得到錯誤結(jié)果;
- 當(dāng)返回的函數(shù)為匿名函數(shù)時,注意匿名函數(shù)中的
this指的是window對象。