重學JavaScript深刻理解系列(三)

JavaScript深刻理解——this

概要

本文將進一步討論與執行上下文密切相關的概念--this關鍵字。程序員

事實證實,this這塊的內容很是的複雜,它在不一樣執行上下文的狀況下其值都會不一樣,而且會相應的引起一些問題。es6

不少程序員一看到this關鍵字,就會把它和麪向對象的編程方式聯繫在一塊兒,它指向利用構造器新建立出來的對象。在ECMAScript中,也支持this,然而, 正如你們所熟知的,this不只僅只用來表示建立出來的對象。算法

接下來給你們揭開在ECMAScript中this神祕的面紗。編程

定義

this是執行上下文的一個屬性:
activeExecutionContext = {
  VO: {...},
  this: thisValue
};
複製代碼

這裏的VO就是前一章介紹的變量對象數組

This與上下文的可執行代碼類型有關,其值在進入上下文階段就肯定了,而且在執行代碼階段是不能改變的

下面就來詳細對其介紹。 bash

全局代碼中的This的值

這種狀況下,一切都變得很是簡單,this的值老是全局對象自己;所以,能夠間接地獲取引用:
// 顯式定義全局對象的屬性
this.a = 10; // global.a = 10
alert(a); // 10
 
// 經過賦值給不受限的標識符來進行隱式定義
b = 20;
alert(this.b); // 20
 
// 經過變量聲明來進行隱式定義
// 由於全局上下文中的變量對象就是全局對象自己
var c = 30;
alert(this.c); // 30
複製代碼

函數代碼中的This的值

當this在函數代碼中的時候,事情就變得有趣多了。這種狀況下是最複雜的,而且會引起不少的問題。app

函數代碼中this值的第一個特性(同時也是最主要的特性)就是:它並不是靜態的綁定在函數上。ecmascript

正如此前提到的,this的值是在進入上下文的階段肯定的,而且在函數代碼中的話,其值每次都會大不相同。ide

然而,一旦進入執行代碼階段,其值就不能改變了。比方說,要想給this賦一個新的值是不可能的,由於this根本就不是變量(相反的,在Python語言中,它顯示定義的self對象是能夠在運行時隨意更改的):函數

var foo = {x: 10};
 
var bar = {
  x: 20,
  test: function () {
 
    alert(this === bar); // true
    alert(this.x); // 20
 
    this = foo; // error, 不能更改this的值
 
    alert(this.x); // 若是沒有錯誤,則其值爲10而不是20
 
  }
 
};
 
// 在進入上下文的時候,this的值就肯定了是「bar」對象
// 至於爲何,會在後面做詳細介紹
 
bar.test(); // true, 20
 
foo.test = bar.test;
 
// 可是,這個時候,this的值又會變成「foo」
// 縱然咱們調用的是同一個函數
 
foo.test(); // false, 10
複製代碼

所以,在函數代碼中影響this值的因素是有不少的。

首先,在通常的函數調用中,this的值是由激活上下文代碼的調用者決定的,好比說,調用函數的外層上下文。this的值是由調用表達式的形式決定的。

理解並謹記這一點是很是必要的,有利於在任何上下文中都能準確的肯定this的值。(俗話說就是誰調用指向誰,es6箭頭函數除外)

影響調用上下文中的this的值的只有多是調用表達式的形式,也就是調用函數的方式。 (一些關於JavaScript的文章和書籍中指出的「this的值取決於函數的定義方式,若是是全局函數,則this的值就會設置爲全局對象,若是是某個對象的方法,則this的值就會設置爲該對象」——這純屬扯淡,根本就是在誤人子弟)。 正如此前你們看到的,縱然是全局函數,this的值也會隨着函數調用方式的不一樣而不一樣:

function foo() {
  alert(this);
}
 
foo(); // global
 
alert(foo === foo.prototype.constructor); // true
 
