在很長的一段時間之內,我一直以為作用域就是上下文,這也就對JavaScript中的this理解增加了很多麻煩,所以這篇文章開篇第一個要陳訴的概念就是作用域和上下文不是一個概念。作用域(scope) 是指變量的可訪問性,上下文是來決定this。(注意執行期上下文指的是作用域,這是JavaScipt規范,所以得遵守)
在JavaScript中只有兩種作用域,一種是全局作用域,另一個就是函數作用域。上下文則會與this息息相關,而this是在運行的時候進行綁定的,它的上下文取決于函數在哪里被調用,this的綁定和函數聲明的位置沒有任何關系。
當一個函數被調用時,會創建一個活動記錄(即執行上下文)。這個活動會包含函數在哪里被調用,函數的調用方法,傳入的參數信息等信息。this就是記錄的其中一個屬性,會在函數的執行過程中用到。
以上引用出自《你不知道的JavaScript(上卷)》,在這里強烈推薦這本書,字字珠璣。
再次強調:this實際上是在函數被調用的時候發生綁定,它指向什么完全取決于函數在哪里調用。
調用位置
接下來我們看看函數調用包括哪幾種情況,只有正確的知道函數調用的位置,才能正確的明白this的指向問題。
默認綁定(全局調用)
var a = 2;
function foo() {
console.log(this.a)
}
foo();
以上就是默認綁定,foo函數是直接調用的。
隱式綁定
b = 2;
var obj = {
b: 3,
foo: foo
}
function foo () {
console.log(this.b);
}
obj.foo(); // 3
這里為什么叫做隱式綁定,因為這個foo函數無論是在obj里面聲明還是在obj外面聲明,他實際上都是不屬于obj這個對象的(obj只是記錄了foo這個屬性的引用值),但是最后在執行的時候this卻被綁定到了obj這個對象上下文中。當然如果有多個對象鏈式調用,this只會綁定到最后一層。obj2.obj1.foo()
,this是綁定到obj1這個對象上下文中。
當然這里有一個注意點
var obj = {
b: 3,
foo: foo
}
function foo () {
console.log(this.b);
}
var bar = obj.foo;
bar(); // 2
這里實際上bar直接是foo的引用,就相當于var bar = obj.foo = foo
,我們打印一下可以發現
console.log(bar === foo && foo === obj.foo && bar === obj.foo) // true
所以此時就和第一種默認綁定一樣,bar函數是直接在全局上下文中被調用的,所以this會指向全局。
還有一種就是嵌套函數了
b = 2;
var obj = {
b: 3,
foo: foo
}
function foo () {
console.log('foo', this.b);// 3
foo2();
}
function foo2() {
console.log('foo2', this.b); // 2
}
obj.foo();
實際上foo2也是直接被(window)調用了。
顯示綁定call,apply,bind
通過call,apply,bind函數可以強制某個函數在哪個對象(或者上下文)中被調用
b = 2;
var obj = {
b: 3,
foo: foo
}
function foo () {
console.log('foo', this.b);
}
foo.call(obj); // 3
當然如果你傳入的是一個基本類型的值,那么JavaScript會把它轉換成它的對象形式。
new綁定
說到new操作符,就不得不說它的內部工作原理了,我們在執行new操作的時候究竟執行了什么。
1 創建一個全新的對象 var obj = {}
2 這個新對象的原型會被執行[[原型]]連接 obj[[prototype]] = Fun.prototye
3 這個新對象會綁定到函數調用的this Fun.bind(obj)
4 如果函數沒有返回其他對象,那么會返回這個新創建的對象 return obj;
所以new綁定實質還是顯式綁定。
總結一下我們可以按照下面的順序進行判斷
1 函數是否在new中調用(new 綁定),如果是this綁定的就是返回的新對象
2 函數是否通過call、apply(顯式綁定)如果是this綁定的是那個指定的對象
3 函數是否在某個上下文對象中調用(隱式綁定),如果是,this綁定的是那個上下文無關文法對象
4 如果都不是那么就是默認綁定,this綁定的就是全局對象或者undefined(嚴格模式)
例外
凡事總有例外,如果你把null、undefined作為this的綁定對象傳入call、apply或者bind那么實際上,這些值在執行的時候會被忽略,實際使用的是默認綁定。那么什么情況下我們會去綁定一個null或者undefined的呢?一種就是用apply來展開一個數組,當然這種方法的確很實用(不過在ES6中出現了...操作符來展開數組)。
function foo(a, b) {return a + b}
foo.apply(null, [2, 3]);
箭頭函數,箭頭函數中的this是根據外層作用域來決定this的,也就是說箭頭函數中的this就和箭頭函數在哪里聲明有關系了。
a = 2;
var obj = {
a: 3,
foo: foo
}
function foo () {
return () => {
console.log(this.a);
};
}
var fun = foo.call(obj);
fun(); // 3 此時箭頭函數的外層作用域為foo,foo函數被調用時,this被綁定在了obj對象上
a = 2;
var obj = {
a: 3,
foo: foo
}
var arrowFun = () => {
console.log(this.a);
}
function foo () {
return arrowFun;
}
var fun = foo.call(obj);
fun(); //2 箭頭函數的外層作用域為全局作用域,全局作用域中的this指向全局上下文
最后歡迎大家關注我的個人博客,將會有更多的精彩文檔,喜歡的話也可以給個star。