本文將進一步討論與執行上下文密切相關的概念--this關鍵字。程序員
事實證實,this這塊的內容很是的複雜,它在不一樣執行上下文的狀況下其值都會不一樣,而且會相應的引起一些問題。es6
不少程序員一看到this關鍵字,就會把它和麪向對象的編程方式聯繫在一塊兒,它指向利用構造器新建立出來的對象。在ECMAScript中,也支持this,然而, 正如你們所熟知的,this不只僅只用來表示建立出來的對象。算法
接下來給你們揭開在ECMAScript中this神祕的面紗。編程
activeExecutionContext = {
VO: {...},
this: thisValue
};
複製代碼
這裏的VO就是前一章介紹的變量對象。數組
This與上下文的可執行代碼類型有關,其值在進入上下文階段就肯定了,而且在執行代碼階段是不能改變的下面就來詳細對其介紹。 bash
// 顯式定義全局對象的屬性
this.a = 10; // global.a = 10
alert(a); // 10
// 經過賦值給不受限的標識符來進行隱式定義
b = 20;
alert(this.b); // 20
// 經過變量聲明來進行隱式定義
// 由於全局上下文中的變量對象就是全局對象自己
var c = 30;
alert(this.c); // 30
複製代碼
當this在函數代碼中的時候,事情就變得有趣多了。這種狀況下是最複雜的,而且會引起不少的問題。app
函數代碼中this值的第一個特性(同時也是最主要的特性)就是:它並不是靜態的綁定在函數上。ecmascript
正如此前提到的,this的值是在進入上下文的階段肯定的,而且在函數代碼中的話,其值每次都會大不相同。ide
然而,一旦進入執行代碼階段,其值就不能改變了。比方說,要想給this賦一個新的值是不可能的,由於this根本就不是變量(相反的,在Python語言中,它顯示定義的self對象是能夠在運行時隨意更改的):函數
var foo = {x: 10};
var bar = {
x: 20,
test: function () {
alert(this === bar); // true
alert(this.x); // 20
this = foo; // error, 不能更改this的值
alert(this.x); // 若是沒有錯誤,則其值爲10而不是20
}
};
// 在進入上下文的時候,this的值就肯定了是「bar」對象
// 至於爲何,會在後面做詳細介紹
bar.test(); // true, 20
foo.test = bar.test;
// 可是,這個時候,this的值又會變成「foo」
// 縱然咱們調用的是同一個函數
foo.test(); // false, 10
複製代碼
所以,在函數代碼中影響this值的因素是有不少的。
首先,在通常的函數調用中,this的值是由激活上下文代碼的調用者決定的,好比說,調用函數的外層上下文。this的值是由調用表達式的形式決定的。
理解並謹記這一點是很是必要的,有利於在任何上下文中都能準確的肯定this的值。(俗話說就是誰調用指向誰,es6箭頭函數除外)
影響調用上下文中的this的值的只有多是調用表達式的形式,也就是調用函數的方式。 (一些關於JavaScript的文章和書籍中指出的「this的值取決於函數的定義方式,若是是全局函數,則this的值就會設置爲全局對象,若是是某個對象的方法,則this的值就會設置爲該對象」——這純屬扯淡,根本就是在誤人子弟)。 正如此前你們看到的,縱然是全局函數,this的值也會隨着函數調用方式的不一樣而不一樣:
function foo() {
alert(this);
}
foo(); // global
alert(foo === foo.prototype.constructor); // true
// 然而,一樣的函數,以另一種調用方式的話,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
// 一樣地,相同的函數以不一樣的調用方式,this的值也就不一樣了
exampleFunc(); // global, false
複製代碼
那麼,究竟調用表達式的方式是如何影響this的值的呢?爲了徹底搞明白這其中的奧妙,首先,這裏有必要先介紹一種內部類型——引用類型(the Reference type)。
var valueOfReferenceType = {
base: ,
propertyName:
};
複製代碼
引用類型的值只有多是如下兩種狀況
關於標識符的處理會在第四章——所用域鏈中做介紹,這裏咱們只要注意的是,此算法總返回一個引用類型的值(這對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"
複製代碼
對於屬性訪問來講,有兩種方式: 點符號(這時屬性名是正確的標識符而且提早已經知道了)或者中括號符號:
foo.bar();
foo['bar']();
複製代碼
中間過程當中,獲得以下的引用類型的值:
var fooBarReference = {
base: foo,
propertyName: 'bar'
};
GetValue(fooBarReference); // function object "bar"
複製代碼
問題又來了,引用類型的值又是如何影響函數上下文中this的值的呢?——很是重要。這也是本文的重點。總的來講,決定函數上下文中this的值的規則以下所示:
函數上下文中this的值是函數調用者提供而且由當前調用表達式的形式而定的。 若是在調用括號()的左邊,有引用類型的值,那麼this的值就會設置爲該引用類型值的base對象。 全部其餘狀況下(非引用類型),this的值老是null。然而,因爲null對於this來講沒有任何意義,所以會隱式轉換爲全局對象。
以下所示:
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的值就設置爲foo對象了:
var fooBarReference = {
base: foo,
propertyName: 'bar'
};
複製代碼
然而,一樣的函數以不一樣的激活方式的話,this的值就徹底不一樣了:
var test = foo.bar;
test(); // global
複製代碼
由於test也是標識符,這樣就產生了另外的引用類型的值,其中base對象(全局對象)
就是this的值:
var testReference = {
base: global,
propertyName: 'test'
};
複製代碼
至此,咱們就能夠精確的解釋,爲何一樣的函數,以不一樣的調用方式激活,this的值也會不一樣了——答案就是處理過程當中,是不一樣的引用類型的值:
function foo() {
alert(this);
}
foo(); // global, 由於
var fooReference = {
base: global,
propertyName: 'foo'
};
alert(foo === foo.prototype.constructor); // true
// 另外一種調用方式
foo.prototype.constructor(); // foo.prototype, 由於
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
複製代碼
咱們來考慮下以下表達式:
(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對象而是全局對象呢?
這裏主要疑問在最後三個表達式,這三個表達式添加了特定的操做以後,調用括號左側就再也不是引用類型的值了。
第一種狀況——很是明確,是引用類型,最終this的值設置爲base對象,foo。
第二種狀況有一個組操做符(grouping operator),該操做符不會觸發調用獲取引用類型實際值的方法,好比:GetValue方法。 相應的,處理組操做符中間過程當中——得到的仍然是一個引用類型的值,這也就解釋了爲何this的值設置成了base對象,foo。
第三種狀況是一個賦值操做符(assignment operator),與組操做符不一樣的是,它會觸發調用GetValue方法。 最後返回的時候就是一個函數對象了(而不是引用類型的值了),這就意味着this的值會設置爲null,最終會變成全局對象。
第四和第五種狀況也是相似的——逗號操做符和OR邏輯表達式都會觸發調用GetValue方法,因而相應地就會丟失原先的引用類型值,變成了函數類型,this的值就變成了全局對象了。
當內部子函數在父函數中被調用的時候就會發生這種狀況。正如變量對象介紹的, 局部變量,內部函數以及函數的形參都會存儲在指定函數的活躍對象中:
function foo() {
function bar() {
alert(this); // global
}
bar(); // 和AO.bar()是同樣的
}
複製代碼
活躍對象老是會返回this值爲——null(用僞代碼來表示,AO.bar()就至關於null.bar())。而後,如此前描述的,this的值最終會由null變爲全局對象。
當函數調用包含在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(); // __catchObject - in ES3, global - fixed in ES5
}
// on idea
var eReference = {
base: __catchObject,
propertyName: 'e'
};
// 然而,既然這是個bug
// 那就應該強制設置爲全局對象
// null => global
var eReference = {
base: global,
propertyName: 'e'
};
複製代碼
一樣的狀況還會在遞歸調用一個非匿名函數的時候發生。在第一次函數調用的時候,base對象是外層的活躍對象(或者全局對象), 在接下來的遞歸調用的時候——base對象應當是一個存儲了可選的函數表達式名字的特殊對象,然而,事實倒是,在這種狀況下,this的值永遠都是全局對象:
(function foo(bar) {
alert(this);
!bar && foo(1); // "should" be special object, but always (correct) global
})(); // global
複製代碼
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的值會設置爲新建立的對象。
以下所示:
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
複製代碼
重學JavaScript深刻理解系列(一)
重學JavaScript深刻理解系列(二)
重學JavaScript深刻理解系列(四)
重學JavaScript深刻理解系列(五)
重學JavaScript深刻理解系列(六)