// 然而,一樣的函數,以另一種調用方式的話,this的值就不一樣了
 
foo.prototype.constructor(); // foo.prototype
複製代碼

調用一個對象的某個方法的時候,this的值也有可能不是該對象的:

var foo = {
  bar: function () {
    alert(this);
    alert(this === foo);
  }
};
 
foo.bar(); // foo, true
 
var exampleFunc = foo.bar;
 
alert(exampleFunc === foo.bar); // true
 
// 一樣地,相同的函數以不一樣的調用方式,this的值也就不一樣了
 
exampleFunc(); // global, false
複製代碼

那麼,究竟調用表達式的方式是如何影響this的值的呢?爲了徹底搞明白這其中的奧妙,首先,這裏有必要先介紹一種內部類型——引用類型(the Reference type)。

引用類型

引用類型的值能夠用僞代碼表示爲一個擁有兩個屬性的對象——base屬性(屬性所屬的對象)以及該base對象中的propertyName屬性:
var valueOfReferenceType = {
 base: ,
 propertyName: 
};
複製代碼

引用類型的值只有多是如下兩種狀況

  1. 當處理一個標識符的時候
  2. 或者進行屬性訪問的時候

關於標識符的處理會在第四章——所用域鏈中做介紹,這裏咱們只要注意的是,此算法總返回一個引用類型的值(這對this的值是相當重要的)。

標識符其實就是變量名,函數名,函數參數名以及全局對象的未受限的屬性。以下所示:

var foo = 10;
function bar() {}
複製代碼

中間過程當中,對應的引用類型的值以下所示:

var fooReference = {
  base: global,
  propertyName: 'foo'
};
 
var barReference = {
  base: global,
  propertyName: 'bar'
};
複製代碼

要從引用類型的值中獲取一個對象實際的值須要GetValue方法,該方法用僞代碼能夠描述成以下形式

function GetValue(value) {
 
  if (Type(value) != Reference) {
    return value;
  }
 
  var base = GetBase(value);
 
  if (base === null) {
    throw new ReferenceError;
  }
 
  return base.[[Get]](GetPropertyName(value));
 
}
複製代碼

上述代碼中的[[Get]]方法返回了對象屬性實際的值,包括從原型鏈中繼承的屬性:

GetValue(fooReference); // 10
GetValue(barReference); // function object "bar"
複製代碼

對於屬性訪問來講,有兩種方式: 點符號(這時屬性名是正確的標識符而且提早已經知道了)或者中括號符號:

foo.bar();
foo['bar']();
複製代碼

中間過程當中,獲得以下的引用類型的值:

var fooBarReference = {
  base: foo,
  propertyName: 'bar'
};
 
GetValue(fooBarReference); // function object "bar"
複製代碼

問題又來了,引用類型的值又是如何影響函數上下文中this的值的呢?——很是重要。這也是本文的重點。總的來講,決定函數上下文中this的值的規則以下所示:

函數上下文中this的值是函數調用者提供而且由當前調用表達式的形式而定的。 若是在調用括號()的左邊,有引用類型的值,那麼this的值就會設置爲該引用類型值的base對象。 全部其餘狀況下(非引用類型),this的值老是null。然而,因爲null對於this來講沒有任何意義,所以會隱式轉換爲全局對象。

以下所示:

function foo() {
  return this;
}
 
foo(); // global
複製代碼

上述代碼中,調用括號的左側是引用類型的值(由於foo是標識符):

var fooReference = {
  base: global,
  propertyName: 'foo'
};
複製代碼

相應的,this的值會設置爲引用類型值的base對象,這裏就是全局對象。

屬性訪問也是相似的:

var foo = {
  bar: function () {
    return this;
  }
};
 
foo.bar(); // foo

複製代碼

一樣的,也是引用類型的值,它的base對象是foo對象,激活bar函數的時候,this的值就設置爲foo對象了:

var fooBarReference = {
  base: foo,
  propertyName: 'bar'
};
複製代碼

