1. 程式人生 > >【慕課網】JavaScript閉包和作用域

【慕課網】JavaScript閉包和作用域

1.閉包


阮一峰部落格閉包文章:http://www.ruanyifeng.com/blog/2009/08/learning_javascript_closures.html

類是有行為的資料,閉包是有資料的行為。

一、變數的作用域

要理解閉包,首先必須理解Javascript特殊的變數作用域。

變數的作用域無非就是兩種:全域性變數和區域性變數。

Javascript語言的特殊之處,就在於函式內部可以直接讀取全域性變數。

var n=999;

function f1(){
  alert(n);
}
f1(); // 999

另一方面,在函式外部自然無法讀取函式內的區域性變數。

function f1(){
  var n=999;
}
alert(n); // error

這裡有一個地方需要注意,函式內部宣告變數的時候,一定要使用var命令。如果不用的話,你實際上聲明瞭一個全域性變數!

function f1(){
  n=999;
}

f1();
alert(n); // 999

二、如何從外部讀取區域性變數

出於種種原因,我們有時候需要得到函式內的區域性變數。但是,前面已經說過了,正常情況下,這是辦不到的,只有通過變通方法才能實現。

那就是在函式的內部,再定義一個函式。

function f1(){
  var n=999;
  function f2(){
    alert(n); // 999
  }
}

在上面的程式碼中,函式f2就被包括在函式f1內部,這時f1內部的所有區域性變數,對f2都是可見的。但是反過來就不行,f2內部的區域性變數,對f1就是不可見的。這就是Javascript語言特有的"鏈式作用域"結構(chain scope),子物件會一級一級地向上尋找所有父物件的變數。所以,父物件的所有變數,對子物件都是可見的,反之則不成立。

既然f2可以讀取f1中的區域性變數,那麼只要把f2作為返回值,我們不就可以在f1外部讀取它的內部變量了嗎!

function f1(){

  var n=999;
  
  function f2(){
    alert(n); 
  }
  return f2;
  
}

var result=f1();
result(); // 999

三、閉包的概念

上一節程式碼中的f2函式,就是閉包。

各種專業文獻上的"閉包"(closure)定義非常抽象,很難看懂。我的理解是,閉包就是能夠讀取其他函式內部變數的函式。

由於在Javascript語言中,只有函式內部的子函式才能讀取區域性變數,因此可以把閉包簡單理解成"定義在一個函式內部的函式"。

所以,在本質上,閉包就是將函式內部和函式外部連線起來的一座橋樑。

四、閉包的用途

閉包可以用在許多地方。它的最大用處有兩個,一個是前面提到的可以讀取函式內部的變數,另一個就是讓這些變數的值始終保持在記憶體中。

怎麼來理解這句話呢?請看下面的程式碼。

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,可以在函式外部對函式內部的區域性變數進行操作。

五、使用閉包的注意點

1)由於閉包會使得函式中的變數都被儲存在記憶體中,記憶體消耗很大,所以不能濫用閉包,否則會造成網頁的效能問題,在IE中可能導致記憶體洩露。解決方法是,在退出函式之前,將不使用的區域性變數全部刪除。

2)閉包會在父函式外部,改變父函式內部變數的值。所以,如果你把父函式當作物件(object)使用,把閉包當作它的公用方法(Public Method),把內部變數當作它的私有屬性(private value),這時一定要小心,不要隨便改變父函式內部變數的值。

六、思考題

如果你能理解下面兩段程式碼的執行結果,應該就算理解閉包的執行機制了。

程式碼片段一。

  var name = "The Window";

  var object = {
    name : "My Object",

    getNameFunc : function(){
      return function(){
        return this.name;
      };

    }

  };

  alert(object.getNameFunc()());   // The Window

程式碼片段二。

  var name = "The Window";

  var object = {
    name : "My Object",

    getNameFunc : function(){
      var that = this;
      return function(){
        return that.name;
      };

    }

  };

  alert(object.getNameFunc()());   // My Object

慕課網教程:

閉包

//閉包
function outer(){
    var localVal = 30;
    return localVal;
}
outer();//30
//函式呼叫返回時,區域性變數localVal就被釋放掉了

function outer(){
    var localVal = 30;
    return function() {
        return localVal;
    }
}
 
