JavaScript ECMA-262-3 深入解析.第三章.this

來源:互聯網
上載者:User

介紹
在這篇文章裡,我們將討論跟執行內容直接相關的更多細節。討論的主題就是this關鍵字。
實踐證明,這個主題很難,在不同執行內容中確定this的值經常會發生問題。
許多程式員習慣的認為,在程式語言中,this關鍵字與物件導向程式開發緊密相關,其完全指向由構造器新建立的對象。在ECMAScript規範中也是這樣實現的,但正如我們將看到那樣,在ECMAScript中,this並不限於只用來指向新建立的對象。
下面讓我們更詳細的瞭解一下,在ECMAScript中this的值到底是什嗎?
定義
this是執行內容中的一個屬性: 複製代碼 代碼如下:activeExecutionContext = {
VO: {...},
this: thisValue
};

這裡VO是我們前一章討論的變數對象。
this與上下文中可執行代碼(的類型)直接相關。this的值在進入上下文時確定,並且在上下文運行代碼期間不會改變this的值。
下面讓我們更詳細研究這些情境。
this在全域代碼中的值
在這裡一切都很簡單。在全域代碼中,this始終是全域對象本身,這樣就有可能間接的引用到它了。 複製代碼 代碼如下:// explicit property definition of
// the global object
this.a = 10; // global.a = 10
alert(a); // 10
// implicit definition via assigning
// to unqualified identifier
b = 20;
alert(this.b); // 20
// also implicit via variable declaration
// because variable object of the global context
// is the global object itself
var c = 30;
alert(this.c); // 30

this在函數代碼中的值
在函數代碼中使用this時很有趣,這種應用情境很難且會導致很多問題。
在這種類型的代碼中,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; // error
alert(this.x); // if there wasn't an error then 20, not 10
}
};
// on entering the context this value is
// determined as "bar" object; why so - will
// be discussed below in detail
bar.test(); // true, 20
foo.test = bar.test;
// however here this value will now refer
// to "foo" – even though we're calling the same 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
// but with another form of the call expression
// of the same function, this value is different
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
// again with another form of the call expression
// of the same function, we have different this value
exampleFunc(); // global, false

那麼,到底調用函數的方式如何影響this的值?為了充分理解this的值是如何確定的,我們需要詳細分析一個內部類型(internal type)——參考型別(Reference type)。
參考型別
用虛擬碼可以把參考型別表示為擁有兩個屬性的對象——base(即擁有屬性的那個對象),和base中的propertyName 。 複製代碼 代碼如下:var valueOfReferenceType = {
base: <base object>,
propertyName: <property name>
};

參考型別的值僅存在於兩種情況中:
1. 當我們處理一個標示符時;(when we deal with an identifier;)
2. 或一個屬性訪問器;(or with a property accessor.)
標示符的處理過程在 Chapter 4. Scope chain中討論;在這裡我們只需要知道,使用這種處理方式的傳回值總是一個參考型別的值(這對this來說很重要)。
標識符是變數名,函數名,函數參數名和全域對象中未識別的屬性名稱。例如,下面標識符的值:
var foo = 10;
function bar() {}
在操作的中間結果中,參考型別對應的值如下: 複製代碼 代碼如下:var fooReference = {
base: global,
propertyName: 'foo'
};
var barReference = {
base: global,
propertyName: 'bar'
};

為了從參考型別中得到一個對象真正的值,在虛擬碼中可以用GetValue方法(譯者註:11.1.6)來表示,如下: 複製代碼 代碼如下: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的值是如何關聯起來的呢?這個關聯的過程是這篇文章的核心。(The given moment is the main of this article.) 在一個函數上下文中確定this的值的通用規則如下:
在一個函數上下文中,this的值由調用者提供,且由調用函數的方式決定。如果調用括弧()的左邊是參考型別的值,this將設為這個參考型別值的base對象,在其他情況下(與參考型別不同的任何其它屬性),this的值都為null。不過,實際不存在this的值為null的情況,因為當this的值為null的時候,其值會被隱式轉換為全域對象。
下面讓我們看個例子: 複製代碼 代碼如下: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啟用時將base設定給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
// another form of the call expression
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