然而,一樣的函數以不一樣的激活方式的話,this的值就徹底不一樣了:

var test = foo.bar;
test(); // global
複製代碼

由於test也是標識符,這樣就產生了另外的引用類型的值,其中base對象(全局對象)
就是this的值:

var testReference = {
  base: global,
  propertyName: 'test'
};
複製代碼

至此,咱們就能夠精確的解釋,爲何一樣的函數,以不一樣的調用方式激活,this的值也會不一樣了——答案就是處理過程當中,是不一樣的引用類型的值:

function foo() {
  alert(this);
}
 
foo(); // global, 由於
 
var fooReference = {
  base: global,
  propertyName: 'foo'
};
 
alert(foo === foo.prototype.constructor); // true
 
// 另外一種調用方式
 
foo.prototype.constructor(); // foo.prototype, 由於
 
var fooPrototypeConstructorReference = {
  base: foo.prototype,
  propertyName: 'constructor'
};
複製代碼

以下是另一種(典型的)利用調用表達式來動態決定this值的例子:

function foo() {
  alert(this.bar);
}
 
var x = {bar: 10};
var y = {bar: 20};
 
x.test = foo;
y.test = foo;
 
x.test(); // 10
y.test(); // 20
複製代碼

函數調用以及非引用類型

正如此前提到過的,當調用括號左側爲非引用類型的時候,this的值會設置爲null,並最終變成全局對象。

咱們來考慮下以下表達式:

(function () {
 alert(this); // null => global
})();
複製代碼

上述例子中,有函數對象,但非引用類型對象(由於它不既不是標識符也不屬於屬性訪問),所以,this的值最終設置爲全局對象。

以下是更爲複雜的例子:

var foo = {
 bar: function () {
   alert(this);
 }
};

foo.bar(); // Reference, OK => foo
(foo.bar)(); // Reference, OK => foo

(foo.bar = foo.bar)(); // global?
(false || foo.bar)(); // global?
(foo.bar, foo.bar)(); // global?
複製代碼

看了上述代碼,你可能又有疑問了:爲何明明是屬性訪問,可是最終this的值不是base對象而是全局對象呢?

這裏主要疑問在最後三個表達式,這三個表達式添加了特定的操做以後,調用括號左側就再也不是引用類型的值了。

第一種狀況——很是明確,是引用類型,最終this的值設置爲base對象,foo。

第二種狀況有一個組操做符(grouping operator),該操做符不會觸發調用獲取引用類型實際值的方法,好比:GetValue方法。 相應的,處理組操做符中間過程當中——得到的仍然是一個引用類型的值,這也就解釋了爲何this的值設置成了base對象,foo。

第三種狀況是一個賦值操做符(assignment operator),與組操做符不一樣的是,它會觸發調用GetValue方法。 最後返回的時候就是一個函數對象了(而不是引用類型的值了),這就意味着this的值會設置爲null,最終會變成全局對象。

第四和第五種狀況也是相似的——逗號操做符和OR邏輯表達式都會觸發調用GetValue方法,因而相應地就會丟失原先的引用類型值,變成了函數類型,this的值就變成了全局對象了。

引用類型以及null (this的值)

有這麼一種狀況下,當調用表達式左側是引用類型的值,可是this的值倒是null,最終變爲全局對象。 發生這種狀況的條件是當引用類型值的base對象剛好爲活躍對象。

當內部子函數在父函數中被調用的時候就會發生這種狀況。正如變量對象介紹的, 局部變量,內部函數以及函數的形參都會存儲在指定函數的活躍對象中:

function foo() {
 function bar() {
   alert(this); // global
 }
 bar(); // 和AO.bar()是同樣的
}
複製代碼

活躍對象老是會返回this值爲——null(用僞代碼來表示,AO.bar()就至關於null.bar())。而後,如此前描述的,this的值最終會由null變爲全局對象。

