閉包算是js里面比較不容易理解的點,尤其是對於沒有編程基礎的人來說。
其實閉包要注意的就那么幾條,如果你都明白了那么征服它並不是什么難事兒。下面就讓我們來談一談閉包的一些基本原理。
閉包的概念
一個閉包就是一個函數和被創建的函數中的作用域對象的組合。(作用域對象下面會說)
通俗一點的就是 “ 只要一個函數中嵌套了一個或多個函數,那么我們就可以稱它們構成了閉包。 ”
類似這樣:
1 function A() { 2 var i = 5; 3 return function() { 4 console.log('i = '+i); 5 } 6 } 7
8 var a = A(); 9 a(); // i = 5
閉包的原理
1、外部函數的局部變量若會被閉包函數調用就不會在外部函數執行完畢之后立即被回收。
我們知道,不管什么語言,操作系統都會存在一個垃圾回收機制,將多余分配的空間回收掉以便減小內存。而一個函數的生命周期的是從調用它開始的,在函數調用完畢的時候函數內部的局部變量等都會被回收機制回收。
我們拿上述例子來說,當我們的外部函數A調用完畢時,A中的局部變量i按理說就會被操作系統回收而不存在,但是當我們用了閉包結果就不是那樣了,i並不會被回收。試想,如果i被回收了那么返回的函數里面豈不是就是打印undefined了?
i為什么沒有被回收?
在javascript執行一個函數的時候都會創建一個作用域對象,將函數中的局部變量(函數的形參也是局部變量)保存進去,伴隨着那些傳入函數的變量一起被初始化。
所以當調用A的時候就創建了一個作用域對象,我們姑且稱之為Aa,那么這個Aa應該是這樣的: Aa = { i: 5 }; 在A函數返回一個函數之后,A執行完畢。Aa對象本應該被回收,但是由於返回的函數使用了Aa的屬性i,所以返回的函數保存了一個指向Aa的引用,所以Aa不會被回收。
所以理解作用域對象,就能理解為什么函數的局部變量在遇到閉包的時候不會在函數調用完畢時立即被回收了。
再來個例子:
1 function A(age) { 2 var name = 'wind'; 3 var sayHello = function() { 4 console.log('hello, '+name+', you are '+age+' years old!'); 5 }; 6 return sayHello; 7 } 8 var wind = A(20); 9 wind(); // hello, wind, you are 20 years old!
你能說出的它的作用域對象Ww是什么嗎?
Ww = { age: 20, name: 'wind' };
2、每調用一次外部函數就產生一個新的閉包,以前的閉包依舊存在且互不影響。
3、同一個閉包會保留上一次的狀態,當它被再次調用時會在上一次的基礎上進行。
每調用一次外部函數產生的作用域對象都不一樣,你可以這樣想,上面的例子,你每次傳入的參數age不一樣,所以就每次生成的對象不一樣。
每調用一次外部函數那么就會生成一個新的作用域對象。
1 function A() { 2 var num = 42; 3 return function() { console.log(num++); } 4 } 5 var a = A(); 6 a(); // 42
7 a(); // 43
8
9 var b = A(); // 重新調用A(),形成新閉包
10 b(); // 42
這個代碼讓我們發現了兩個事情,一、當我們連續調用兩次a();,num會在原基礎上自加。說明同一個閉包會保留上一次的狀態,當它被再次調用時會在上一次的基礎上進行。 二、我們的b();的結果為42,說明它是一個新的閉包,並且不受其他閉包的影響。
我們可以這樣想,就好比我們吹肥皂泡一樣,我每次吹一下(調用外部函數),就會產生一個新的肥皂泡(閉包),多個肥皂泡可以同時存在且兩個肥皂泡之間不會相互影響。
4、在外部函數中存在的多個函數 “ 同生共死 ”
以下三個函數被同時聲明並且都可以對作用域對象的屬性(局部變量)進行訪問與操作。
var fun1, fun2, fun3; function A() { var num = 42; fun1 = function() { console.log(num); } fun2 = function() { num++; } fun3 = function() { num--; } } A(); fun1(); // 42
fun2(); fun2(); fun1(); // 44
fun3(); fun1(); //43
var old = fun1; A(); fun1(); // 42
old(); // 43 上一個閉包的fun1()
由於函數不能有多個返回值,所以我用了全局變量。我們再次可以看出在我們第二次調用A()時產生了一個新的閉包。
當閉包遇到循環變量
當我們說到閉包就不得不說當閉包遇到循環變量這一種情況,看如下代碼:
1 function buildArr(arr) { 2 var result = []; 3 for (var i = 0; i < arr.length; i++) { 4 var item = 'item' + i; 5 result.push( function() {console.log(item + ' ' + arr[i])} ); 6 } 7 return result; 8 } 9
10 var fnlist = buildArr([1,2,3]); 11 fnlist[0](); // item2 undefined
12 fnlist[1](); // item2 undefined
13 fnlist[2](); // item2 undefined
怎么會這樣呢?我們預想的三個輸出應該是 item0 1, item1 2, item2 3。為什么結果卻是返回的result數組里面存儲了三個 item2 undefined ?
我們上文中提到過兩點,1、閉包在返回的時候對作用域對象有一個引用。2、在外部函數中存在的多個內部函數 “ 同生共死 ”。
我們的for循環為外部函數創建了多個“同生共死”的內部函數,它們都共享一個環境,而當result數組返回的時候,所有的內部函數都引用了同一個作用域對象:
1 var bArr = { 2 item: 'item2', 3 i: 3, 4 arr: [1,2,3] 5 }
為什么作用域對象是這樣的?拿我們上面的例子來說,當循環全部結束的時候作用域對象中的屬性 i 正好是i++之后的3,而arr[3]是沒有值的,所以為undefined。
有朋友會疑惑:為什么item的值是item2,難道不應該是item3嗎?
注意,在最后一次循環的時候也就是 i = 2的時候,item的值為item2,當 i++,i = 3循環條件不滿足循環結束,此時的item的值已經被保存下來了,所以此時的arr[i]為arr[3],而item為item2。這樣能理解嗎?
如果我們將代碼改成這樣那就說得通了:
function buildArr(arr) { var result = []; for (var i = 0; i < arr.length; i++) { result.push( function() {console.log('item' + i + ' ' + arr[i])} ); } return result; } var fnlist = buildArr([1,2,3]); fnlist[1](); // item3 undefined
那么問題來了,如何改正呢?且看代碼:
1 function buildArr(arr) { 2 var result = []; 3 for (var i = 0; i < arr.length; i++) { 4 result.push( (function(n) { 5 return function() { 6 var item = 'item' + n; 7 console.log(item + ' ' + arr[n]); 8 } 9 })(i)); 10 } 11 return result; 12 } 13
14 var fnlist = buildArr([1,2,3]); 15 fnlist[0](); // item0 1
16 fnlist[1](); // item1 2
17 fnlist[2](); // item2 3
我們可以用一個自執行函數將i綁定,這樣i的每一個狀態都會被存儲,答案就和我們預期的一樣了。
所以以后在使用閉包的時候遇到循環變量我們要習慣性的想到用自執行函數來綁定它。
=========================3月14日更新======================================================
關於上面的問題還有一個更簡單的方法:
1 function buildArr(arr) { 2 var result = []; 3 for (let i = 0; i < arr.length; i++) { 4 let item = 'item' + i; 5 result.push( function() {console.log(item + ' ' + arr[i])} ); 6 } 7 return result; 8 } 9
10 var fnlist = buildArr([1,2,3]); 11 fnlist[0](); // item0 1
這里使用了let代替var,let的好處是可以“模擬創建”塊作用域,點到為止,有興趣的朋友可以自行深入了解let。
以上就是我對閉包的理解,如果有什么意見或建議希望我們能在評論區多多交流。感謝,共勉。