深入理解JavaScript系列(13) This? Yes,this!_javascript技巧

來源:互聯網
上載者:User
介紹
在這篇文章裡,我們將討論跟執行內容直接相關的更多細節。討論的主題就是this關鍵字。實踐證明,這個主題很難,在不同執行內容中this的確定經常會發生問題。

許多程式員習慣的認為,在程式語言中,this關鍵字與物件導向程式開發緊密相關,其完全指向由構造器新建立的對象。在ECMAScript規範中也是這樣實現的,但正如我們將看到那樣,在ECMAScript中,this並不限於只用來指向新建立的對象。

英文翻譯: Dmitry A. Soshnikov在Stoyan Stefanov的協助下
發布: 2010-03-07
http://dmitrysoshnikov.com/ecmascript/chapter-3-this/

俄文原文: Dmitry A. Soshnikov
修正: Zeroglif
發布: 2009-06-28;
更新:2010-03-07
http://dmitrysoshnikov.com/ecmascript/ru-chapter-3-this/

本文絕大部分內容參考了:http://www.denisdeng.com/?p=900
部分句子參考了:justin的中文翻譯
複製代碼
讓我們更詳細的瞭解一下,在ECMAScript中this到底是什嗎?

定義
this是執行內容中的一個屬性:
複製代碼 代碼如下:

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

這裡VO是我們前一章討論的變數對象。

this與上下文中可執行代碼的類型有直接關係,this值在進入上下文時確定,並且在上下文運行期間永久不變。

下面讓我們更詳細研究這些案例:

全域代碼中的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時很有趣,這種情況很難且會導致很多問題。

這種類型的代碼中,this值的首要特點(或許是最主要的)是它不是靜態綁定到一個函數。

正如我們上面曾提到的那樣,this是進入上下文時確定,在一個函數代碼中,這個值在每一次完全不同。

不管怎樣,在代碼運行時的this值是不變的,也就是說,因為它不是一個變數,就不可能為其分配一個新值(相反,在Python程式設計語言中,它明確的定義為對象本身,在運行期間可以不斷改變)。
複製代碼 代碼如下:

var foo = {x: 10};

var bar = {
x: 20,
test: function () {

alert(this === bar); // true
alert(this.x); // 20

this = foo; // 錯誤,任何時候不能改變this的值

alert(this.x); // 如果不出錯的話,應該是10,而不是20

}

};

// 在進入內容相關的時候
// this被當成bar對象
// determined as "bar" object; why so - will
// be discussed below in detail

bar.test(); // true, 20

foo.test = bar.test;

// 不過,這裡this依然不會是foo
// 儘管調用的是相同的function

foo.test(); // false, 10

那麼,影響了函數代碼中this值的變化有幾個因素:

首先,在通常的函數調用中,this是由啟用上下文代碼的調用者來提供的,即調用函數的父上下文(parent context )。this取決於調用函數的方式。

為了在任何情況下準確無誤的確定this值,有必要理解和記住這重要的一點。正是調用函數的方式影響了調用的上下文中的this值,沒有別的什麼(我們可以在一些文章,甚至是在關於javascript的書籍中看到,它們聲稱:“this值取決於函數如何定義,如果它是全域函數,this設定為全域對象,如果函數是一個對象的方法,this將總是指向這個對象。–這絕對不正確”)。繼續我們的話題,可以看到,即使是正常的全域函數也會被呼叫者式的不同形式啟用,這些不同的調用方式導致了不同的this值。
複製代碼 代碼如下:

function foo() {
alert(this);
}

foo(); // global

alert(foo === foo.prototype.constructor); // true

// 但是同一個function的不同的調用運算式,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

// 再一次,同一個function的不同的調用運算式,this是不同的

exampleFunc(); // global, false

那麼,調用函數的方式如何影響this值?為了充分理解this值的確定,需要詳細分析其內部類型之一——參考型別(Reference type)。

參考型別(Reference type)
使用虛擬碼我們可以將參考型別的值可以表示為擁有兩個屬性的對象——base(即擁有屬性的那個對象),和base中的propertyName 。
複製代碼 代碼如下:

var valueOfReferenceType = {
base: <base object>,
propertyName: <property name>
};

參考型別的值只有兩種情況:

