閉包的理解
因為內部函數在被創建時,其作用域鏈對外部函數對應的變量對象存在一個引用,而JS采用引用計數的方法進行內存管理,所以當外部函數被執行完畢后,其對應的變量對象不會被回收,這樣就發生了閉包,在外部函數執行完畢后,我們在內部函數中仍然可以訪問外部函數作用域中的變量。
閉包就是函數的局部變量集合,只是這些局部變量在函數返回后會繼續存在。
閉包就是就是函數的“堆棧”在函數返回后并不釋放,我們也可以理解為這些函數堆棧并不在棧上分配而是在堆上分配,當在一個函數內定義另外一個函數就會產生閉包而當一個函數中的變量或者函數有權訪問另一個函數作用域中的變量或者函數時就產生了閉包了
嵌套的函數定義
我們只能通過變通的辦法來訪問函數的局部變量,一般來說,這個變通的辦法就是在函數內部再定義一個函數,因為一個函數不僅可以訪問全局變量,還可以訪問它的外部函數(Outer function)定義的局部變量,比如下面的代碼:
var global_var = "I'm global";
function outer() {
var local_var = "I'm local";
return function inner() {
console.log("local: " + local_var);
};
}
outer()(); // 輸出:local: I'm local
函數 inner 不僅有自己的內部作用域,還可以訪問全局變量,也可以訪問它外部的 outer 函數定義的所有局部變量。我們知道函數是 JavaScript 的一等公民,可以作為其他函數的參數或者作為函數的返回值,這里我們把 inner 函數返回,這樣我們就通過變通的方法在 outer 函數外部訪問了 outer 函數內部定義的局部變量。那這里的內部函數 inner 就構成了閉包。在 JavaScript 語言中,閉包的定義可以簡化為嵌套定義在函數內部的函數。
instanceof和typeof都能用來判斷一個變量是否為空或是什么類型的變量。typeof用以獲取一個變量的類型,typeof一般只能返回如下幾個結果:number,boolean,string,function,object,undefined。我們可以使用typeof來獲取一個變量是否存在,如if(typeof a!="undefined"){},而不要去使用if(a)因為如果a不存在(未聲明)則會出錯,對于Array,Null等特殊對象使用typeof一律返回object,這正是typeof的局限性。
function f1(){
var n=999;
nAdd=function(){n+=1}
function f2(){
alert(n);
}
return f2;
}
var result=f1();
result(); // 999
nAdd();
result(); // 1000
在這段代碼中,result實際上就是閉包f2函數。它一共運行了兩次,第一次的值是999,第二次的值是1000。這證明了,函數f1中的局部變量n一直保存在內存中,并沒有在f1調用后被自動清除。
為什么會這樣呢?原因就在于f1是f2的父函數,而f2被賦給了一個全局變量,這導致f2始終在內存中,而f2的存在依賴于f1,因此f1也始終在內存中,不會在調用結束后,被垃圾回收機制(garbage collection)回收。
這段代碼中另一個值得注意的地方,就是"nAdd=function(){n+=1}"這一行,首先在nAdd前面沒有使用var關鍵字,因此nAdd是一個全局變量,而不是局部變量。其次,nAdd的值是一個匿名函數(anonymous function),而這個匿名函數本身也是一個閉包,所以nAdd相當于是一個setter,可以在函數外部對函數內部的局部變量進行操作。
立即執行函數
立即執行函數能配合閉包保存狀態。
像普通的函數傳參一樣,立即執行函數也能傳參數。如果在函數內部再定義一個函數,而里面的那個函數能引用外部的變量和參數(閉包),利用這一點,我們能使用立即執行函數鎖住變量保存狀態。
// 并不會像你想象那樣的執行,因為i的值沒有被鎖住
// 當我們點擊鏈接的時候,其實for循環已經執行完了
// 于是在點擊的時候i的值其實已經是elems.length了
var elems = document.getElementsByTagName( 'a' );
for ( var i = 0; i < elems.length; i++ ) {
elems[ i ].addEventListener( 'click', function(e){
e.preventDefault();
alert( 'I am link #' + i );
}, 'false' );
}
// 這次我們得到了想要的結果
// 因為在立即執行函數內部,i的值傳給了lockedIndex,并且被鎖在內存中
// 盡管for循環結束后i的值已經改變,但是立即執行函數內部lockedIndex的值并不會改變
var elems = document.getElementsByTagName( 'a' );
for ( var i = 0; i < elems.length; i++ ) {
(function( lockedInIndex ){
elems[ i ].addEventListener( 'click', function(e){
e.preventDefault();
alert( 'I am link #' + lockedInIndex );
}, 'false' );
})( i );
}
內存泄露
function assignHandler() {
var el = document.getElementById('demo');
el.onclick = function() {
console.log(el.id);
}
}
assignHandler();
以上代碼創建了作為el元素事件處理程序的閉包,而這個閉包又創建了一個循環引用,只要匿名函數存在,el的引用數至少為1,因些它所占用的內存就永完不會被回收。
function assignHandler() {
var el = document.getElementById('demo');
var id = el.id;
el.onclick = function() {
console.log(id);
}
el = null;
}
assignHandler();
把變量el設置null能夠解除DOM對象的引用,確保正常回收其占用內存。
function fun(n,o) {
console.log(o)
return {
fun:function(m){
return fun(m,n);
}
};
}
var a = fun(0); a.fun(1); a.fun(2); a.fun(3);//undefined,?,?,?
var b = fun(0).fun(1).fun(2).fun(3);//undefined,?,?,?
var c = fun(0).fun(1); c.fun(2); c.fun(3);//undefined,?,?,?
//問:三行a,b,c的輸出分別是什么?
//這是一道非常典型的JS閉包問題。其中嵌套了三層fun函數,搞清楚每層fun的函數是那個fun函數尤為重要。
//答案: //a: undefined,0,0,0 //b: undefined,0,1,2 //c: undefined,0,1,1
JavaScript的執行上下文生成之后,會創建一個叫做變量對象的特殊對象(具體會在下一篇文章與執行上下文一起總結),JavaScript的基礎數據類型往往都會保存在變量對象中。
嚴格意義上來說,變量對象也是存放于堆內存中,但是由于變量對象的特殊職能,我們在理解時仍然需要將其于堆內存區分開來。
基礎數據類型都是一些簡單的數據段,JavaScript中有5中基礎數據類型,分別是Undefined、Null、Boolean、Number、String。基礎數據類型都是按值訪問,因為我們可以直接操作保存在變量中的實際的值。