JavaScript之例題中徹底理解this
前面的文章講解了 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();// ? 複製程式碼
輸出:
innerwindow
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)()// ? 複製程式碼
輸出:
person1person2 windowwindow windowperson2window person1person1person2
上面 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)()// 複製程式碼
輸出:
personApersonB personApersonA windowpersonBwindow personApersonApersonB
例 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 });// ? 複製程式碼
輸出:
111undefined undefinedundefined
例 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 中的執行上下文,作用域、作用域鏈,閉包,變數物件,函式執行過程的基礎上。
有興趣深入瞭解上下文,作用域,閉包相關內容的同學可以翻看我之前的文章。