作者:Dmitry A. Soshnikov
編譯地址:http://dmitrysoshnikov.com/ecmascript/javascript-the-core/
譯者:JeremyWei
譯文地址:http://weizhifeng.net/javascript-the-core.html
這篇文章是「深入ECMA-262-3」系列的一個概覽和摘要。每個部分都包含了對應章節的鏈接,所以你可以閱讀它們以便對其有更深的理解。
面向讀者:經驗豐富的程序員,專家。
我們以思考對象的概念做為開始,這是ECMAScript的基礎。
對象
ECMAScript做為一個高度抽象的面向對象語言,是通過對象
來交互的。即使ECMAScript里邊也有基本類型
,但是,當需要的時候,它們也會被轉換成對象。
一個對象就是一個屬性集合,并擁有一個獨立的prototype(原型)對象。這個prototype可以是一個對象或者null。
讓我們看一個關于對象的基本例子。一個對象的prototype是以內部的[[Prototype]]
屬性來引用的。但是,在示意圖里邊我們將會使用__<internal-property>__
下劃線標記來替代兩個括號,對于prototype對象來說是:__proto__
。
對于以下代碼:
var foo = {
x: 10,
y: 20
};
我們擁有一個這樣的結構,兩個明顯的自身屬性和一個隱含的__proto__
屬性,這個屬性是對foo
原型對象的引用:
這些prototype有什么用?讓我們以原型鏈
(prototype chain)的概念來回答這個問題。
原型鏈
原型對象也是簡單的對象并且可以擁有它們自己的原型。如果一個原型對象的原型是一個非null的引用,那么以此類推,這就叫作原型鏈
。
原型鏈是一個用來實現繼承和共享屬性的有限對象鏈。
考慮這么一個情況,我們擁有兩個對象,它們之間只有一小部分不同,其他部分都相同。顯然,對于一個設計良好的系統,我們將會重用
相似的功能/代碼,而不是在每個單獨的對象中重復它。在基于類的系統中,這個代碼重用
風格叫作類繼承
-你把相似的功能放入類 A
中,然后類 B
和類 C
繼承類 A
,并且擁有它們自己的一些小的額外變動。
ECMAScript中沒有類的概念。但是,代碼重用的風格并沒有太多不同(盡管從某些方面來說比基于類(class-based)的方式要更加靈活)并且通過原型鏈
來實現。這種繼承方式叫作委托繼承
(delegation based inheritance)(或者,更貼近ECMAScript一些,叫作原型繼承
(prototype based inheritance))。
跟例子中的類A
,B
,C
相似,在ECMAScript中你創建對象:a
,b
,c
。于是,對象a
中存儲對象b
和c
中通用的部分。然后b
和c
只存儲它們自身的額外屬性或者方法。
var a = {
x: 10,
calculate: function (z) {
return this.x + this.y + z
}
};
var b = {
y: 20,
__proto__: a
};
var c = {
y: 30,
__proto__: a
};
// call the inherited method
b.calculate(30); // 60
c.calculate(40); // 80
足夠簡單,是不是?我們看到b
和c
訪問到了在對象a
中定義的calculate
方法。這是通過原型鏈實現的。
規則很簡單:如果一個屬性或者一個方法在對象自身
中無法找到(也就是對象自身沒有一個那樣的屬性),然后它會嘗試在原型鏈中尋找這個屬性/方法。如果這個屬性在原型中沒有查找到,那么將會查找這個原型的原型,以此類推,遍歷整個原型鏈(當然這在類繼承中也是一樣的,當解析一個繼承的方法
的時候-我們遍歷class鏈
( class chain))。第一個被查找到的同名屬性/方法會被使用。因此,一個被查找到的屬性叫作繼承
屬性。如果在遍歷了整個原型鏈之后還是沒有查找到這個屬性的話,返回undefined
值。
注意,繼承方法中所使用的this的值被設置為原始
對象,而并不是在其中查找到這個方法的(原型)對象。也就是,在上面的例子中this.y
取的是b
和c
中的值,而不是a
中的值。但是,this.x
是取的是a
中的值,并且又一次通過原型鏈
機制完成。
如果沒有明確為一個對象指定原型,那么它將會使用__proto__
的默認值-Object.prototype
。Object.prototype
對象自身也有一個__proto__
屬性,這是原型鏈的終點
并且值為null
。
下一張圖展示了對象a
,b
,c
之間的繼承層級:
注意: ES5標準化了一個實現原型繼承的可選方法,即使用Object.create
函數:
var b = Object.create(a, {y: {value: 20}});
var c = Object.create(a, {y: {value: 30}});
你可以在對應的章節獲取到更多關于ES5新API的信息。 ES6標準化了 __proto__
屬性,并且可以在對象初始化的時候使用它。
通常情況下需要對象擁有相同或者相似的狀態結構
(也就是相同的屬性集合),賦以不同的狀態值
。在這個情況下我們可能需要使用構造函數
(constructor function),其以指定的模式
來創造對象。
構造函數
除了以指定模式創建對象之外,構造函數
也做了另一個有用的事情-它自動地為新創建的對象設置一個原型對象
。這個原型對象存儲在ConstructorFunction.prototype
屬性中。
換句話說,我們可以使用構造函數來重寫上一個擁有對象b
和對象c
的例子。因此,對象a
(一個原型對象)的角色由Foo.prototype
來扮演:
// a constructor function
function Foo(y) {
// which may create objects
// by specified pattern: they have after
// creation own "y" property
this.y = y;
}
// also "Foo.prototype" stores reference
// to the prototype of newly created objects,
// so we may use it to define shared/inherited
// properties or methods, so the same as in
// previous example we have:
// inherited property "x"
Foo.prototype.x = 10;
// and inherited method "calculate"
Foo.prototype.calculate = function (z) {
return this.x + this.y + z;
};
// now create our "b" and "c"
// objects using "pattern" Foo
var b = new Foo(20);
var c = new Foo(30);
// call the inherited method
b.calculate(30); // 60
c.calculate(40); // 80
// let's show that we reference
// properties we expect
console.log(
b.__proto__ === Foo.prototype, // true
c.__proto__ === Foo.prototype, // true
// also "Foo.prototype" automatically creates
// a special property "constructor", which is a
// reference to the constructor function itself;
// instances "b" and "c" may found it via
// delegation and use to check their constructor
b.constructor === Foo, // true
c.constructor === Foo, // true
Foo.prototype.constructor === Foo // true
b.calculate === b.__proto__.calculate, // true
b.__proto__.calculate === Foo.prototype.calculate // true
);
這個代碼可以表示為如下關系:
這張圖又一次說明了每個對象都有一個原型。構造函數Foo
也有自己的__proto__
,值為Function.prototype
,Function.prototype
也通過其__proto__
屬性關聯到Object.prototype
。因此,重申一下,Foo.prototype
就是Foo
的一個明確的屬性,指向對象b
和對象c
的原型。
正式來說,如果思考一下分類
的概念(并且我們已經對Foo
進行了分類
),那么構造函數和原型對象合在一起可以叫作「類」。實際上,舉個例子,Python的第一級
(first-class)動態類(dynamic classes)顯然是以同樣的屬性/方法處理方案來實現的。從這個角度來說,Python中的類就是ECMAScript使用的委托繼承的一個語法糖。
注意: 在ES6中「類」的概念被標準化了,并且實際上以一種構建在構造函數上面的語法糖來實現,就像上面描述的一樣。從這個角度來看原型鏈成為了類繼承的一種具體實現方式:
// ES6
class Foo {
constructor(name) {
this._name = name;
}
getName() {
return this._name;
}
}
class Bar extends Foo {
getName() {
return super.getName() + ' Doe';
}
}
var bar = new Bar('John');
console.log(bar.getName()); // John Doe
有關這個主題的完整、詳細的解釋可以在ES3系列的第七章找到。分為兩個部分:7.1 面向對象.基本理論,在那里你將會找到對各種面向對象范例、風格的描述以及它們和ECMAScript之間的對比,然后在7.2 面向對象.ECMAScript實現,是對ECMAScript中面向對象的介紹。
現在,在我們知道了對象的基礎之后,讓我們看看運行時程序的執行
(runtime program execution)在ECMAScript中是如何實現的。這叫作執行上下文棧
(execution context stack),其中的每個元素也可以抽象成為一個對象。是的,ECMAScript幾乎在任何地方都和對象的概念打交道;)
執行上下文堆棧
這里有三種類型的ECMAScript代碼:全局
代碼、函數
代碼和eval
代碼。每個代碼是在其執行上下文
(execution context)中被求值的。這里只有一個全局上下文,可能有多個函數執行上下文以及eval
執行上下文。對一個函數的每次調用,會進入到函數執行上下文中,并對函數代碼類型進行求值。每次對eval
函數進行調用,會進入eval
執行上下文并對其代碼進行求值。
注意,一個函數可能會創建無數的上下文,因為對函數的每次調用(即使這個函數遞歸的調用自己)都會生成一個具有新狀態的上下文:
function foo(bar) {}
// call the same function,
// generate three different
// contexts in each call, with
// different context state (e.g. value
// of the "bar" argument)
foo(10);
foo(20);
foo(30);
一個執行上下文可能會觸發另一個上下文,比如,一個函數調用另一個函數(或者在全局上下文中調用一個全局函數),等等。從邏輯上來說,這是以棧的形式實現的,它叫作執行上下文棧
。
一個觸發其他上下文的上下文叫作caller
。被觸發的上下文叫作callee
。callee在同一時間可能是一些其他callee的caller(比如,一個在全局上下文中被調用的函數,之后調用了一些內部函數)。
當一個caller觸發(調用)了一個callee,這個caller會暫緩自身的執行,然后把控制權傳遞給callee。這個callee被push到棧中,并成為一個運行中
(活動的)執行上下文。在callee的上下文結束后,它會把控制權返回給caller,然后caller的上下文繼續執行(它可能觸發其他上下文)直到它結束,以此類推。callee可能簡單的返回
或者由于異常
而退出。一個拋出的但是沒有被捕獲的異常可能退出(從棧中pop)一個或者多個上下文。
換句話說,所有ECMAScript程序的運行時
可以用執行上下文(EC)棧
來表示,棧頂
是當前活躍
(active)上下文:
當程序開始的時候它會進入全局執行上下文
,此上下文位于棧底
并且是棧中的第一個
元素。然后全局代碼進行一些初始化,創建需要的對象和函數。在全局上下文的執行過程中,它的代碼可能觸發其他(已經創建完成的)函數,這些函數將會進入它們自己的執行上下文,向棧中push新的元素,以此類推。當初始化完成之后,運行時系統(runtime system)就會等待一些事件
(比如,用戶鼠標點擊),這些事件將會觸發一些函數,從而進入新的執行上下文中。
在下個圖中,擁有一些函數上下文EC1
和全局上下文Global EC
,當EC1
進入和退出全局上下文的時候,下面的棧將會發生變化:
這就是ECMAScript的運行時系統如何真正地管理代碼執行的。
更多有關ECMAScript中執行上下文的信息可以在對應的第一章 執行上下文中獲取。
像我們所說的,棧中的每個執行上下文都可以用一個對象來表示。讓我們來看看它的結構以及一個上下文到底需要什么狀態
(什么屬性)來執行它的代碼。
執行上下文
一個執行上下文可以抽象的表示為一個簡單的對象。每一個執行上下文擁有一些屬性(可以叫作上下文狀態
)用來跟蹤和它相關的代碼的執行過程。
在下圖中展示了一個上下文的結構:
除了這三個必需的屬性(一個變量對象
(variable objec),一個this
值以及一個作用域鏈
(scope chain))之外,執行上下文可以擁有任何附加的狀態,這取決于實現。
讓我們詳細看看上下文中的這些重要的屬性。
變量對象
變量對象是與執行上下文相關的數據作用域。它是一個與上下文相關的特殊對象,其中存儲了在上下文中定義的變量和函數聲明。
注意,函數表達式
(與函數聲明
相對)不包含
在變量對象之中。
變量對象是一個抽象概念。對于不同的上下文類型,在物理上,是使用不同的對象。比如,在全局上下文中變量對象就是全局對象本身
(這就是為什么我們可以通過全局對象的屬性名來關聯全局變量)。
讓我們在全局執行上下文中考慮下面這個例子:
var foo = 10;
function bar() {} // function declaration, FD
(function baz() {}); // function expression, FE
console.log(
this.foo == foo, // true
window.bar == bar // true
);
console.log(baz); // ReferenceError, "baz" is not defined
之后,全局上下文的變量對象(variable objec,簡稱VO)將會擁有如下屬性:
再看一遍,函數baz
是一個函數表達式
,沒有被包含在變量對象之中。這就是為什么當我們想要在函數自身之外訪問它的時候會出ReferenceError
。
注意,與其他語言(比如C/C++)相比,在ECMAScript中只有函數
可以創建一個新的作用域。在函數作用域中所定義的變量和內部函數在函數外邊是不能直接訪問到的,而且并不會污染全局變量對象。
使用eval
我們也會進入一個新的(eval
類型)執行上下文。無論如何,eval
使用全局的變量對象或者使用caller(比如eval
被調用時所在的函數)的變量對象。
那么函數和它的變量對象是怎么樣的?在函數上下文中,變量對象是以活動對象
(activation object)來表示的。
活動對象
當一個函數被caller所觸發
(被調用),一個特殊的對象,叫作活動對象
(activation object)將會被創建。這個對象中包含形參
和那個特殊的arguments
對象(是對形參的一個映射,但是值是通過索引來獲取)。活動對象
之后會做為函數上下文的變量對象
來使用。
換句話說,函數的變量對象也是一個同樣簡單的變量對象,但是除了變量和函數聲明之外,它還存儲了形參和arguments
對象,并叫作活動對象
。
考慮如下例子:
function foo(x, y) {
var z = 30;
function bar() {} // FD
(function baz() {}); // FE
}
foo(10, 20);
我們看下函數foo
的上下文中的活動對象(activation object,簡稱AO):
并且函數表達式
baz
還是沒有被包含在變量/活動對象中。
關于這個主題所有細節方面(像變量和函數聲明的提升問
(hoisting))的完整描述可以在同名的章節第二章 變量對象中找到。
注意,在ES5中變量對象
和活動對象
被并入了詞法環境
模型(lexical environments model),詳細的描述可以在對應的章節找到。
然后我們向下一個部分前進。眾所周知,在ECMAScript中我們可以使用內部函數
,然后在這些內部函數我們可以引用父
函數的變量或者全局
上下文中的變量。當我們把變量對象命名為上下文的作用域對象
,與上面討論的原型鏈相似,這里有一個叫作作用域鏈
的東西。
作用域鏈
作用域鏈是一個對象列表,上下文代碼中出現的標識符在這個列表中進行查找。
這個規則還是與原型鏈同樣簡單以及相似:如果一個變量在函數自身的作用域(在自身的變量/活動對象)中沒有找到,那么將會查找它父函數(外層函數)的變量對象,以此類推。
就上下文而言,標識符指的是:變量名稱
,函數聲明,形參,等等。當一個函數在其代碼中引用一個不是局部變量(或者局部函數或者一個形參)的標識符,那么這個標識符就叫作自由變量
。搜索這些自由變量
(free variables)正好就要用到作用域鏈
。
在通常情況下,作用域鏈
是一個包含所有父(函數)變量對象__加上
(在作用域鏈頭部的)函數自身變量/活動對象
的一個列表。但是,這個作用域鏈也可以包含任何其他對象,比如,在上下文執行過程中動態加入到作用域鏈中的對象-像with對象
或者特殊的catch從句
(catch-clauses)對象。
當解析
(查找)一個標識符的時候,會從作用域鏈中的活動對象開始查找,然后(如果這個標識符在函數自身的活動對象中沒有被查找到)向作用域鏈的上一層查找-重復這個過程,就和原型鏈一樣。
var x = 10;
(function foo() {
var y = 20;
(function bar() {
var z = 30;
// "x" and "y" are "free variables"
// and are found in the next (after
// bar's activation object) object
// of the bar's scope chain
console.log(x + y + z);
})();
})();
我們可以假設通過隱式的__parent__
屬性來和作用域鏈對象進行關聯,這個屬性指向作用域鏈中的下一個對象。這個方案可能在真實的Rhino代碼中經過了測試,并且這個技術很明確得被用于ES5的詞法環境中(在那里被叫作outer連接)。作用域鏈的另一個表現方式可以是一個簡單的數組。利用__parent__
概念,我們可以用下面的圖來表現上面的例子(并且父變量對象存儲在函數的[[Scope]]
屬性中):
在代碼執行過程中,作用域鏈可以通過使用with
語句和catch
從句對象來增強。并且由于這些對象是簡單的對象,它們可以擁有原型(和原型鏈)。這個事實導致作用域鏈查找變為兩個維度
:(1)首先是作用域鏈連接,然后(2)在每個作用域鏈連接上-深入作用域鏈連接的原型鏈(如果此連接擁有原型)。
對于這個例子:
Object.prototype.x = 10;
var w = 20;
var y = 30;
// in SpiderMonkey global object
// i.e. variable object of the global
// context inherits from "Object.prototype",
// so we may refer "not defined global
// variable x", which is found in
// the prototype chain
console.log(x); // 10
(function foo() {
// "foo" local variables
var w = 40;
var x = 100;
// "x" is found in the
// "Object.prototype", because
// {z: 50} inherits from it
with ({z: 50}) {
console.log(w, x, y , z); // 40, 10, 30, 50
}
// after "with" object is removed
// from the scope chain, "x" is
// again found in the AO of "foo" context;
// variable "w" is also local
console.log(x, w); // 100, 40
// and that's how we may refer
// shadowed global "w" variable in
// the browser host environment
console.log(window.w); // 20
})();
我們可以給出如下的結構(確切的說,在我們查找__parent__
連接之前,首先查找__proto__
鏈):
注意,不是在所有的實現中全局對象都是繼承自Object.prototype
。上圖中描述的行為(從全局上下文中引用「未定義」的變量x
)可以在諸如SpiderMonkey引擎中進行測試。
由于所有父變量對象都存在,所以在內部函數中獲取父函數中的數據沒有什么特別-我們就是遍歷作用域鏈去解析(搜尋)需要的變量。就像我們上邊提及的,在一個上下文結束之后,它所有的狀態和它自身都會被銷毀
。在同一時間父函數可能會返回
一個內部函數
。而且,這個返回的函數之后可能在另一個上下文中被調用。如果自由變量的上下文已經「消失」了,那么這樣的調用將會發生什么?通常來說,有一個概念可以幫助我們解決這個問題,叫作(詞法)閉包
,其在ECMAScript中就是和作用域鏈
的概念緊密相關的。
閉包
在ECMAScript中,函數是第一級
(first-class)對象。這個術語意味著函數可以做為參數傳遞給其他函數(在那種情況下,這些參數叫作「函數類型參數」(funargs,是"functional arguments"的簡稱))。接收「函數類型參數」的函數叫作高階函數
或者,貼近數學一些,叫作高階操作符
。同樣函數也可以從其他函數中返回。返回其他函數的函數叫作以函數為值
(function valued)的函數(或者叫作擁有函數類值
的函數(functions with functional value))。
這有兩個在概念上與「函數類型參數(funargs)」和「函數類型值(functional values)」相關的問題。并且這兩個子問題在"Funarg problem"
(或者叫作"functional argument"問題)中很普遍。為了解決整個"funarg problem"
,閉包
(closure)的概念被創造了出來。我們詳細的描述一下這兩個子問題(我們將會看到這兩個問題在ECMAScript中都是使用圖中所提到的函數的[[Scope]]
屬性來解決的)。
「funarg問題」的第一個子問題是「向上funarg問題」
(upward funarg problem)。它會在當一個函數從另一個函數向上返回(到外層)并且使用上面所提到的自由變量
的時候出現。為了在即使父函數上下文結束
的情況下也能訪問其中的變量,內部函數在被創建的時候
會在它的[[Scope]]
屬性中保存父函數的作用域鏈
。所以當函數被調用
的時候,它上下文的作用域鏈會被格式化成活動對象與[[Scope]]
屬性的和(實際上就是我們剛剛在上圖中所看到的):
Scope chain = Activation object + [[Scope]]
```
再次注意這個關鍵點-確切的說在`創建時刻`-函數會保存`父函數的`作用域鏈,因為確切的說這個`保存下來的作用域鏈`將會在未來的函數調用時用來查找變量。
```
function foo() {
var x = 10;
return function bar() {
console.log(x);
};
}
// "foo" returns also a function
// and this returned function uses
// free variable "x"
var returnedFunction = foo();
// global variable "x"
var x = 20;
// execution of the returned function
returnedFunction(); // 10, but not 20
```
這個類型的作用域叫作`靜態(或者詞法)作用域`。我們看到變量`x`在返回的`bar`函數的`[[Scope]]`屬性中被找到。通常來說,也存在`動態作用域`,那么上面例子中的變量`x`將會被解析成`20`,而不是`10`。但是,動態作用域在ECMAScript中沒有被使用。
「funarg問題」的第二個部分是`「向下funarg問題」`。這種情況下可能會存在一個父上下文,但是在解析標識符的時候可能會模糊不清。問題是:標識符該使用`哪個作用域`的值-以靜態的方式存儲在函數創建時刻的,還是在執行過程中以動態方式生成的(比如`caller`的作用域)?為了避免這種模棱兩可的情況并形成閉包,`靜態作用域`被采用:
```
// global "x"
var x = 10;
// global function
function foo() {
console.log(x);
}
(function (funArg) {
// local "x"
var x = 20;
// there is no ambiguity,
// because we use global "x",
// which was statically saved in
// [[Scope]] of the "foo" function,
// but not the "x" of the caller's scope,
// which activates the "funArg"
funArg(); // 10, but not 20
})(foo); // pass "down" foo as a "funarg"
```
我們可以斷定`靜態作用域`是一門語言擁有`閉包的必需條件`。但是,一些語言可能會同時提供動態和靜態作用域,允許程序員做選擇-什么應該包含(closure)在內和什么不應包含在內。由于在ECMAScript中只使用了靜態作用域(比如我們對于funarg問題的兩個子問題都有解決方案),所以結論是:`ECMAScript完全支持閉包`,技術上是通過函數的`[[Scope]]`屬性實現的。現在我們可以給閉包下一個準確的定義:
>閉包是一個代碼塊(在ECMAScript是一個函數)和以靜態方式/詞法方式進行存儲的所有父作用域的一個集合體。所以,通過這些存儲的作用域,函數可以很容易的找到自由變量。
注意,由于`每個`(標準的)函數都在創建的時候保存了`[[Scope]]`,所以理論上來講,ECMAScript中的`所有函數`都是`閉包`。
另一個需要注意的重要事情是,多個函數可能擁有`相同的父作用域`(這是很常見的情況,比如當我們擁有兩個內部/全局函數的時候)。在這種情況下,`[[Scope]]`屬性中存儲的變量是在擁有相同父作用域鏈的`所有函數之間共享`的。一個閉包對變量進行的修改會`體現`在另一個閉包對這些變量的讀取上:
```
function baz() {
var x = 1;
return {
foo: function foo() { return ++x; },
bar: function bar() { return --x; }
};
}
var closures = baz();
console.log(
closures.foo(), // 2
closures.bar() // 1
);
```
以上代碼可以通過下圖進行說明:
[](http://s0-weizhifeng-net.b0.upaiyun.com/images/tech/shared-scope.png)
確切來說這個特性在循環中創建多個函數的時候會使人非常困惑。在創建的函數中使用循環計數器的時候,一些程序員經常會得到非預期的結果,所有函數中的計數器都是`同樣`的值。現在是到了該揭開謎底的時候了-因為所有這些函數擁有同一個`[[Scope]]`,這個屬性中的循環計數器的值是最后一次所賦的值。
```
var data = [];
for (var k = 0; k < 3; k++) {
data[k] = function () {
alert(k);
};
}
data[0](); // 3, but not 0
data[1](); // 3, but not 1
data[2](); // 3, but not 2
```
這里有幾種技術可以解決這個問題。其中一種是在作用域鏈中提供一個額外的對象-比如,使用額外函數:
```
var data = [];
for (var k = 0; k < 3; k++) {
data[k] = (function (x) {
return function () {
alert(x);
};
})(k); // pass "k" value
}
// now it is correct
data[0](); // 0
data[1](); // 1
data[2](); // 2
```
對閉包理論和它們的實際應用感興趣的同學可以在[第六章 閉包](http://dmitrysoshnikov.com/ecmascript/chapter-6-closures/)中找到額外的信息。如果想獲取更多關于作用域鏈的信息,可以看一下同名的[第四章 作用域鏈](http://dmitrysoshnikov.com/ecmascript/chapter-4-scope-chain/)。
然后我們移動到下個部分,考慮一下執行上下文的最后一個屬性。這就是關于`this`值的概念。
This
--
>this是一個與執行上下文相關的特殊對象。因此,它可以叫作上下文對象(也就是用來指明執行上下文是在哪個上下文中被觸發的對象)。
`任何對象`都可以做為上下文中的`this`的值。我想再一次澄清,在一些對ECMAScript執行上下文和部分`this`的描述中的所產生誤解。`this`經常被`錯誤的`描述成是變量對象的一個屬性。這類錯誤存在于比如像[這本書](http://yuiblog.com/assets/High_Perf_JavaScr_Ch2.pdf)中(即使如此,這本書的相關章節還是十分不錯的)。
>再重復一次:this是執行上下文的一個屬性,而不是變量對象的一個屬性
這個特性非常重要,因為`與變量相反`,`this` `從不會參與到標識符解析過程`。換句話說,在代碼中當訪問`this`的時候,它的值是`直接`從執行上下文中獲取的,并`不需要任何作用域鏈查找`。`this`的值只在`進入上下文`的時候進行`一次`確定。
順便說一下,與ECMAScript相反,比如,Python的方法都會擁有一個被當作簡單變量的`self`參數,這個變量的值在各個方法中是相同的的并且在執行過程中可以被更改成其他值。在ECMAScript中,給`this`賦一個新值是`不可能的`,因為,再重復一遍,它不是一個變量并且不存在于變量對象中。
在全局上下文中,`this`就等于`全局對象本身`(這意味著,這里的`this`等于`變量對象`):
```
var x = 10;
console.log(
x, // 10
this.x, // 10
window.x // 10
);
```
在函數上下文的情況下,對`函數的每次調用`,其中的`this`值可能是`不同的`。這個`this`值是通過`函數調用表達式`(也就是函數被調用的方式)的形式由`caller`所提供的。舉個例子,下面的函數`foo`是一個`callee`,在全局上下文中被調用,此上下文為caller。讓我們通過例子看一下,對于一個代碼相同的函數,`this`值是如何在不同的調用中(函數觸發的不同方式),由caller給出`不同的`結果的:
```
// the code of the "foo" function
// never changes, but the "this" value
// differs in every activation
function foo() {
alert(this);
}
// caller activates "foo" (callee) and
// provides "this" for the callee
foo(); // global object
foo.prototype.constructor(); // foo.prototype
var bar = {
baz: foo
};
bar.baz(); // bar
(bar.baz)(); // also bar
(bar.baz = bar.baz)(); // but here is global object
(bar.baz, bar.baz)(); // also global object
(false || bar.baz)(); // also global object
var otherFoo = bar.baz;
otherFoo(); // again global object
```
為了深入理解`this`為什么(并且更本質一些-`如何`)在每個函數調用中可能會發生變化,你可以閱讀[第三章 This](http://dmitrysoshnikov.com/ecmascript/chapter-3-this/)。在那里,上面所提到的情況都會有詳細的討論。
總結
--
通過本文我們完成了對概要的綜述。盡管,它看起來并不像是「概要」)。對所有這些主題進行完全的解釋需要一本完整的書。我們只是沒有涉及到兩個大的主題:`函數`(和不同函數之間的區別,比如,`函數聲明`和`函數表達式`)和ECMAScript中所使用的`求值策略`(evaluation strategy )。這兩個主題是可以ES3系列的在對應章節找到:[第五章 函數](http://dmitrysoshnikov.com/ecmascript/chapter-5-functions/)和[第八章 求值策略](http://dmitrysoshnikov.com/ecmascript/chapter-8-evaluation-strategy/)。
如果你有留言,問題或者補充,我將會很樂意地在評論中討論它們。
祝學習ECMAScript好運!