var func = outer();
func();//30

localVal是不能被釋放的;因為呼叫outer後返回的是一個匿名函式,這個匿名函式仍然可以訪問外部的outer的區域性變數localVal,函式outer呼叫結束後,再次呼叫func,仍可以訪問到外部outer函式的區域性變數 ==》這就是閉包。

閉包,不同於一般的函式,它允許一個函式在立即詞法作用域外呼叫時,仍可訪問非本地變數。

回撥函式仍有能力呼叫外部函式的區域性變數。

優點:靈活和方便、封裝

缺點: 複雜作用域內,函式呼叫後,沒有被釋放掉,造成空間浪費,記憶體洩漏,效能消耗。

function fun1() {
           var value=30;
           return function () {
               return value;
           }
 };
//將fun1()賦值給變數fun2之後,此時函式會返回一個匿名函式,並將該匿名函式賦值給fun2       
var fun2=fun1(); 
console.log(fun2);   //列印function(){return value;}
//fun2()就相當於呼叫了該匿名函式,匿名函式返回的是fun1()作用域下的變數,在閉包中可以引用
//因此下面的將會列印30
console.log(fun2());  //列印30

閉包-常見錯誤之迴圈閉包

document.body.innerHTML="<div id=div1>aaa</div><div id=div2>bbb</div><div id=div3>ccc</div>";

for(var i=1;i<4;i++){
	document.getElementById("div"+i).addEventListener("click",function(){
		alert(i);//error: all are 4!
	})
}
改正後:
document.body.innerHTML="<div id=div1>aaa</div><div id=div2>bbb</div><div id=div3>ccc</div>";

for(var i=1;i<4;i++){
	(function(i){
		document.getElementById("div"+i).addEventListener("click",function(){
		alert(i);// 1,2,3 
	})
	})(i);
}

閉包-封裝

(function(){
	var _userId = 23492;
	var _typeId = 'item';
	var export = {};
	
	function converter(userId){
		return +userId;
	}
	export.getUserId = function(){
		return converter(_userId);
	}
	export.getTypeId = function(){
		return _typeId;
	}
	window.export = export;
}());

export.getUserId(); //23492
export.getTypeId(); //item

export._userId; //undefined
export._typeId; //undefined
export.converter; //undefined

附註:判斷this的指向(1-4優先順序逐級遞減,1優先順序最高,4優先順序最低):
1、函式被new呼叫,this指向由new新構造出來的這個物件;
2、函式通過call()、apply()、bind()呼叫,this指向被繫結的物件;
3、函式作為方法被呼叫,this指向這個物件(即常說的物件上下文);
4、預設(非嚴格模式)情況下,this指向window, 嚴格模式下,this指向undefined

2.作用域

作用域:全域性、函式、eval

作用域:JS只有全域性作用域和區域性(函式)作用域,沒有塊級作用域【ES6引入了塊級作用域】。所以在條件語句、迴圈語句等內部宣告的變數,在語句外也是可以訪問到的。

作用域鏈:內部作用域可以訪問外部作用域中的變數和函式,作用域鏈的前端為當前執行環境、終端為全域性環境。

var a = 10;
(function(){
	var b = 20;
})();
console.log(a); //10
console.log(b); //error,b in not defined

for(var item in {a: 1, b:2}){
console.log(item);
}
console.log(item);//item still in scope
js中沒有塊級作用域,在for()等裡面定義的變數,在外面依然可以得到,所以item在外部也可以訪問到

用new Function構造器是無法訪問同級作用域的變數的.

function outer(){
	var i = 1;
	var func = new Function("console.log(typeof i);");
	func(); //undefined
}
outer();

利用函式作用域封裝:

//利用匿名函式封裝將函式內部的變數變為區域性變數,而不是全域性變數,防止大量的全域性變數與類庫或程式碼引發衝突
(function(){
	//do sth here
	var a,b;
})(); 

//開頭用!或+,作用是將函式變為函式表示式,而不是函式宣告。如果省略!,以一個完整的function開頭的話,它會理解為函式宣告,這樣就會被前置處理掉,那麼就會留下一對(),會報語法錯誤。
!function(){
	//do sth here
	var a,b;
}();

3.ES3執行上下文

