JavaScript之例題中徹底理解this

南波發表於2019-03-04

本文共 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
person2

window
window

window
person2
window

person1
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
personB

personA
personA

window
personB
window

personA
personA
personB

例 4 和 例 3 大致一樣,唯一的區別在於兩點:

  1. 建構函式中 this 指向被建立的例項
  2. 建構函式,也是函式,所以存在作用域,所以裡面的箭頭函式,它們的 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
undefined

undefined
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,總是指向定義時所在的物件,而不是執行時所在的物件。

  1. foo.call({}) 在執行時,內部的第一層箭頭函式才被定義
  2. 箭頭函式無法繫結 this, 所以 call 函式指定 this 無效
  3. 箭頭函式的 this 來自於上一層作用域(非箭頭函式作用域)的 this

總結

有本書中有提到,當理解 JavaScript 中的 this 之後,JavaScript 才算入門,我深以為然。

原因是,要徹底理解 this, 應該是建立在已經大致理解了 JS 中的執行上下文,作用域、作用域鏈,閉包,變數物件,函式執行過程的基礎上。

有興趣深入瞭解上下文,作用域,閉包相關內容的同學可以翻看我之前的文章。

參考連結:

1:this、apply、call、bind
2: 從這兩套題,重新認識JS的this、作用域、閉包、物件
3: 關於箭頭函式this的理解幾乎完全是錯誤的
4: 深入JS系列

歡迎關注我的個人公眾號“謝南波”,專注分享原創文章。

JavaScript之例題中徹底理解this

掘金專欄 JavaScript 系列文章

  1. JavaScript之變數及作用域
  2. JavaScript之宣告提升
  3. JavaScript之執行上下文
  4. JavaScript之變數物件
  5. JavaScript原型與原型鏈
  6. JavaScript之作用域鏈
  7. JavaScript之閉包
  8. JavaScript之this
  9. JavaScript之arguments
  10. JavaScript之按值傳遞
  11. JavaScript之例題中徹底理解this
  12. JavaScript專題之模擬實現call和apply

相關文章