JavaScript之例題中完全理解this

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

概述

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

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

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

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

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

1、隱式綁定

// 例 1
var name = "window";

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

    console.log(this.name);
}

foo();  // ?

輸出:閉包

windowapp

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

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

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();  // ?

輸出:

inner
window

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

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

再看本文前面給的結論:

「箭頭函數」的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系列

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

掘金專欄 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
相關文章
相關標籤/搜索