在很長的一段時間之內,我一直以為作用域就是上下文,這也就對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。