本文嘗試闡述Javascript中的上下文與做用域背後的機制,主要涉及到執行上下文(execution context)、做用域鏈(scope chain)、閉包(closure)、this
等概念。javascript
>> 原文 <<前端
執行上下文(簡稱上下文)決定了Js執行過程當中能夠獲取哪些變量、函數、數據,一段程序可能被分割成許多不一樣的上下文,每個上下文都會綁定一個變量對象(variable object),它就像一個容器,用來存儲當前上下文中全部已定義或可獲取的變量、函數等。位於最頂端或最外層的上下文稱爲全局上下文(global context),全局上下文取決於執行環境,如Node中的global
和Browser中的window
:java
須要注意的是,上下文與做用域(scope)是不一樣的概念。Js自己是單進程的,每當有function被執行時,就會產生一個新的上下文,這一上下文會被壓入Js的上下文堆棧(context stack)中,function執行結束後則被彈出,所以Js解釋器老是在棧頂上下文中執行。在生成新的上下文時,首先會綁定該上下文的變量對象,其中包括arguments
和該函數中定義的變量;以後會建立屬於該上下文的做用域鏈(scope chain),最後將this
賦予這一function所屬的Object,這一過程能夠經過下圖表示:git
上文提到this
被賦予function所屬的Object,具體來講,當function是定義在global對中時,this
指向global;當function做爲Object的方法時,this
指向該Object:github
var x = 1; var f = function(){ console.log(this.x); } f(); // -> 1 var ff = function(){ this.x = 2; console.log(this.x); } ff(); // -> 2 x // -> 2 var o = {x: "o's x", f: f}; o.f(); // "o's x"
上文提到,在function被執行時生成新的上下文時會先綁定當前上下文的變量對象,再建立做用域鏈。咱們知道function的定義是能夠嵌套在其餘function所建立的上下文中,也能夠並列地定義在同一個上下文中(如global)。做用域鏈實際上就是自下而上地將全部嵌套定義的上下文所綁定的變量對象串接到一塊兒,使嵌套的function能夠「繼承」上層上下文的變量,而並列的function之間互不干擾:閉包
var x = 'global'; function a(){ var x = "a's x"; function b(){ var y = "b's y"; console.log(x); }; b(); } function c(){ var x = "c's x"; function d(){ console.log(y); }; d(); } a(); // -> "a's x" c(); // -> ReferenceError: y is not defined x // -> "global" y // -> ReferenceError: y is not defined
若是理解了上文中提到的上下文與做用域鏈的機制,再來看閉包的概念就很清楚了。每一個function在調用時會建立新的上下文及做用域鏈,而做用域鏈就是將外層(上層)上下文所綁定的變量對象逐一串連起來,使當前function能夠獲取外層上下文的變量、數據等。若是咱們在function中定義新的function,同時將內層function做爲值返回,那麼內層function所包含的做用域鏈將會一塊兒返回,即便內層function在其餘上下文中執行,其內部的做用域鏈仍然保持着原有的數據,而當前的上下文可能沒法獲取原先外層function中的數據,使得function內部的做用域鏈被保護起來,從而造成「閉包」。看下面的例子:函數
var x = 100; var inc = function(){ var x = 0; return function(){ console.log(x++); }; }; var inc1 = inc(); var inc2 = inc(); inc1(); // -> 0 inc1(); // -> 1 inc2(); // -> 0 inc1(); // -> 2 inc2(); // -> 1 x; // -> 100
執行過程以下圖所示,inc
內部返回的匿名function在建立時生成的做用域鏈包括了inc
中的x
,即便後來賦值給inc1
和inc2
以後,直接在global context
下調用,它們的做用域鏈仍然是由定義中所處的上下文環境決定,並且因爲x
是在function inc
中定義的,沒法被外層的global context
所改變,從而實現了閉包的效果:this
咱們已經反覆提到執行上下文和做用域其實是經過function建立、分割的,而function中的this
與做用域鏈不一樣,它是由執行該function時當前所處的Object環境所決定的,這也是this
最容易被混淆用錯的一點。通常狀況下的例子以下:prototype
var name = "global"; var o = { name: "o", getName: function(){ return this.name } }; o.getName(); // -> "o"
因爲執行o.getName()
時getName
所綁定的this
是調用它的o
,因此此時this == o
;更容易搞混的是在closure條件下:code
var name = "global"; var oo = { name: "oo", getNameFunc: function(){ return function(){ return this.name; }; } } oo.getNameFunc()(); // -> "global"
此時閉包函數被return
後調用至關於:
getName = oo.getNameFunc(); getName(); // -> "global"
換一個更明顯的例子:
var ooo = { name: "ooo", getName: oo.getNameFunc() // 此時閉包函數的this被綁定到新的Object }; ooo.getName(); // -> "ooo"
固然,有時候爲了不閉包中的this
在執行時被替換,能夠採起下面的方法:
var name = "global"; var oooo = { name: "ox4", getNameFunc: function(){ var self = this; return function(){ return self.name; }; } }; oooo.getNameFunc()(); // -> "ox4"
或者是在調用時強行定義執行的Object:
var name = "global"; var oo = { name: "oo", getNameFunc: function(){ return function(){ return this.name; }; } } oo.getNameFunc()(); // -> "global" oo.getNameFunc().bind(oo)(); // -> "oo"
Js是一門頗有趣的語言,因爲它的不少特性是針對HTML中DOM的操做,於是顯得隨意而略失嚴謹,但隨着前端的不斷繁榮發展和Node的興起,Js已經再也不是"toy language"或是jQuery時代的"CSS擴展",本文提到的這些概念不管是對新手仍是從傳統Web開發中過分過來的Js開發人員來講,都很容易被混淆或誤解,但願本文能夠有所幫助。
寫這篇總結的緣由是我在Github上分享的Learn javascript in one picture,剛開始有人質疑這隻能算是一張語法表(syntax cheat sheet),根本不會涉及更深層的閉包、做用域等內容,可是出乎意料的是這個項目居然得到3000多個star,因此不能有始無終,以上。