JavaScript之例題中完全理解this

本文共 2025 字,看完只需 8 分鐘

概述

前面的文章講解了 JavaScript 中的執行上下文,做用域,變量對象,this 的相關原理,可是我後來在網上看到一些例題的時候,依然沒能全作對,說明本身有些細節還沒能掌握,本文就結合例題進行深刻實踐,討論函數在不一樣的調用方式 this 的指向問題。javascript

老規矩,先給結論 1 和 結論2:java

this 始終指向最後調用它的對象

「箭頭函數」的this,老是指向定義時所在的對象,而不是運行時所在的對象。git

特別提示:
本文的例子,最好本身在瀏覽器控制檯中去試一遍,看完過兩天就會忘的,必定要實踐。es6

1、隱式綁定

// 例 1
var name = "window";

function foo() {
    var name = "inner";

    console.log(this.name);
}

foo();  // ?

輸出:github

window

例 1 中,非嚴格模式,因爲 foo 函數是在全局環境中被調用,this 會被默認指向全局對象 window;瀏覽器

因此符合了咱們的結論一:閉包

this 始終指向最後調用它的對象

2、通常函數和箭頭函數的對象調用

// 例 2
var name = "window";

var person = {
    name: "inner",
    show1: function () {
        console.log(this.name);
    },
    show2: () => {
        console.log(this.name);
    }
}

person.show1();  // ?
person.show2();  // ?

輸出:app

inner
window

person.show1() 輸出 inner 沒毛病,person.show2() 箭頭函數爲何會輸出 window 呢。MDN 中對 this 的定義是:函數

箭頭函數不綁定 this, 箭頭函數不會建立本身的this,它只會從本身的做用域鏈的上一層繼承this。

再看本文前面給的結論:post

「箭頭函數」的this,老是指向定義時所在的對象,而不是運行時所在的對象。

因爲 JS 中只有全局做用域和函數做用域,箭頭函數在定義時的上一層做用域是全局環境,全局環境中的 this 指向全局對象自己,即 window。

3、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。

4、構造函數中的 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。

5、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;

6、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、嵌套箭頭函數

// 例 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系列

歡迎關注個人我的公衆號「謝南波」,專一分享原創文章。

相關文章
相關標籤/搜索