函數調用和非參考型別
那麼,正如我們已經指出,當調用括弧的左邊不是參考型別而是其它類型,this的值自動化佈建為null,實際最終this的值被隱式轉換為全域對象。
讓我們思考下面這種函數運算式: 複製代碼 代碼如下:(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。
在第二個例子中,分組操作符(譯者註:這裡的分組操作符就是指foo.bar外面的括弧"()")沒有實際意義,想想上面提到的,從參考型別中獲得一個對象真正的值的方法,如GetValue (參考11.1.6)。相應的,在分組操作的傳回值中———我們得到的仍是一個參考型別。這就是this的值為什麼再次被設為base對象,即 foo。
第三個例子中,與分組操作符不同,賦值操作符調用了GetValue方法(參考11.13.1的第三步)。返回的結果已經是函數對象(不是參考型別),這意味著this的值被設為null,實際最終結果是被設定為global對象。
第四個和第五個也是一樣——逗號操作符和邏輯操作符(OR)調用了GetValue 方法,相應地,我們失去了參考型別的值而得到了函數類型的值,所以this的值再次被設為global對象。
參考型別和this為null
有一種情況,如果調用方式確定了參考型別的值(when call expression determinates on the left hand side of call brackets the value of Reference type。譯者注,原文有點拖遝!),不管怎樣,只要this的值被設定為null,其最終就會被隱式轉換成global。當參考型別值的base對象是啟用物件時,就會導致這種情況。
下面的執行個體中,內建函式被父函數調用,此時我們就能夠看到上面說的那種特殊情況。正如我們在 第二章 學到的一樣,局部變數、內建函式、形式參數都儲存在給定函數的啟用物件中。 複製代碼 代碼如下:function foo() {
function bar() {
alert(this); // global
}
bar(); // the same as AO.bar()
}

啟用物件總是作為this的值返回——null(即虛擬碼AO.bar()相當於null.bar())。(譯者註:不明白參考這裡)這裡我們再次回到上面描述的情況,this的值最終還是被設定為全域對象。
有一種情況除外:“在with語句中調用函數,且在with對象(譯者註:即下面例子中的__withObject)中包含函數名屬性時”。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'
};
// but, as this is a bug
// then this value is forced to global
// null => global
var eReference = {
base: global,
propertyName: 'e'
};

同樣的情況出現在命名函數(函數的更多細節參考Chapter 5. Functions)的遞迴調用中。在函數的第一次調用中,base對象是父啟用物件(或全域對象),在遞迴調用中,base對象應該是儲存著函數運算式可選名稱的特定對象。但是,在這種情況下,this的值也總是被設定為global。 複製代碼 代碼如下:(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的值,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關鍵字的特徵(and they really are features, in contrast, say, with C++ or Java,譯者註:這句話沒什麼大用,還不知道咋翻好,暫不翻了)。我希望這篇文章有助於你準確的理解ECMAScript中this關鍵字如何工作。同樣,我很高興在評論中回答您的問題。
其他參考
10.1.7 – This;
11.1.1 – The this keyword;
11.2.2 – The new operator;
11.2.3 – Function calls.
英文地址 : ECMA-262-3 in detail. Chapter 3. This.
中文地址 : [JavaScript]ECMA-262-3 深入解析.第三章.this
翻譯聲明:
1.因為Denis已經翻譯過這篇文章,所以該篇譯文在部分章節參考了他的譯文,參考引用部分大概占整篇文章的30%左右,另外70%左右完全是重新翻譯的。
2.在翻譯過程中,跟原作者進行了充分的溝通,大家看譯文的時候,可以多參考原文的留言列表。
3.再好的翻譯也趕不上原汁原味的原文,所以推薦大家看過譯文之後還是要再仔細看看原文。

相關文章

聯繫我們

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