JavaScript閉包?看完這幾個例子你就完全搞懂了!
理解閉包,首先要知道作用域鏈是個什麼東西。
1.什麼是作用域鏈
在 JavaScript 的最頂層程式碼中(也就是不包含在任何函式定義內的程式碼),作用域鏈有一個全域性物件組成。在不包含巢狀的函式體內,作用域鏈上有兩個物件,第一個是定義函式引數和區域性變數的物件,第二個是全域性物件。在一個巢狀的函式體內,作用域鏈上至少有三個物件。
當定義一個函式時,它實際上儲存一個作用域鏈。
當呼叫這個函式時,它建立一個新的物件來儲存它的區域性變數,並將這個物件新增至儲存的那個作用域鏈上,同時建立一個新的更長的表示函式呼叫作用域的“鏈”。
對於巢狀函式來說,每次呼叫外部函式時,內部函式又會重新定義一次,因為每次呼叫外部函式的時候,作用域鏈都是不同的。
內部函式在每次定義的時候都有微妙的差別,即每次呼叫外部函式的時候,內部函式的程式碼都是相同的,而關聯這段程式碼的作用域鏈卻是不相同的
2.什麼是閉包
函式物件可以通過作用域鏈相互關聯起來,函式體內部的變數都可以儲存在函式作用域內,這種特性在電腦科學文獻中稱為“閉包”——是指函式變數可以被隱藏於作用域之內,因此看起來是函式將變數“包裹”起來了
例1
var scope = 'global scope'; function checkscope() { var scope = "local scope"; function f() { console.log(scope); } return f(); } checkscope();
執行函式之後列印
checkscope()函式聲明瞭一個區域性變數,並定義一個函式 f(), 函式 f() 返回了這個變數的值,最後將函式 f() 的執行結果返回。這個結果是顯而易見的返回函式內區域性變數,然後我們改動一下函式再看看。。。
var scope = 'global scope';
function checkscope() {
var scope = "local scope";
function f() {
console.log(scope);
}
return f;
}
checkscope()();
上面例子也可以改為這樣
var scope = 'global scope';
function checkscope() {
var scope = "local scope";
function f() {
console.log(scope);
}
return f;
}
var f = checkscope();
f();
結果並沒有改變,這是什麼原因呢?
會看一下作用域的基本規則:javascript 函式執行用到了作用域鏈,這個作用域鏈是函式定義的時候建立的。巢狀的函式 f() 定義在這個作用域裡,其中的變數scope一定是區域性變數,不管在何時何地執行函式f(),這種繫結在執行f()時依然有效。簡言之,閉包的這個特性就是:它們可以捕捉到區域性變數(和引數),並一直儲存下來,看起來像這些變數繫結到了在其中定義的外部函式。
3.利用閉包
例2
function counter() {
var n = 0;
return {
count: function() {
return n++;
},
reset: function() {
n = 0;
}
};
}
var c = counter(), d = counter();
console.log(c.count());
console.log(d.count());
c.reset();
console.log(c.count());
console.log(d.count());
每次呼叫couter()都會建立一個新的作用域和一個新的私有變數。因此,變數c重置了n=0,所以第二次c,d執行count()的結果不一樣。
例3
function counter(n) {
return {
get count() {return n++;},
set count(m) {
if(m>=n) {
n = m;
}else {
throw Error("count can only be set to a larger value")
}
}
}
}
var c = counter(1000);
console.log(c.count);
console.log(c.count);
console.log(c.count = 2000);
console.log(c.count);
console.log(c.count = 2000);
這個版本的counter()函式並未申明區域性變數,而只是使用引數n來儲存私有狀態,屬性儲存器方法可以訪問n,這樣的話,呼叫counter()的函式就可以指定私有變數的初始值了。
例4(利用閉包實現的私有屬性儲存器方法)
function addPrivateProperty(o, name, predicate) {
var value;
o["get" + name] = function() { return value; };
o["set" + name] = function(v) {
if(predicate && !predicate(v)){
throw Error("set" + name + ": invalid value "+ v);
}else {
value = v;
}
}
}
var o = {};
addPrivateProperty(o, "Name", function(x) { return typeof x == "string"});
o.setName("Frank");
console.log(o.getName());
o.setName(o); //將丟擲異常
這裡除了呼叫o物件的setName()方法,且只能傳入字串對變數value進行賦值,同樣只能通過o物件的geiName()方法才能訪問到變數value的值。
4.閉包的危害
例5
function constfunc(v) {return function() {return v;};}
var funcs = [];
for(var i = 0 ; i< 10; i++) {
funcs[i] = constfunc(i);
}
console.log(funcs[5]());
function constfuncs() {
var funcs = [];
for(var i = 0; i < 10; i++){
funcs[i] = function() {
return i;
}
}
return funcs;
}
var funcs2 = constfuncs();
console.log(funcs2[5]());
第一個函式顯然使我們需要的結果,可以第二次為什麼會呼叫結果顯示為10呢?其實陣列funcs2儲存的所有函式執行結果都是10。
第二個函式中建立了是個閉包,並將它們儲存在一個數組中,這些閉包都是在同一個函式呼叫中定義的,因此它們可以共享變數i。當constfuncs()返回時,變數i的值是10,所有的閉包都共享這一個值,因此陣列中的函式的返回值都是同一個值。
故而我們需要像第一個函式那樣,不是直接將變數i返回而是通過傳入引數的方法,實現變數私有化的方法來實現想要的結果。