本文共 2025 字,看完只需 8 分鐘
概述
前面的文章講解了 JavaScript 中的執行上下文,作用域,變數物件,this 的相關原理,但是我後來在網上看到一些例題的時候,依然沒能全做對,說明自己有些細節還沒能掌握,本文就結合例題進行深入實踐,討論函式在不同的呼叫方式 this 的指向問題。
老規矩,先給結論 1 和 結論2:
this 始終指向最後呼叫它的物件
“箭頭函式”的this,總是指向定義時所在的物件,而不是執行時所在的物件。
特別提示:
本文的例子,最好自己在瀏覽器控制檯中去試一遍,看完過兩天就會忘的,一定要實踐。
一、隱式繫結
// 例 1
var name = "window";
function foo() {
var name = "inner";
console.log(this.name);
}
foo(); // ?
複製程式碼
輸出:
window
例 1 中,非嚴格模式,由於 foo 函式是在全域性環境中被呼叫,this 會被預設指向全域性物件 window;
所以符合了我們的結論一:
this 始終指向最後呼叫它的物件
二、一般函式和箭頭函式的物件呼叫
// 例 2
var name = "window";
var person = {
name: "inner",
show1: function () {
console.log(this.name);
},
show2: () => {
console.log(this.name);
}
}
person.show1(); // ?
person.show2(); // ?
複製程式碼
輸出:
inner
window
person.show1() 輸出 inner 沒毛病,person.show2() 箭頭函式為什麼會輸出 window 呢。MDN 中對 this 的定義是:
箭頭函式不繫結 this, 箭頭函式不會建立自己的this,它只會從自己的作用域鏈的上一層繼承this。
再看本文前面給的結論:
“箭頭函式”的this,總是指向定義時所在的物件,而不是執行時所在的物件。
由於 JS 中只有全域性作用域和函式作用域,箭頭函式在定義時的上一層作用域是全域性環境,全域性環境中的 this 指向全域性物件本身,即 window。
三、call
// 例 3
var name = `window`
var person1 = {
name: `person1`,
show1: function () {
console.log(this.name)
},
show2: () => console.log(this.name),
show3: function () {
return function () {
console.log(this.name)
}
},
show4: function () {
return () => console.log(this.name)
}
}
var person2 = { name: `person2` }
person1.show1() // ?
person1.show1.call(person2) // ?
person1.show2() // ?
person1.show2.call(person2) // ?
person1.show3()() // ?
person1.show3().call(person2) // ?
person1.show3.call(person2)() // ?
person1.show4()() // ?
person1.show4().call(person2) // ?
person1.show4.call(person2)() // ?
複製程式碼
輸出:
person1
person2window
windowwindow
person2
windowperson1
person1
person2
上面 10 行列印,你對了幾個呢?
首先:
person1.show1()
和 person1.show1.call(person2)
輸出結果應該沒問題,call
的作用就是改變了呼叫的物件 為 person2
。
其次:
person1.show2()
,person1.show2.call(person2)
,由於呼叫的是箭頭函式,和本文例 2 中是一樣的,箭頭函式定義時 this 指向的是上一層,也就是全域性物件, 並且 箭頭函式不繫結自己的 this, 所以通過 call()
或 apply()
方法呼叫箭頭函式時,只能傳遞引數,不能傳遞新的物件進行繫結。故列印的值都是 window。
進而:
function foo () {
return function () {
console.log(this.name)
}
}
foo()();
複製程式碼
部落格前面的文章有講過閉包,上面這段程式碼也是典型的閉包運用,可以看作:
function foo () {
return function () {
console.log(this.name)
}
}
var bar = foo();
bar();
複製程式碼
所以,很明顯,被返回的內部函式其實是在全域性環境下被呼叫的。回到前面看我們的結論 1,this 始終指向最後呼叫函式的物件
,這句話的關鍵詞應該是什麼?我覺得應該是 呼叫
,什麼時候呼叫,誰呼叫。
再回過頭來看:
person1.show3()()
輸出 window,因為內部函式在全域性環境中被呼叫。
person1.show3().call(person2)
輸出 person2, 因為內部函式被 person2 物件呼叫了。
person1.show3.call(person2)()
輸出 window,也是因為內部函式在全域性環境中被呼叫。
最後:
重點理解結論 2:
“箭頭函式”的this,總是指向定義時所在的物件,而不是執行時所在的物件。
show4: function () {
return () => console.log(this.name)
}
複製程式碼
這段程式碼中,箭頭函式是在 外層函式 show4 執行後才被定義的。為什麼?可以翻看我前面關於作用域鏈,執行上下文,變數物件的文章,函式在進入執行階段時,會先查詢內部的變數和函式宣告,將他們作為變數物件的屬性,關聯作用域鏈,並繫結 this 指向。
所以:
person1.show4()()
輸出 person1,因為外部函式在執行時的 this 為 person1, 此時定義了內部函式,而內部函式為外部函式的 this。
person1.show4().call(person2)
輸出 person1,箭頭函式不會繫結 this, 所以 call 傳入 this 指向無效。
person1.show4.call(person2)()
輸出 person2,因為外部函式在執行時的 this 為 person2,此時定義了內部函式,而內部函式為外部函式的 this。
四、建構函式中的 this
// 例 4
var name = `window`
function Person (name) {
this.name = name;
this.show1 = function () {
console.log(this.name)
}
this.show2 = () => console.log(this.name)
this.show3 = function () {
return function () {
console.log(this.name)
}
}
this.show4 = function () {
return () => console.log(this.name)
}
}
var personA = new Person(`personA`)
var personB = new Person(`personB`)
personA.show1() //
personA.show1.call(personB) //
personA.show2() //
personA.show2.call(personB) //
personA.show3()() //
personA.show3().call(personB) //
personA.show3.call(personB)() //
personA.show4()() //
personA.show4().call(personB) //
personA.show4.call(personB)() //
複製程式碼
輸出:
personA
personBpersonA
personAwindow
personB
windowpersonA
personA
personB
例 4 和 例 3 大致一樣,唯一的區別在於兩點:
- 建構函式中 this 指向被建立的例項
- 建構函式,也是函式,所以存在作用域,所以裡面的箭頭函式,它們的 this 指向,來自於上一層,就不再是全域性環境 window, 而是建構函式 的 this。
五、setTimeout 函式
// 例 5
function foo(){
setTimeout(() =>{
console.log("id:", this.id)
setTimeout(() =>{
console.log("id:", this.id)
}, 100);
}, 100);
}
foo.call({id: 111}); //
複製程式碼
輸出:
111
111
注意一點:
setTimeout
函式是在全域性環境被 window 物件執行的,但是 foo 函式在執行時,setTimtout
委託的匿名箭頭函式被定義,箭頭函式的 this 來自於上層函式 foo 的呼叫物件, 所以列印結果才為 111;
六、setTimeout 函式 2
// 例 6
function foo1(){
setTimeout(() =>{
console.log("id:", this.id)
setTimeout(function (){
console.log("id:", this.id)
}, 100);
}, 100);
}
function foo2(){
setTimeout(function() {
console.log("id:", this.id)
setTimeout(() => {
console.log("id:", this.id)
}, 100);
}, 100);
}
foo1.call({ id: 111 }); // ?
foo2.call({ id: 222 }); // ?
複製程式碼
輸出:
111
undefinedundefined
undefined
例 5 中已經提到,setTimeout
函式被 window 物件呼叫,如果
是普通函式,內部的 this 自然指向了全域性物件下的 id, 所以為 undefined
,如果是箭頭函式,this 指向的就是外部函式的 this。
七、巢狀箭頭函式
// 例 7
function foo() {
return () => {
return () => {
return () => {
console.log("id:", this.id);
};
};
};
}
var f = foo.call({id: 1});
var t1 = f.call({id: 2})()(); //
var t2 = f().call({id: 3})(); //
var t3 = f()().call({id: 4}); //
複製程式碼
輸出:
1
1
1
這段程式碼是為了鞏固我們的結論2:
“箭頭函式”的this,總是指向定義時所在的物件,而不是執行時所在的物件。
- foo.call({}) 在執行時,內部的第一層箭頭函式才被定義
- 箭頭函式無法繫結 this, 所以 call 函式指定 this 無效
- 箭頭函式的 this 來自於上一層作用域(非箭頭函式作用域)的 this
總結
有本書中有提到,當理解 JavaScript 中的 this 之後,JavaScript 才算入門,我深以為然。
原因是,要徹底理解 this, 應該是建立在已經大致理解了 JS 中的執行上下文,作用域、作用域鏈,閉包,變數物件,函式執行過程的基礎上。
有興趣深入瞭解上下文,作用域,閉包相關內容的同學可以翻看我之前的文章。
參考連結:
1:this、apply、call、bind
2: 從這兩套題,重新認識JS的this、作用域、閉包、物件
3: 關於箭頭函式this的理解幾乎完全是錯誤的
4: 深入JS系列
歡迎關注我的個人公眾號“謝南波”,專注分享原創文章。