閉包是很多語言都具備的特性,上篇《從抽象代數漫遊函數式程式設計(1):閉包概念再Java/PHP/JS中的定義》
閉包的特性
閉包有三個特性:
- 函數嵌套函數
- 函數内部可以引用外部的參數和變量
- 參數和變量不會被垃圾回收機制回收
在js中,閉包主要涉及到js的幾個其他的特性:作用域鍊,垃圾(記憶體)回收機制,函數嵌套,等等。
閉包(closure)是Javascript語言特色(函數式程式設計特色),很多進階應用都要依靠閉包實作。但是JavaScript的一個難點,因為JavaScript這個早産兒先天不足,不想強類型語言那麼泾渭分明。引用《ECMAScript進化史(1):話說Web腳本語言王者JavaScript的加冕曆史 》的段落:
Javascript其實(簡化的)函數式程式設計+(簡化的)面向對象程式設計,這是由Brendan Eich(函數式程式設計)與網景公司(面向對象程式設計)共同決定的。它是C語言和Self語言一夜情的怪胎。'它的優秀之處并非原創,它的原創之處并不優秀。'
總的來說,Brendan Eich的設計思路是這樣的:
- 借鑒C語言的基本文法;
- 借鑒Java語言的資料類型和記憶體管理;
- 借鑒Scheme語言,将函數提升到"第一等公民"(first class)的地位;
- 借鑒Self語言,使用基于原型(prototype)的繼承機制。
…………
原因一:javascript是一個函數程式設計語言,怪就怪在它也有this指針,說明這個函數程式設計語言也是面向對象的語言,說的具體點,javascript裡的函數是一個高階函數,程式設計語言裡的高階函數是可以作為對象傳遞的,同時javascript裡的函數還有可以作為構造函數,這個構造函數可以建立執行個體化對象,結果導緻方法執行時候this指針的指向會不斷發生變化,很難控制。
原因二:javascript裡的全局作用域對this指針有很大的影響,由上面java的例子我們看到,this指針隻有在使用new操作符後才會生效,但是javascript裡的this在沒有進行new操作也會生效,這時候this往往會指向全局對象window。
原因三:javascript裡call和apply操作符可以随意改變this指向,這看起來很靈活,但是這種不合常理的做法破壞了我們了解this指針的本意,同時也讓寫代碼時候很難了解this的真正指向
诠釋JS閉包函數
在了解閉包以前.最好能先了解一下JavaScript的垃圾回收機制與作用域鍊的含義,推薦閱讀《再談JavaScript垃圾回收機制:分析與排查JS記憶體洩露情形》
javascript的垃圾回收原理
- 引用計數(reference counting):機制就是跟蹤一個值的引用次數,當聲明一個變量并将一個引用類型指派給該變量時該值引用次數加1,當這個變量指向其他一個時該值的引用次數便減一。當該值引用次數為0時就會被回收。該方式會引起記憶體洩漏的原因是它不能解決循環引用的問題: var a={};var b={};a.prop = b;b.prop = a;
-
标記清除(mark and sweep):大部分浏覽器以此方式進行垃圾回收,當變量進入執行環境(函數中聲明變量)的時候,垃圾回收器将其标記為“進入環境”,當變量離開環境的時候(函數執行結束)将其标記為“離開環境”,在離開環境之後還有的變量則是需要被删除的變量。标記方式不定,可以是某個特殊位的反轉或維護一個清單等。
垃圾收集器給記憶體中的所有變量都加上标記,然後去掉環境中的變量以及被環境中的變量引用的變量的标記。在此之後再被加上的标記的變量即為需要回收的變量,因為環境中的變量已經無法通路到這些變量。
其實我們隻需要記住:
- 在javascript中,如果一個對象不再被引用,那麼這個對象就會被GC回收;
- 如果兩個對象互相引用,而不再被第3者所引用,那麼這兩個互相引用的對象也會被回收。
JavaScript作用域鍊
簡單來說,,作用域鍊就是函數在定義的時候建立的,用于尋找使用到的變量的值的一個索引,而他内部的規則是:
- 把函數自身的本地變量放在最前面,
- 把自身的父級函數中的變量放在其次
- 把再高一級函數中的變量放在更後面
- ……以此類推直至全局對象為止
當函數中需要查詢一個變量的值的時候,js解釋器會去作用域鍊去查找。從最前面的本地變量中先找,如果沒有找到對應的變量,則到下一級的鍊上找,一旦找到了變量,則不再繼續。如果找到最後也沒找到需要的變量,則解釋器傳回undefined。
一般來說,一個函數在執行開始的時候,會給其中定義的變量劃分記憶體空間儲存,以備後面的語句所用,等到函數執行完畢傳回了,這些變量就被認為是無用的了。對應的記憶體空間也就被回收了。下次再執行此函數的時候,所有的變量又回到最初的狀态,重新指派使用。
但是如果這個函數内部又嵌套了另一個函數,而這個函數是有可能在外部被調用到的。并且這個内部函數又使用了外部函數的某些變量的話。這種記憶體回收機制就會出現問題:如果在外部函數傳回後,又直接調用了内部函數,那麼内部函數就無法讀取到他所需要的外部函數中變量的值了。是以JavaScript解釋器在遇到函數定義的時候,會自動把函數和他可能使用的變量(包括本地變量和父級和祖先級函數的變量(自由變量))一起儲存起來。也就是建構一個閉包,這些變量将不會被記憶體回收器所回收,隻有當内部的函數不可能被調用以後(例如被删除了,或者沒有了指針),才會銷毀這個閉包,而沒有任何一個閉包引用的變量才會被下一次記憶體回收啟動時所回收。
也就是說,有了閉包,嵌套的函數結構才可以運作,這也是符合我們的預期的.
在生活上,我們去看中共政辦事,找A辦事,你還先得找B門蓋個章,B說,你先得找C蓋個章,C說,這個東西不是我們的職權範圍…… 踢皮球,這就是非閉包。閉包就是負責到底,你找到A部門,A部門接待的那個人負責到底,他/她去協調B部門和C部門。
在工程上,閉包就是項目經理,負責排程項目所需要的資源。老闆、客戶有什麼事情,直接找項目經理即可,不用再去找其它的人。
閉包的定義及其優缺點概況
閉包 是指有權通路另一個函數作用域中的變量的函數,建立閉包的最常見的方式就是在一個函數内建立另一個函數,通過另一個函數通路這個函數的局部變量。
閉包的缺點
一般函數執行完畢後,局部活動對象就被銷毀,記憶體中僅僅儲存全局作用域。但閉包的情況不同!
閉包的缺點就是常駐記憶體,會增大記憶體使用量,使用不當很容易造成記憶體洩露。
使用閉包的好處
那麼使用閉包有什麼好處呢?使用閉包的好處是:
- 希望一個變量長期駐紮在記憶體中
- 避免全局變量的污染
- 私有成員的存在(設計私有的方法和變量。)
嵌套函數的閉包
function closure () {
var a = 1;
return function () {
console.log(a++);
};
}
var fun = closure();
fun();// 1 執行後 a++,,然後a還在~
fun();// 2
fun = null;//a被回收!!
閉包會使變量始終儲存在記憶體中,如果不當使用會增大記憶體消耗。
代碼示範JS閉包
talk is cheap ,show me code
一、全局變量的累加
var a = 1;
function abc(){
a++;
console.log(a);
}
abc();// 2
abc();// 3
二、局部變量
function abc(){
var a = 1;
a++;
console.log(a);
}
abc();// 2
abc();// 2
那麼怎麼才能做到變量a既是局部變量又可以累加呢?
三、局部變量的累加
function outer () {
var x = 10;
//函數嵌套函數
return function () {
x++;
alert(x);
};
}
//外部函數賦給變量y;
var y = outer();
//y函數調用一次,結果為11,相當于outer()();
y();
//y函數調用第二次,結果為12,實作了累加
y();
函數聲明與函數表達式
在js中我們可以通過關鍵字
function
來聲明一個函數:
function abc () {
console.log(123);
}
abc();
我們也可以通過一個"()"來将這個聲明變成一個表達式:
//然後通過()直接調用前面的表達式即可,是以函數可以不必寫名字;
(function () {
console.log(123);
})();
四、子產品化代碼,減少全局變量的污染
var abc = (function(){ //abc為外部匿名函數的傳回值
var a = 1;
return function(){
a++;
console.log(a);
}
})();
abc(); //2 ;調用一次abc函數,其實是調用裡面内部函數的傳回值
abc(); //3
五、私有成員的存在
var aaa = (function(){
var a = 1;
function bbb(){
a++;
console.log(a);
}
function ccc(){
a++;
alert(a);
}
return { b:bbb, c:ccc } //json結構
})();
aaa.b(); //2
aaa.c(); //3
六.使用匿名函數實作累加
function box(){
var age = 100;
return function(){ //匿名函數
age++;
return age;
};
}
var b = box();
console.log(b());
console.log(b()); //即alert(box()());
console.log(b());
console.log(b);
b = null; //解除引用,等待垃圾回收
七、在循環中直接找到對應元素的索引
window.onload = function () {
var aLi = document.getElementsByTagName('li')
for(let i =0 ;i<aLi.length;i++){
(function () {
//TODO
})(i)
}
};
九.記憶體洩露問題
由于
IE
的
js
對象和
DOM
對象使用不同的垃圾收集方法,是以閉包在
IE
中會導緻記憶體洩露問題,也就是無法銷毀駐留在記憶體中的元素
function closure(){
var oDiv = document.getElementById('oDiv');//oDiv用完之後一直駐留在記憶體中
oDiv.onclick = function () {
console.log('oDiv.innerHTML');//這裡用oDiv導緻記憶體洩露
};
}
closure();
//最後應将oDiv解除引用來避免記憶體洩露
function closure(){
var oDiv = document.getElementById('oDiv');
var test = oDiv.innerHTML;
oDiv.onclick = function () {
alert(test);
};
oDiv = null;
}