javascript系列之this

來源:互聯網
上載者:User

引言    在這篇文章裡我們將會討論與執行內容直接相關的更多細節。討論的主題就是this關鍵字。實踐證明,這個主題是足夠難的並且在不同的執行內容中判定this的值經常會引發出許多問題。      許多編程人員習慣於認為程式設計語言中的this關鍵字和物件導向編程時密切相關的,準確的說就是this指向通過建構函式新建立的對象(譯者注:c++中this指標只能在一個類的成員函數中調用,它表示當前對象的地址)。在ECMAScript中這個理念也是被實現了的,然而,我們必須要明白,在ECMAScript中它不僅僅局限在指向建立的對象。下面讓我們詳細看看ECMAScript中確切的this值。 定義this是執行內容的一個屬性: 1 activeExecutionContext={2     VO:{...},3     this:thisValue4 }      在這裡VO是變數對象,在前面的章節中我們已經討論過。this與內容相關的執行代碼類型是直接相關的。一旦進入上下文它的值就被決定了,並且當代碼在上下文執行時其值是不可變的。讓我們更詳細的考慮這些情形。 全域代碼中的this值      在全域代碼中,一切事情都是很簡單的,this值就是全域對象本身,因此,間接引用this是可能的: 複製代碼1 //全域對象的顯式屬性定義2 this.a=10;//global.a=103 alert(a);//104 //通過對無類型標識符賦值的隱式定義5 b=20;6 alert(this.a);//207 //通過變數聲明隱式定義,因為全域內容相關的變數對象就是全域對象自身8 var c=30;9 alert(this.c);//30複製代碼函數代碼中的this值     當this使用在函數代碼中事情就變得有趣多了。這種情況是最難的也會引發很多的問題。在函數代碼中,this值的第一(可能也是最重要的)特性就是不能 靜態綁定到一個函數上。正如上面已經提到的,this值在進入上下文時就已經被決定了,在函數代碼的情形下,每次this值可以是完全不一樣的。       然而,在代碼運行時,this值是不可改變的。例如,不能給this賦一個新值,因為this不是變數(相反,python程式設計語言中,可以顯式的定義self對象,在啟動並執行時候self對象可重複改變) 複製代碼 1 var foo={x:10}; 2 var bar={ 3     x:20, 4     test:function(){ 5         alert(this===bar);//true 6         alert(this.x);//20 7         this=foo;//error,不能改變this值 8         alert(this.x);//如果這裡沒有錯的話,結果會是10,不是20 9     }10 };11 //當進入上下文時,this值由"bar"對象決定的。為什麼這樣-將在下面詳細討論12 bar.test();//true,2013 foo.test=bar.test;14 //然而,此時在這裡的this將指向"foo"-即使我們調用同一個函數15 foo.test();//false,10複製代碼     在函數代碼中是什麼影響了this值的變化?有多方面的因素。      首先,在一般的函數調用中,this由觸發上下文代碼的調用者提供。例如,調用函數的父上下文環境。並且this值由調用形式所決定(換句話說,就是由函 數調用的文法形式決定)。為了能夠在任何上下文中準確無誤的判定this值,必須去理解和記住這一個重要理論。準確的調用形式,例如調用函數的方式,影響 調用內容相關的的this值,別無其他了。 (當我們在一些關於javascript 的文章和書本中會看到“this值由函數如何定義來決定:如果是全域函數,this值被設定為全域對象,如果函數時一個對象的方法,this值設定為這個 對象”-這是一個錯誤的描述)。從下面我們可以看出即使是一般的全域函數,也能夠被產生不同this值的調用運算式所觸發。 複製代碼1 function foo(){2     alert(this);3 }4 foo();//global5 alert(foo===foo.prototype.constructor);//true6 //但是對於同一函數的另一種調用運算式形式,this值是不同的7 foo.prototype.constructor();//foo.prototype複製代碼調用一個被定義為對象方法的函數,但是this值不會被設定為這個對象也是同樣可能的: 複製代碼 1 var foo={ 2     bar:function(){ 3         alert(this); 4         alert(this===foo); 5     } 6 }; 7 foo.bar();//foo,true 8 var exampleFunc=foo.bar; 9 alert(exampleFunc===foo.bar);//true10 //同一函數的另一種調用運算式形式,得到不同的this值11 exampleFunc();//global,false複製代碼      調用形式是如何影響this值了?為了充分理解this值的確定值,必須詳細考慮一種內建類型-Reference類型 參考型別     使用虛擬碼編程思想。Reference類型的值可以由含兩個屬性的對象表示:base(例如,屬性屬於這個對象)和這個base中的屬性名稱: 1 var valueOfReferenceType={2     base:<base object>,3     propertyName:<property name>4 }Reference類型的值僅僅只有兩種情況: 當我們處理一個標識符時或者擷取屬性    標識符通過標識符擷取進程來處理,這會在第四節:範圍鏈中詳細考慮。在這裡我們僅僅關注這裡面一直有 Reference類型(對this值是很重要的) 的演算法返回情況 。     標識符是變數名,函數名,函數參數名和全域對象的未聲明(譯者註:前面沒有var)屬性名稱。例如,下面標識符的值:  1    var foo = 10;2    function bar() {} 操作符的中間結果,對應的Reference值如下: 複製代碼1 var fooReference={2     base;global,3     propertyName:'foo'4 };5 var barReference={6     base:global,7     propertyName:'bar'8 };複製代碼為了能夠從Reference類型的值中擷取一個對象的真實值,這裡有一個GetValue方法,在虛擬碼中描述如下: 複製代碼1    function GetValue(value) {3      if (Type(value) != Reference) {4        return value;5      }     7      var base = GetBase(value); 9      if (base === null) {10        throw new ReferenceError;11      } 13      return base.[[Get]](GetPropertyName(value));15    }複製代碼內建的[[Get]]方法返回對象屬性的真實值,也包括從原型鏈中解析出的繼承屬性: 1    GetValue(fooReference); // 102    GetValue(barReference); // function object "bar"我們也知道屬性擷取有兩種方式:點操作符(當屬性名稱是正確的標識符並且已知的),或者用中括弧: 1    foo.bar();2    foo['bar']();返回中間計算結果時,我們可以獲得Reference類型的值: 1    var fooBarReference = {2      base: foo,3      propertyName: 'bar'4    };  6    GetValue(fooBarReference); // function object "bar"    因此,在許多重要的場合,Reference類型的值是如何和函數上下文中的this值關聯的?來到本文的重要時刻。在函數上下文中決定this值的一般規則如下: 在函數上下文中的this值由調用者提供並由當前的調用運算式形式所決定(這個函數在文法意義上如何書寫的)。如果在調用(...)的左邊,有一個Reference類型的值,這樣this值被這設定為Reference類型的base對象。在其他所有的情形(例如與Reference類型不同的任何類型值),this值一直設定為null。但由於this值為null沒有任何意義,它會隱式的轉化為全域對象我們看下面的例子: 1 function foo() {2   return this;3 }  5 foo(); // global我們看到在調用括弧的左邊是一個參考型別值(因為foo是一個標示符): 1 var fooReference = {2   base: global,3   propertyName: 'foo'4 };相應地,this也設定為參考型別的base對象。即全域對象。同樣,使用屬性訪問器: 複製代碼1 var foo = {2   bar: function () {3     return this;4   }5 };6   7 foo.bar(); // foo複製代碼同樣,我們擁有一個參考型別的值,其base是foo對象,在函數bar啟用時將base設定給this。 1 var fooBarReference = {2   base: foo,3   propertyName: 'bar'4 };但是,如果用另一種方式啟用相同的函數,this的值將不同。 1 var test = foo.bar;2 test(); // global因為test作為標識符,產生了其他參考型別的值,該值的base(全域對象)被設定為this的值。 1 var testReference = {2   base: global,3   propertyName: 'test'4 }現在,我們可以很明確的說,為什麼用不同的形式啟用同一個函數會產生不同的this,答案在於不同的參考型別(type Reference)的中間值。 複製代碼 1 function foo() { 2   alert(this); 3 }  5 foo(); // global, because  7 var fooReference = { 8   base: global, 9   propertyName: 'foo'10 };  12 alert(foo === foo.prototype.constructor); // true 14 // another form of the call expression  16 foo.prototype.constructor(); // foo.prototype, because 18 var fooPrototypeConstructorReference = {19   base: foo.prototype,20   propertyName: 'constructor'21 };複製代碼另一個通過調用方式動態確定this的值的經典例子: 複製代碼 1 function foo() { 2   alert(this.bar); 3 } 4    5 var x = {bar: 10}; 6 var y = {bar: 20}; 7    8 x.test = foo; 9 y.test = foo;10   11 x.test(); // 1012 y.test(); // 20複製代碼函數調用和非參考型別    那麼,正如我們已經指出,當調用括弧的左邊不是參考型別而是其它類型,this的值自動化佈建為null,實際最終this的值被隱式轉換為全域對象。讓我們思考下面這種函數運算式: 1 (function  () {2   alert(this); // null => global3 })();     在這個例子中,我們有一個函數對象但不是參考型別的對象(因為它不是標示符,也不是屬性訪問器),相應地,this的值最終被設為全域對象。更多複雜的例子: 複製代碼 1 var foo = { 2   bar: function () { 3     alert(this); 4   } 5 }; 6    7 foo.bar(); // Reference, OK => foo 8 (foo.bar)(); // Reference, OK => foo 9   10 (foo.bar = foo.bar)(); // global?11 (false || foo.bar)(); // global?12 (foo.bar, foo.bar)(); // global?複製代碼   那麼,為什麼我們有一個屬性訪問器,它的中間值應該為參考型別的值,但是在某些調用中我們得到this的值不是base對象,而是global對象?問題出現在後面的三個調用,在執行一定的操作運算之後,在調用括弧的左邊的值不再是參考型別。 第一個例子很明顯———明顯的參考型別,結果是,this為base對象,即foo。 在 第二個例子中,分組操作符(譯者註:這裡的分組操作符就是指foo.bar外面的括弧"()")沒有實際意義,想想上面提到的,從參考型別中獲得一個對象 真正的值的方法,如GetValue 。相應的,在分組操作的傳回值中——我們得到的仍是一個參考型別。這就是this的值為什麼再次被設為base對象,即 foo。 第三個例子中,與分組操作符不同,賦值操作符調用了GetValue方法。返回的結果已經是函數對象(不是參考型別),這意味著this的值被設為null,實際最終結果是被設定為global對象。 第四個和第五個也是一樣——逗號操作符和邏輯操作符(OR)調用了GetValue 方法,相應地,我們失去了參考型別的值而得到了函數類型的值,所以this的值再次被設為global對象。 參考型別和this為null    有一種情況,如果調用方式確定了參考型別的值,不管怎樣,只要this的值被設定為null,其最終就會被隱式轉換成global。當參考型別值的 base對象是啟用物件時,就會導致這種情況。下面的執行個體中,內建函式被父函數調用,此時我們就能夠看到上面說的那種特殊情況。正如我們在 第二章學到的一樣,局部變數、內建函式、形式參數都儲存在給定函數的使用中的物件中。 複製代碼1 function foo() {2   function bar() {3     alert(this); // global4   }5   bar(); // the same as AO.bar()6 }複製代碼    啟用物件總是作為this的值返回——null(即虛擬碼AO.bar()相當於null.bar())。這裡我們再次回到上面描述的情況,this的值最終還是被設定為全域對象。     有一種情況除外:“在with語句中調用函數,且在with對象(譯者註:即下面例子中的__withObject)中包含函數名屬性時”。 with語句將其對象添加在範圍鏈最前端,即在啟用物件的前面。那麼對應的,參考型別有值(通過標識符或屬性訪問器),其base對象不再是啟用物件, 而是with語句的對象。順便提一句,這種情況不僅跟內建函式相關,還跟全域函數相關,因為with對象比範圍鏈裡的最前端的對象(全域對象或一個啟用 對象)還要靠前。 複製代碼 1 var x = 10; 2    3 with ({ 4    5   foo: function () { 6     alert(this.x); 7   }, 8   x: 20 9   10 }) {11   12   foo(); // 2013   14 }15   16 // because17   18 var  fooReference = {19   base: __withObject,20   propertyName: 'foo'21 };複製代碼    在catch語句的實際參數中的函數調用存在類似情況:在這種情況下,catch對象被添加到範圍的最前端,即在啟用物件或全域對象的前面。但 是,這個特定的行為被確認為是ECMA-262-3的一個bug,這個在新版的ECMA-262-5中修複了。修複後,在特定的啟用物件中,this指向 全域對象。而不是catch對象。 複製代碼 1 try { 2   throw function () { 3     alert(this); 4   }; 5 } catch (e) { 6   e(); // __catchObject - in ES3, global - fixed in ES5 7 } 8    9 // on idea10   11 var eReference = {12   base: __catchObject,13   propertyName: 'e'14 };15   16 // but, as this is a bug17 // then this value is forced to global18 // null => global19   20 var eReference = {21   base: global,22   propertyName: 'e'23 };複製代碼    同樣的情況出現的命名函數(函數的更多細節參考Chapter 5. Functions)的遞迴調用中。在函數的第一次調用中,base對象是父啟用物件(或全域對象),在遞迴調用中,base對象應該是儲存著函數運算式可選名稱的特定對象。但是,在這種情況下,this的值也總是被設定為global。 複製代碼1 (function  foo(bar) {2   3   alert(this);4   5   !bar && foo(1); // "should" be special object, but always (correct) global6   7 })(); // global 複製代碼建構函式中的this       還有一個在函數的上下文中與this的值相關的情況是:函數作為構造器調用時。 複製代碼1 function A() {2   alert(this); // newly created object, below - "a" object3   this.x = 10;4 }5   6 var a = new A();7 alert(a.x); // 10複製代碼在這個例子中,new操作符調用“A”函數內部的[[Construct]]方法,接著,在對象建立後,調用其內部的[[Call]]方法,所有相同的函數“A”都將this的值設定為新建立的對象。 手動設定函數調用的this值     在Function.prototype中定義了兩個方法允許手動設定函數調用時this的值,它們是apply和call方法(所有的函數都 可以訪問它們)。它們用接受的第一個參數作為this的值,this在調用的範圍中使用。這兩個方法的區別不大,對於apply,第二個參數必須是數組 (或者是類似數組的對象,如arguments,相反,call能接受任何參數。兩個方法必須的參數都是第一個參數值—this。 例如: 複製代碼 1 var b = 10; 3 function a(c) { 4   alert(this.b); 5   alert(c); 6 }   8 a(20); // this === global, this.b == 10, c == 2010 a.call({b: 20}, 30); // this === {b: 20}, this.b == 20, c == 3011 a.apply({b: 30}, [40]) // this === {b: 30}, this.b == 30, c == 40複製代碼總結    在這篇文章中我們討論了ECMAScript中this值的特性(和 c++/java中確實不同的)。我希望這篇文章能協助你更準確的理解ECMAScript中this關鍵字是如何工作的。  

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在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.