深入理解JavaScript內部原理(3): this

來源:互聯網
上載者:User

本文是翻譯 http://dmitrysoshnikov.com/ecmascript/chapter-3-this/

概要

本文將進一步討論與執行內容密切相關的概念——this關鍵字。

事實證明,this這塊的內容非常的複雜,它在不同執行內容的情況下其值都會不同,並且會相應的引發一些問題。

很多程式員一看到this關鍵字,就會把它和物件導向的編程方式聯絡在一起,它指向利用構造器新建立出來的對象。在ECMAScript中,也支援this,然而, 正如大家所熟知的,this不僅僅只用來表示建立出來的對象。

接下來給大家揭開在ECMAScript中this神秘的面紗。

定義

This是執行內容的一個屬性:

activeExecutionContext = {  VO: {...},  this: thisValue};

這裡的VO就是前一章介紹的變數對象。

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

讓我們來詳細的看看this在ECMAScript中式如何表現的。

全域代碼中This的值

這種情況下,一切都變得非常簡單,this的值總是全域對象本身;因此,可以間接地擷取引用:

// 顯式定義全域對象的屬性this.a = 10; // global.a = 10alert(a); // 10 // 通過賦值給不受限的標識符來進行隱式定義b = 20;alert(this.b); // 20 // 通過變數聲明來進行隱式定義// 因為全域上下文中的變數對象就是全域對象本身var c = 30;alert(this.c); // 30
函數代碼中This的值

this在函數代碼中的時候,事情就變得有趣多了。這種情況下是最複雜的,並且會引發很多的問題。

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

正如此前提到的,this的值是在進入內容相關的階段確定的,並且在函數代碼中的話,其值每次都會大不相同

然而,一旦進入執行代碼階段,其值就不能改變了比方說,要想給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的值。

影響調用上下文中的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); // 10GetValue(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(); // 10y.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方法(參見11.13.1中的第三步)。 最後返回的時候就是一個函數對象了(而不是參考型別的值了),這就意味著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 == 30a.apply({b: 30}, [40]) // this === {b: 30}, this.b == 30, c == 40
總結

本文我們討論了ECMAScript中this關鍵字的特性(相對C++或者Java而言,真的可以說是特性)。洗完此文對大家理解this關鍵字在ECMAScript中的工作原理有所協助。

擴充閱讀
  • This
  • this關鍵字
  • new操作符
  • 函數調用
相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.