當我們處理一個標示符時
或一個屬性訪問器
標示符的處理過程在下一篇文章裡詳細討論,在這裡我們只需要知道,在該演算法的傳回值中,總是一個參考型別的值(這對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"

屬性訪問器都應該熟悉。它有兩種變體:點(.)文法(此時屬性名稱是正確的標示符,且事Crowdsourced Security Testing道),或括弧文法([])。
複製代碼 代碼如下:

foo.bar();
foo['bar']();

在中間計算的傳回值中,我們有了參考型別的值。
複製代碼 代碼如下:

var fooBarReference = {
base: foo,
propertyName: 'bar'
};

GetValue(fooBarReference); // function object "bar"

參考型別的值與函數上下文中的this值如何相關?——從最重要的意義上來說。 這個關聯的過程是這篇文章的核心。 一個函數上下文中確定this值的通用規則如下:

在一個函數上下文中,this由調用者提供,由調用函數的方式來決定。如果調用括弧()的左邊是參考型別的值,this將設為參考型別值的base對象(base object),在其他情況下(與參考型別不同的任何其它屬性),這個值為null。不過,實際不存在this的值為null的情況,因為當this的值為null的時候,其值會被隱式轉換為全域對象。註:第5版的ECMAScript中,已經不強迫轉換成全域變數了,而是賦值為undefined。

我們看看這個例子中的表現:
複製代碼 代碼如下:

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。
複製代碼 代碼如下:

var fooBarReference = {
base: foo,
propertyName: 'bar'
};

但是,用另外一種形式啟用相同的函數,我們得到其它的this值。
複製代碼 代碼如下:

var test = foo.bar;
test(); // global

因為test作為標示符,產生了參考型別的其他值,其base(全域對象)用作this 值。
複製代碼 代碼如下:

var testReference = {
base: global,
propertyName: 'test'
};

現在,我們可以很明確的告訴你,為什麼用運算式的不同形式啟用同一個函數會不同的this值,答案在於參考型別(type Reference)不同的中間值。
複製代碼 代碼如下:

function foo() {
alert(this);
}

foo(); // global, because

var fooReference = {
base: global,
propertyName: 'foo'
};

alert(foo === foo.prototype.constructor); // true

// 另外一種形式的調用運算式

foo.prototype.constructor(); // foo.prototype, because

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

函數調用和非參考型別
因此,正如我們已經指出,當調用括弧的左邊不是參考型別而是其它類型,這個值自動化佈建為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對象,而是global對象?

問題在於後面的三個調用,在應用一定的運算操作之後,在調用括弧的左邊的值不在是參考型別。

第一個例子很明顯———明顯的參考型別,結果是,this為base對象,即foo。
在第二個例子中,組運算子並不適用,想想上面提到的,從參考型別中獲得一個對象真正的值的方法,如GetValue。相應的,在組運算的返回中———我們得到仍是一個參考型別。這就是this值為什麼再次設為base對象,即foo。
第三個例子中,與組運算子不同,賦值運算子調用了GetValue方法。返回的結果是函數對象(但不是參考型別),這意味著this設為null,結果是global對象。
第四個和第五個也是一樣——逗號運算子和邏輯運算子(OR)調用了GetValue 方法,相應地,我們失去了引用而得到了函數。並再次設為global。
參考型別和this為null
有一種情況是這樣的:當調用運算式限定了call括弧左邊的參考型別的值, 儘管this被設定為null,但結果被飲食轉化成global。當參考型別值的base對象是被使用中的物件時,這種情況就會出現。

下面的執行個體中,內建函式被父函數調用,此時我們就能夠看到上面說的那種特殊情況。正如我們在第12章知道的一樣,局部變數、內建函式、形式參數儲存在給定函數的啟用物件中。
複製代碼 代碼如下:

function foo() {
function bar() {
alert(this); // global
}
bar(); // the same as AO.bar()
}

使用中的物件總是作為this返回,值為null——(即虛擬碼的AO.bar()相當於null.bar())。這裡我們再次回到上面描述的例子,this設定為全域對象。

有一種情況除外:如果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(); // ES3標準裡是__catchObject, ES5標準裡是global
}

// on idea

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

// ES5新標準裡已經fix了這個bug,
// 所以this就是全域對象了
var eReference = {
base: global,
propertyName: 'e'
};

同樣的情況出現在命名函數(函數的更對細節參考第15章Functions)的遞迴調用中。在函數的第一次調用中,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); // "a"對象下建立一個新屬性
this.x = 10;
}

var a = new A();
alert(a.x); // 10

在這個例子中,new運算子調用“A”函數的內部的[[Construct]] 方法,接著,在對象建立後,調用內部的[[Call]] 方法。 所有相同的函數“A”都將this的值設定為新建立的對象。

函數調用中手動設定this
在函數原型中定義的兩個方法(因此所有的函數都可以訪問它)允許去手動設定函數調用的this值。它們是.apply和.call方法。他們用接受的第一個參數作為this值,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,它們的確是特色)。我希望這篇文章有助於你準確的理解ECMAScript中this關鍵字如何工作。同樣,我很樂意在評論中回到你的問題。

其它參考
  • 10.1.7 – This
  • 11.1.1 – The this keyword
  • 11.2.2 – The new operator
  • 11.2.3 – Function calls

聯繫我們

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