當函數調用包含在with語句的代碼塊中,而且with對象包含一個函數屬性的時候,就會出現例外的狀況。with語句會將該對象添加到做用域鏈的最前面,在活躍對象的以前。 相應地,在引用類型的值(標識符或者屬性訪問)的狀況下,base對象就再也不是活躍對象了,而是with語句的對象。另外,值得一提的是,它不只僅只針對內部函數,全局函數也是如此, 緣由就是with對象掩蓋了做用域鏈中更高層的對象(全局對象或者活躍對象):

var x = 10;

with ({

 foo: function () {
   alert(this.x);
 },
 x: 20

}) {

 foo(); // 20

}

// because

var  fooReference = {
 base: __withObject,
 propertyName: 'foo'
};
複製代碼

當調用的函數剛好是catch從句的參數時,狀況也是相似的:在這種狀況下,catch對象也會添加到做用域鏈的最前面,在活躍對象和全局對象以前。 然而,這個行爲在ECMA-262-3中被指出是個bug,而且已經在ECMA-262-5中修正了;所以,在這種狀況下,this的值應該設置爲全局對象,而不是catch對象。

try {
 throw function () {
   alert(this);
 };
} catch (e) {
 e(); // __catchObject - in ES3, global - fixed in ES5
}

// on idea

var eReference = {
 base: __catchObject,
 propertyName: 'e'
};

// 然而,既然這是個bug
// 那就應該強制設置爲全局對象
// null => global

var eReference = {
 base: global,
 propertyName: 'e'
};
複製代碼

一樣的狀況還會在遞歸調用一個非匿名函數的時候發生。在第一次函數調用的時候,base對象是外層的活躍對象(或者全局對象), 在接下來的遞歸調用的時候——base對象應當是一個存儲了可選的函數表達式名字的特殊對象,然而,事實倒是,在這種狀況下,this的值永遠都是全局對象:

(function foo(bar) {

 alert(this);

 !bar && foo(1); // "should" be special object, but always (correct) global

})(); // global
複製代碼

但函數做爲構造器被調用時this的值

這裏要介紹的是函數上下文中關於this值的另一種狀況——當函數做爲構造器被調用的時候:
function A() {
 alert(this); // newly created object, below - "a" object
 this.x = 10;
}

var a = new A();
alert(a.x); // 10
複製代碼

在這種狀況下,new操做符會調用「A」函數的內部[[Construct]]。 在對象建立以後,會調用內部的[[Call]]函數,而後全部「A」函數中this的值會設置爲新建立的對象。

手動設置函數調用時this的值

Function.prototype上定義了兩個方法(所以,它們對全部函數而言都是可訪問的),容許手動指定函數調用時this的值。這兩個方法是:.apply和.call; 它們都接受第一個參數做爲調用上下文中this的值。而它們的不一樣點其實可有可無:對於.apply來講,第二個參數接受數組類型(或者是類數組的對象,好比arguments), 而.call方法接受任意多的參數。這兩個方法只有第一個參數是必要的——this的值。

以下所示:

var b = 10;
 
function a(c) {
  alert(this.b);
  alert(c);
}
 
a(20); // this === global, this.b == 10, c == 20
 
a.call({b: 20}, 30); // this === {b: 20}, this.b == 20, c == 30
a.apply({b: 30}, [40]) // this === {b: 30}, this.b == 30, c == 40
複製代碼

總結

本文咱們討論了ECMAScript中this關鍵字的特性(相對C++或者Java而言,真的能夠說是特性)。但願此文對你們理解this關鍵字在ECMAScript中的工做原理有所幫助。

原文地址
譯文地址

重學系列傳送門

重學JavaScript深刻理解系列(一)
重學JavaScript深刻理解系列(二)
重學JavaScript深刻理解系列(四)
重學JavaScript深刻理解系列(五)
重學JavaScript深刻理解系列(六)

相關文章
相關標籤/搜索