1.1.1 摘要
相信有C++、C#或Java等編程經驗的各位,對於this關鍵字再熟悉不過了。由於Javascript是一種物件導向的程式設計語言,它和C++、C#或Java一樣都包含this關鍵字,接下來我們將向大家介紹Javascript中的this關鍵字。
本文目錄
全域代碼中的this
函數中的this
參考型別
函數調用以及非參考型別
參考型別以及this的null值
函數作為構造器被調用時this的值
手動設定函數調用時this的值
1.1.2 本文
由於許多物件導向的程式設計語言都包含this關鍵字,我們會很自然地把this和物件導向的編程方式聯絡在一起,this通常指向利用構造器新建立出來的對象。而在ECMAScript中,this不僅僅只用來表示建立出來的對象,也是執行內容的一個屬性: 複製代碼 代碼如下:activeExecutionContext = {
// Variable object.
VO: {...},
this: thisValue
};
全域代碼中的this 複製代碼 代碼如下:// Global scope
// The implicit property of
// the global object
foo1 = "abc";
alert(foo1); // abc
// The explicit property of
// the global object
this.foo2 = "def";
alert(foo2); // def
// The implicit property of
// the global object
var foo3 = "ijk";
alert(foo3); // ijk
前面我們通過顯式和隱式定義了全域屬性foo1、foo2和foo3,由於this在全域上下文中,所以它的值是全域對象本身(在瀏覽器中是window object);接下來我們將介紹函數中的this。
函數中的this
當this在函數代碼中,情況就複雜多了,並且會引發很多的問題。
函數代碼中this值的第一個特性(同時也是最主要的特性)就是:它並非靜態綁定在函數上。
正如此前提到的,this的值是在進入執行內容(Excution context)的階段確定的,並且在函數代碼中的話,其值每次都不盡相同。
然而,一旦進入執行代碼階段,其值就不能改變了。如果要想給this賦一個新的值是不可能的,因為在那時this根本就不是變數了。
接下來,我們通過具體的例子說明函數中的this。
首先我們定義兩個對象foo和person,foo包含一個屬性name,而person包含屬性name和方法say(),具體的定義如下: 複製代碼 代碼如下:// Defines foo object.
var foo = {
name: "Foo"
};
// Defines person object.
var person = {
name: "JK_Rush",
say: function() {
alert(this === person);
alert("My name is " + this.name);
}
};
person.say(); // My name is JK_Rush
// foo and person object refer to
// the same function say
foo.say = person.say;
foo.say(); // My name is Foo.
通過上面的代碼,我們發現調用person的say()方法時,this指向person對象,當通過賦值方式使得foo的say()方法指向peson中的say()方法時。我們調用foo的say()方法,發現this不是指向person對象,而不是指向foo對象,這究竟是什麼原因呢?
首先,我們必須知道this的值在函數中是非靜態,它的值確定在函數調用時,具體代碼執行前,this的值是由啟用上下文代碼的調用者決定的,比如說,調用函數的外層上下文;更重要的是,this的值是由調用運算式的形式決定的,所以說this並非靜態綁定在函數上。
由於this並非靜態地綁定在函數上,那麼我們是否可以在函數中動態地修改this的值呢? 複製代碼 代碼如下:// Defines foo object.
var foo = {
name: "Foo"
};
// Defines person object.
var person = {
name: "JK_Rush",
say: function() {
alert(this === person);
this = foo; // ReferenceError
alert("My name is " + this.name);
}
};
person.say(); // My name is JK_Rush
現在我們在方法say()中,動態地修改this的值,當我們重新執行以上代碼,發現this的值引用錯誤。這是由於一旦進入執行代碼階段(函數調用時,具體代碼執行前),this的值就確定了,所以不能改變了。
參考型別
前面我們提到this的值是由啟用上下文代碼的調用者決定的,更重要的是,this的值是由調用運算式的形式決定的;那麼運算式的形式是如何影響this的值呢?
首先,讓我們介紹一個內部類型——參考型別,它的值可以用虛擬碼表示為一個擁有兩個屬性的對象分別是:base屬性(屬性所屬的對象)以及該base對象中的propertyName屬性: 複製代碼 代碼如下:// Reference type.
var valueOfReferenceType = {
base: mybase,
propertyName : 'mybasepropertyName'
};
參考型別的值只有可能是以下兩種情況:
當處理一個標識符的時候
或者進行屬性訪問的時候
標識符其實就是變數名、函數名、函數參數名以及全域對象的未受限的屬性。 複製代碼 代碼如下:// Declares varible.
var foo = 23;
// Declares a function
function say() {
// Your code.
}
中間過程中,對應的參考型別如下: 複製代碼 代碼如下:// Reference type.
var fooReference = {
base: global,
propertyName: 'foo'
};
var sayReference = {
base: global,
propertyName: 'say'
};
我們知道Javascript中屬性訪問有兩種方式:點符號和中括弧符號: 複製代碼 代碼如下:// Invokes the say method.
foo.say();
foo['say']();
由於say()方法是標識符,所以它對應於foo對象參考型別如下: 複製代碼 代碼如下:// Reference type.
var fooSayReference = {
base: foo,
propertyName: 'say'
};
我們發現say()方法的base屬性值為foo對象,那麼它對應的this屬性也將指向foo對象。
假設,我們直接調用say()方法,它對應的參考型別如下: 複製代碼 代碼如下:// Reference type.
var sayReference = {
base: global,
propertyName: 'say'
};
由於say()方法的base屬性值為global(通常來說是window object),那麼它對應的this屬性也將指向global。
函數上下文中this的值是函數調用者提供並且由當前調用運算式的形式而定的。如果在調用括弧()的左邊有參考型別的值,那麼this的值就會設定為該參考型別值的base對象。 所有其他情況下(非參考型別),this的值總是null。然而,由於null對於this來說沒有任何意義,因此會隱式轉換為全域對象。
函數調用以及非參考型別
前面我們提到,當調用括弧左側為非參考型別的時,this的值會設定為null,並最終隱式轉換為全域對象。
現在我們定義了一個匿名自執行函數,具體實現如下: 複製代碼 代碼如下:// Declares anonymous function
(function () {
alert(this); // null => global
})();
由於括弧()左邊的匿名函數是非參考型別對象(它既不是標識符也不屬於屬性訪問),因此,this的值設定為全域對象。 複製代碼 代碼如下:// Declares object.
var foo = {
bar: function () {
alert(this);
}
};
(foo.bar)(); // foo.
(foo.bar = foo.bar)(); // global?
(false || foo.bar)(); // global?
(foo.bar, foo.bar)(); // global
這裡注意到四個運算式中,只有第一個運算式this是指向foo對象的,而其他三個運算式則執行global。
現在我們又有疑問了:為什麼屬性訪問,但是最終this的值不是參考型別對象而是全域對象呢?
我們注意到運算式二是賦值(assignment operator),與運算式一組操作符不同的是,它會觸發調用GetValue方法(參見11.13.1中的第三步)。 最後返回的時候就是一個函數對象了(而不是參考型別的值了),這就意味著this的值會設定為null,最終會變成全域對象。
第三和第四種情況也是類似的——逗號操作符和OR邏輯運算式都會觸發調用GetValue方法,於是相應地就會丟失原先的參考型別值,變成了函數類型,this的值就變成了全域對象了。
參考型別以及this的null值
對於前面提及的情形,還有例外的情況,當調用運算式左側是參考型別的值,但是this的值卻是null,最終變為全域對象(global object)。 發生這種情況的條件是當參考型別值的base對象恰好為活躍對象(activation object)。
當內部子函數在父函數中被調用的時候就會發生這種情況,通過下面的示意代碼介紹活躍對象: 複製代碼 代碼如下:// Declares foo function.
function foo() {
function bar() {
alert(this); // global
}
// The same as AO.bar().
bar();
}
由於活躍對象(activation object)總是會返回this值為——null(用虛擬碼來表示AO.bar()就相當於null.bar()),然後,this的值最終會由null轉變為全域對象。
當函數調用包含在with語句的代碼塊中,並且with對象包含一個函數屬性的時候,就會出現例外的情況。with語句會將該對象添加到範圍鏈的最前面,在活躍對象的之前。 相應地,在參考型別的值(標識符或者屬性訪問)的情況下,base對象就不再是活躍對象了,而是with語句的對象。另外,值得一提的是,它不僅僅只針對內建函式,全域函數也是如此, 原因就是with對象掩蓋了範圍鏈中更高層的對象(全域對象或者活躍對象):
函數作為構造器被調用時this的值
函數作為建構函式時,我們通過new操作符建立執行個體對象是,它會調用Foo()函數的內部[[Construct]]方法;在對象建立之後,會調用內部的[[Call]]方法,然後所有Foo()函數中this的值會設定為新建立的對象。 複製代碼 代碼如下:// Declares constructor
function Foo() {
// The new object.
alert(this);
this.x = 10;
}
var foo = new Foo();
foo.x = 23;
alert(foo.x); // 23手動設定函數調用時this的值
Function.prototype原型上定義了兩個方法,允許手動指定函數調用時this的值。這兩個方法分別是:.apply()和.call()。這兩個方法都接受第一個參數作為調用上下文中this的值,而這兩個方法的區別是傳遞的參數,對於.apply()方法來說,第二個參數接受數群組類型(或者是類數組的對象,比如arguments), 而.call()方法接受任意多的參數(通過逗號分隔);這兩個方法只有第一個參數是必要的——this的值。
通過範例程式碼介紹call()方法和apply()方法的使用: 複製代碼 代碼如下:var myObject = {};
var myFunction = function(param1, param2) {
//setviacall()'this'points to my Object when function is invoked
this.foo = param1;
this.bar = param2;
//logs Object{foo = 'foo', bar = 'bar'}
console.log(this);
};
// invokes function, set this value to myObject
myFunction.call(myObject, 'foo', 'bar');
// logs Object {foo = 'foo', bar = 'bar'}
console.log(myObject);
call()方法第一個參數是必要的this值,接著我們可以傳遞任意多個參數,接著介紹apply()方法的使用。 複製代碼 代碼如下: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;
// logs Object{foo='foo', bar='bar'}
console.log(this);
};
// invoke function, set this value
myFunction.apply(myObject, ['foo', 'bar']);
// logs Object {foo = 'foo', bar = 'bar'}
console.log(myObject);
通過與call()方法對比,我們發現apply()方法和call()方法沒有太大的區別,只是方法簽名不一樣。
1.1.3 總結
本文介紹Javascript中this的使用,更重要的是協助我們能更好地理解this值在全域、函數、建構函式以及一些特例的情況中值的變化。
對於在函數上下文中this的值是函數調用者提供並且由當前調用運算式的形式而定的。如果在調用括弧()的左邊有參考型別的值,那麼this的值就會設定為該參考型別值的base對象。 所有其他情況下(非參考型別),this的值總是null。然而,由於null對於this來說沒有任何意義,因此會隱式轉換為全域對象。
對於特例情況,我們要記住賦值符、逗號操作符以及||邏輯運算式,會使this丟失原先的參考型別值,變成了函數類型,this的值就變成了全域對象了
參考
[1] http://dmitrysoshnikov.com/ecmascript/chapter-3-this/ 英文版
[2] http://blog.goddyzhao.me/post/11218727474/this 譯文
[3] https://net.tutsplus.com/tutorials/javascript-ajax/fully-understanding-the-this-keyword/
[作者]: JK_Rush