「譯文」JavaScript核心

原文:http://dmitrysoshnikov.com/ecmascript/javascript-the-core/javascript

  1. 對象
  2. 原型鏈
  3. 構造函數
  4. 執行上下文棧
  5. 執行上下文
  6. 變量對象
  7. 活動對象
  8. 做用域鏈
  9. 閉包
  10. This
  11. 總結

這篇文章是「深刻ECMA-262-3」系列的一個概覽和摘要。每一個部分都包含了對應章節的連接,因此你能夠閱讀它們以便對其有更深的理解。java

面向讀者:經驗豐富的程序員,專家。程序員

咱們以思考對象的概念作爲開始,這是ECMAScript的基礎。express

對象

ECMAScript作爲一個高度抽象的面嚮對象語言,是經過對象來交互的。即便ECMAScript裏邊也有基本類型,可是,當須要的時候,它們也會被轉換成對象。api

一個對象就是一個屬性集合,並擁有一個獨立的prototype(原型)對象。這個prototype能夠是一個對象或者null。

讓咱們看一個關於對象的基本例子。一個對象的prototype是之內部的[[Prototype]]屬性來引用的。可是,在示意圖裏邊咱們將會使用__<internal-property>__下劃線標記來替代兩個括號,對於prototype對象來講是:__proto__數組

對於如下代碼:閉包

var foo = {
  x: 10,
  y: 20
};

咱們擁有一個這樣的結構,兩個明顯的自身屬性和一個隱含的__proto__屬性,這個屬性是對foo原型對象的引用:ecmascript

這些prototype有什麼用?讓咱們以原型鏈(prototype chain)的概念來回答這個問題。ide

原型鏈

原型對象也是簡單的對象而且能夠擁有它們本身的原型。若是一個原型對象的原型是一個非null的引用,那麼以此類推,這就叫做原型鏈函數

原型鏈是一個用來實現繼承和共享屬性的有限對象鏈。

考慮這麼一個狀況,咱們擁有兩個對象,它們之間只有一小部分不一樣,其餘部分都相同。顯然,對於一個設計良好的系統,咱們將會重用類似的功能/代碼,而不是在每一個單獨的對象中重複它。在基於類的系統中,這個代碼重用風格叫做類繼承-你把類似的功能放入類 A中,而後類 B和類 C繼承類 A,而且擁有它們本身的一些小的額外變更。

ECMAScript中沒有類的概念。可是,代碼重用的風格並無太多不一樣(儘管從某些方面來講比基於類(class-based)的方式要更加靈活)而且經過原型鏈來實現。這種繼承方式叫做委託繼承(delegation based inheritance)(或者,更貼近ECMAScript一些,叫做原型繼承(prototype based inheritance))。

跟例子中的類ABC類似,在ECMAScript中你建立對象:abc。因而,對象a中存儲對象bc中通用的部分。而後bc只存儲它們自身的額外屬性或者方法。

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

足夠簡單,是否是?咱們看到bc訪問到了在對象a中定義的calculate方法。這是經過原型鏈實現的。

規則很簡單:若是一個屬性或者一個方法在對象自身中沒法找到(也就是對象自身沒有一個那樣的屬性),而後它會嘗試在原型鏈中尋找這個屬性/方法。若是這個屬性在原型中沒有查找到,那麼將會查找這個原型的原型,以此類推,遍歷整個原型鏈(固然這在類繼承中也是同樣的,當解析一個繼承的方法的時候-咱們遍歷class鏈( class chain))。第一個被查找到的同名屬性/方法會被使用。所以,一個被查找到的屬性叫做繼承屬性。若是在遍歷了整個原型鏈以後仍是沒有查找到這個屬性的話,返回undefined值。

注意,繼承方法中所使用的this的值被設置爲原始對象,而並非在其中查找到這個方法的(原型)對象。也就是,在上面的例子中this.y取的是bc中的值,而不是a中的值。可是,this.x是取的是a中的值,而且又一次經過原型鏈機制完成。

若是沒有明確爲一個對象指定原型,那麼它將會使用__proto__的默認值-Object.prototypeObject.prototype對象自身也有一個__proto__屬性,這是原型鏈的終點而且值爲null

下一張圖展現了對象abc之間的繼承層級:

注意:
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.prototypeFunction.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
);

以上代碼能夠經過下圖進行說明:

確切來講這個特性在循環中建立多個函數的時候會令人很是困惑。在建立的函數中使用循環計數器的時候,一些程序員常常會獲得非預期的結果,全部函數中的計數器都是一樣的值。如今是到了該揭開謎底的時候了-由於全部這些函數擁有同一個[[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

對閉包理論和它們的實際應用感興趣的同窗能夠在第六章 閉包中找到額外的信息。若是想獲取更多關於做用域鏈的信息,能夠看一下同名的第四章 做用域鏈

而後咱們移動到下個部分,考慮一下執行上下文的最後一個屬性。這就是關於this值的概念。

This

this是一個與執行上下文相關的特殊對象。所以,它能夠叫做上下文對象(也就是用來指明執行上下文是在哪一個上下文中被觸發的對象)。

任何對象均可以作爲上下文中的this的值。我想再一次澄清,在一些對ECMAScript執行上下文和部分this的描述中的所產生誤解。this常常被錯誤的描述成是變量對象的一個屬性。這類錯誤存在於好比像這本書中(即便如此,這本書的相關章節仍是十分不錯的)。再重複一次:

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。在那裏,上面所提到的狀況都會有詳細的討論。

總結

經過本文咱們完成了對概要的綜述。儘管,它看起來並不像是「概要」;)。對全部這些主題進行徹底的解釋須要一本完整的書。咱們只是沒有涉及到兩個大的主題:函數(和不一樣函數之間的區別,好比,函數聲明函數表達式)和ECMAScript中所使用的求值策略(evaluation strategy )。這兩個主題是能夠ES3系列的在對應章節找到:第五章 函數第八章 求值策略

若是你有留言,問題或者補充,我將會很樂意地在評論中討論它們。

祝學習ECMAScript好運!

做者:Dmitry A. Soshnikov 發佈於:2010-09-02

相關文章
相關標籤/搜索