今天的教程來自天才的Cody Lindley的新書:《JavaScript啟蒙教程 / JavaScript Enlightenment》。他討論了令人迷惑的關鍵字this,以及確定和設定this的值的方法。
概念性的概述this
當一個函式建立後,一個關鍵字this就隨之(在後臺)建立,它連結到一個物件,而函式正是在這個物件中進行操作。換句話說,關鍵字this可在函式中使用,是對一個物件的引用,而函式正是該物件的屬性或方法。
讓我們來看這個物件:
1 2 3 4 5 6 7 8 9 10 11 |
<!DOCTYPE html><html lang="en"><body><script> var cody = { living:true, age:23, gender:'male', getGender:function(){return cody.gender;} }; console.log(cody.getGender()); // logs 'male' </script></body></html> |
1 |
注意在函式getGender裡,由於在cody物件內部,我們可以通過.來獲取gender屬性(也就是cody.gender)。也可以用this來獲取cody物件,因為this正是指向cody物件。
1 2 3 4 5 6 7 8 9 10 11 |
<!DOCTYPE html><html lang="en"><body><script> var cody = { living:true, age:23, gender:'male', getGender:function(){return this.gender;} }; console.log(cody.getGender()); // logs 'male' </script></body></html> |
this.gender中this指向cody物件,而getGender函式可以操作cody物件。
關於this的主題可能有點讓人感到困惑,其實不必如此。僅記住,通常,this指向的物件正是包含函式的物件,而不是函式本身(當然也有例外,例如採用關鍵字new或者call()和apply())。
重要提示
– 關鍵字this就像其他的變數,唯一不同就是你不能更改它。
– 不同於傳給函式的其他引數和變數,在呼叫函式的物件中,this是一個關鍵字(而不是屬性)。
如何確定this的值?
this傳遞給所有的函式,它的值取決於函式執行時何時被呼叫。這裡請注意,因為這是你需要記住的一個很特別的地方。
下面的程式碼中myObject物件有個屬性sayFoo,它指向函式sayFoo。當在全域性域中呼叫sayFoo函式時,this指向window物件。當myObject呼叫函式時,this指向的是myObject。
因為myObject有個叫foo的屬性,在這裡被使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<!DOCTYPE html><html lang="en"><body><script> var foo = 'foo'; var myObject = {foo: 'I am myObject.foo'}; var sayFoo = function() { console.log(this['foo']); }; // give myObject a sayFoo property and have it point to sayFoo function myObject.sayFoo = sayFoo; myObject.sayFoo(); // logs 'I am myObject.foo' 12 sayFoo(); // logs 'foo' </script></body></html> |
很清楚,this的值取決於函式什麼時候被呼叫。myObject.sayFoo和sayFoo都指向同樣的函式,但sayFoo()呼叫的上下文不同,this的值也就不同。下面是類似的程式碼,head物件(window)顯式使用,希望對你有用。
1 2 3 4 5 6 7 8 9 10 |
<!DOCTYPE html><html lang="en"><body><script> window.foo = 'foo'; window.myObject = {foo: 'I am myObject.foo'}; window.sayFoo = function() { ! console.log(this.foo); }; window.myObject.sayFoo = window.sayFoo; window.myObject.sayFoo(); window.sayFoo(); </script></body></html> |
確保當你有多個引用指向同一個函式的時候,你清楚的知道this的值是隨呼叫函式的上下文的不同而改變。
重要提示
– 除了this以外的所有變數和引數都屬於靜態變數範圍(lexical scope)。
在嵌入函式內this指向head物件
你可能想知道在嵌入在另外一個函式的函式中使用this會發生什麼事。不幸的是在ECMA 3中,this不遵循規律,它不指向函式屬於的物件,而是指向head物件(瀏覽器的window物件)。
在下面的程式碼,func2和func3中的this不再指向myObject,而是head物件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<!DOCTYPE html><html lang="en"><body><script> var myObject = { func1:function() { console.log(this); //logs myObject varfunc2=function() { console.log(this); //logs window, and will do so from this point on varfunc3=function() { console.log(this); //logs window, as it’s the head object }(); }(); } }; myObject.func1(); </script></body></html> |
然而在ECMAScript 5中,這個問題將會得到修正。現在,你應該意識到這個問題,尤其是當你將一個函式的值傳遞到另一個函式時。
看看下面的程式碼,將一個匿名函式傳給foo.func1,當在foo.func1中呼叫匿名函式(函式巢狀在另一個函式中),匿名函式中this將會指向是head物件。
1 2 3 4 5 6 7 8 9 10 |
<!DOCTYPE html><html lang="en"><body><script> var foo = { func1:function(bar){ bar(); //logs window, not foo console.log(this);//the this keyword here will be a reference to foo object } }; foo.func1(function(){console.log(this)}); </script></body></html> |
現在你不會忘了,如果包含this的函式在另一個函式中,或者被另一個函式呼叫,this的值將會指向的是head物件(再說一次,這將在ECMAScript 5中被修正。)
解決巢狀函式的問題
為了使this的值不丟失,你可以在父函式中使用一個作用域鏈(scope chain)來儲存對this進行引用。下面的程式碼中,使用一個叫that的變數,利用它的作用域,我們可以更好的儲存函式上下文。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<!DOCTYPE html><html lang="en"><body><script> var myObject = { myProperty:'Icanseethelight', myMethod:function() { var that=this; //store a reference to this (i.e.myObject) in myMethod scope varhelperFunctionfunction(){//childfunction var helperFunction function() { //childfunction //logs 'I can see the light' via scope chain because that=this console.log(that.myProperty); //logs 'I can see the light' console.log(this); // logs window object, if we don't use "that" }(); } } myObject.myMethod(); // invoke myMethod </script></body></html> |
控制this的值
this的值通常取決於呼叫函式的上下文(除非使用關鍵字new,稍後會為你介紹),但是你可以用apply()或call()指定觸發一個函式時this指向的物件,以改變/控制this的值。用這兩種方法就好像再說:“嘿,呼叫X函式,但讓Z物件來作this的值。”這樣做,JavaScript預設的this的值將被更改。
下面,我們建立了一個物件和一個函式,然後我們通過call()來觸發函式,所以函式中的this指向的是myOjbect。在myFunction函式中的this會操作myObject而不是head物件,這樣我們就改變了在myFunction中this指向的物件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<!DOCTYPE html><html lang="en"><body><script> var myObject = {}; var myFunction = function(param1, param2) { //setviacall()'this'points to my Object when function is invoked this.foo = param1; this.bar = param2; console.log(this); //logs Object{foo = 'foo', bar = 'bar'} }; myFunction.call(myObject, 'foo', 'bar'); // invoke function, set this value to myObject console.log(myObject) // logs Object {foo = 'foo', bar = 'bar'} </script></body></html> |
在上面的例子,我們用了call(),apply()也可適用於同樣用法,二者的不同之處在於引數如何傳給函式。用call(),引數用逗號分開,而用apply(),引數放在一個陣列中傳遞。下面是同樣的程式碼,但是用apply()。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<!DOCTYPE html><html lang="en"><body><script> var myObject = {}; var myFunction = function(param1, param2) { //set via apply(), this points to my Object when function is invoked this.foo=param1; this.bar=param2; console.log(this); // logs Object{foo='foo', bar='bar'} }; myFunction.apply(myObject, ['foo', 'bar']); // invoke function, set this value console.log(myObject); // logs Object {foo = 'foo', bar = 'bar'} </script></body></html> |
在自定義建構函式中用this
當函式用關鍵字new來觸發,this的值–由於在建構函式中宣告–指向例項本身。換種說法:在建構函式中,我們可以在物件真正建立之前,就用this來指定物件。這樣看來,this值的更改和call()或apply()相似。
下面,我們構造了一個建構函式Person,this指向建立的物件。當Person的物件建立後,this指向這個物件,並將屬性name放在物件內,值為傳給這個建構函式的引數值(name)。
1 2 3 4 5 6 7 8 9 10 11 |
<!DOCTYPE html><html lang="en"><body><script> var Person = function(name) { this.name = name || 'johndoe'; // this will refer to the instanc ecreated } var cody = new Person('Cody Lindley'); // create an instance, based on Person constructor console.log(cody.name); // logs 'Cody Lindley' </script></body></html> |
這樣,當用關鍵字new觸發建構函式時,this指向“要建立的物件”。那麼如果我們沒有用關鍵字new,this的值將會指向觸發Person的上下文——這時是head物件。讓我們來看看下面的程式碼。
1 2 3 4 5 6 7 8 9 10 11 |
<!DOCTYPE html><html lang="en"><body><script> var Person = function(name) { this.name=name||'johndoe'; } var cody = Person('Cody Lindley'); // notice we did not use 'new' console.log(cody.name); // undefined, the value is actually set at window.name console.log(window.name); // logs 'Cody Lindley' </script></body></html> |
在prototype方法內的this指向構造例項
當一個方法作為一個建構函式的prototype屬性時,這個方法中的this指向觸發方法的例項。這裡,我們有一個Person()的建構函式,它需要person的全名(full name),為了獲得全名(full name),我們在Person.prototype中加入了一個whatIsMyFullName方法,所有的Person例項都繼承該方法。這個方法中的this指向觸發這個方法的例項(以及它的屬性)。
下面我建立了兩個Person物件(cody和lisa),繼承的whatIsMyFullName方法包含的this就指向這個例項。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
<!DOCTYPE html><html lang="en"><body><script> var Person = function(x){ if(x){this.fullName = x}; }; Person.prototype.whatIsMyFullName = function() { return this.fullName; // 'this' refers to the instance created from Person() } var cody = new Person('cody lindley'); var lisa = new Person('lisa lindley'); // call the inherited whatIsMyFullName method, which uses this to refer to the instance console.log(cody.whatIsMyFullName(), lisa.whatIsMyFullName()); /* The prototype chain is still in effect, so if the instance does not have a fullName property, it will look for it in the prototype chain. Below, we add a fullName property to both the Person prototype and the Object prototype. See notes. */ Object.prototype.fullName = 'John Doe'; var john = new Person(); // no argument is passed so fullName is not added to instance console.log(john.whatIsMyFullName()); // logs 'John Doe' </script></body></html> |
在prototype物件內的方法裡使用this,this就指向例項。如果例項不包含屬性的話,prototype查詢便開始了。
提示
– 如果this指向的物件不包含想要查詢的屬性,那麼這時對於任何屬性都適用的法則在這裡也適用,也就是,屬性會沿著prototype鏈(prototype chain)上“尋找”。所以在我們的例子中,如果例項中不包含fullName屬性,那麼fullName就會查詢Person.prototype.fullName,然後是Object.prototype.fullName。
英文原文:Fully Understanding the this Keyword 編譯:伯樂線上 – 唐小娟
【如需轉載,請標註並保留原文連結、譯文連結和譯者等資訊,謝謝合作!】