預編譯四部曲:
1.建立AO物件(Activation Object)(執行期上下文)
2.找形參和變數宣告,作為物件的屬性名,值為undefined
3.將實參和形參相統一
4.在函式體裡找函式宣告,值賦予函式體

執行上下文(Execution Context,EC):

在這裡插入圖片描述

JS直譯器如何找到我們定義的函式和變數?

變數物件(Variable Object,VO)是一個抽象概念中的“物件”,它用於儲存執行上下文中的:1.變數 2.函式宣告 3.函式引數

VO按照下列順序填充:

1.函式引數(若未傳入,初始化該引數值為undefined)
2.函式宣告(若發生命名衝突,會覆蓋)
3.變數宣告(初始化變數值為undefined,若發生命名衝突,會忽略)

執行上下文和變數物件

在這裡插入圖片描述

全域性執行上下文(瀏覽器):

在這裡插入圖片描述

全域性上下文/變數中含有Math、String、isNaN、…、window等,所以String(10)的時候就是在呼叫全域性global的[[global]].String(10),其他類似。

函式中的啟用物件

函式中的啟用物件AO,他與VO是不同執行階段的物件,AO在函式呼叫時用來初始化函式引數,之後AO也就和VO一樣(同一物件),繼續執行函式形參、函式宣告的初始化等工作。

在這裡插入圖片描述

變數初始化階段

function test(a,b){
	var c = 10;
	function d(){}
	var e = function _e(){};
	(function x(){});
	b = 20;
}
test(10);

AO(test) = {
	a:10,
	b:undefined,
	c:undefined,
	d:<ref to func ''d''>
	e:undefined
};

VO按照下列順序填充:
1.函式引數(若未傳入,初始化該引數值為undefined)
2.函式宣告(若發生命名衝突,會覆蓋)【所以有函式提前,因為在函式執行前的初始化階段就有把函式宣告放在了VO裡面】
3.變數宣告(初始化變數值為undefined,若發生命名衝突,會忽略)

針對上面程式碼解釋過程:
1.AO 為執行上下文,所以在呼叫 test(10); 語句後開始初始化填充
2.填充順序為: 函式引數(未傳入,初始化為undefined) -> 函式宣告(命名衝突,覆蓋其他) -> 變數宣告(初始化值為undefined,命名衝突,被忽略)
3. AO的填充是在初始化階段,而不是執行階段
var c = 10; // 其中 = 為賦值語句,在初始化並不執行,故AO 中 c:undefined,也就是初始化變數值為undefined
4. 函式宣告導致函式前置了

在這裡插入圖片描述

在這裡插入圖片描述

程式碼執行階段:

在這裡插入圖片描述

測試一下

在這裡插入圖片描述

alert(x);//function
 
var x = 10;
alert(x);// 10
x = 20;
 
function x(){
    alert(x);// 20
}
if(){
    var a = 1;
} else {
    var b = true;
}
 
alert(a);//1
alert(b);// undefined  var前置處理

解析過程:
先處理函式宣告function x(){},所以x為函式物件,再處理變數提前,x,a和b三個,a和b是undefined,變數宣告中重複x,所以x忽略,還是函式物件,所以最前面的alert(x)返回function,x賦值之後alert就是10了,之後是x=20,還是函式宣告是被提前的,所以function x(){}是被處理過的,直接到alert(x),所以輸出是20,為true所以a=1,前面說了var b被前置處理,是undefined。

4.小結

理解函式
函式宣告和表示式
this和呼叫方式
函式屬性與arguments(函式也是一種物件)
理解閉包和作用域
解析ES3執行上下文

this

1.全域性的this(瀏覽器)
2.一般函式的this(瀏覽器)
3.作為物件方法的函式的this //(this指向物件)
4.物件原型鏈上的this //this指向物件本身
5.get/set方法中的this //指向物件本身
6.構造器中的this function yy(){ this.a = 33;} var xx = new yy() // this 會指向空物件,並且空物件的原型指向一樣yy();的prototype屬性; 當沒有return或者return基本型別時,會返回this。如果是物件,則返回該物件。
7.apply,call a.call(b,xx,xx) 中this指向當前的作用域.這裡a方法在b作用域中執行this指向b;
8.var g = f.bind({a:“test”}); //this指向bind的引數物件 {a:“test”}