前端面試必會 | 一文讀懂 JavaScript 中的 this 關鍵字

this 是一個令無數 JavaScript 編程者又愛又恨的知識點。它的重要性毋庸置疑,然而真正想掌握它卻並不是易事。但願本文能夠幫助你們理解 thisjavascript

JavaScript 中的 this

JavaScript 引擎在查找 this 時不會經過原型鏈一層一層的查找,由於 this 徹底是在函數調用時才能夠肯定的,讓咱們來看下面幾種函數調用的形式。前端

Function Invocation Pattern

普通的函數調用,這是咱們使用較多的一種, foo 是以單獨的變量出現而不是屬性。其中的 this 指向全局對象。java

function foo() {
  console.log(this)
}

foo() // Window
複製代碼

Method Invocation Pattern

函數做爲對象的方法調用,會經過 obj.func 或者 obj[func] 的形式調用。其中的 this 指向調用它的對象。node

const obj = {
  name: 'lxfriday',
  getName(){
    console.log(this.name)
  }
}

obj.getName() // lxfriday
複製代碼

Constructor Pattern

經過 new Constructor() 的形式調用,其 this 會指向新生成的對象。面試

function Person(name){
  this.name = name
}

const person = new Person('lxfriday')
console.log(person.name) // lxfriday
複製代碼

Apply Pattern

經過 foo.apply(thisObj) 或者 foo.call(thisObj) 的形式調用,其中的 this 指向 thisObj。若是 thisObjnull 或者 undefined ,其中的 this 會指向全局上下文 Window(在瀏覽器中)。編程


掌握以上的幾種函數調用形式就基本能夠覆蓋開發中遇到的常見問題了,下面我翻譯了一篇文章,幫助你更深刻的理解 this數組

本文接下來的內容翻譯自 blog.bitsrc.io/what-is-thi…,做者 Rajat S,內容有刪改,標題有改動。瀏覽器

若是你已經使用過一些 JavaScript 庫,你必定會注意到一個特殊的關鍵字 thisbash

this 在 JavaScript 中很常見,可是有不少開發人員花了不少時間來徹底理解 this 關鍵字的確切功能以及在代碼中何處使用。閉包

在這篇文章中,我將幫助您深刻了解 this 其機制。

在深刻了解以前,請確保已在系統上安裝了 Node 。而後,打開命令終端並運行 node 命令。

全局環境中的 this

this 的工做機制並不容易理解。爲了理解 this 是如何工做的,咱們將探索不一樣環境中的 this。首先咱們從全局上下文開始。

在全局層面中,this 等同於全局對象,在 Node repl(交互式命令行) 環境中叫 global

$ node
> this === global
true
複製代碼

但上述狀況只出如今 Node repl 環境中,若是咱們在 JS 文件中跑相同的代碼,咱們將會獲得不一樣的答案。

爲了測試,咱們建立一個 index.js 的文件,並添加下面的代碼:

console.log(this === global);
複製代碼

而後經過 node 命令運行:

$ node index.js
false 
複製代碼

出現上面狀況的緣由是在 JS 文件中, this 指向 module.exports,並非指向 global

函數中的 this

Function Invocation Pattern

在函數中 this 的指向取決於函數的調用形式。因此,函數每次執行的時候,可能擁有不一樣的 this 指向。

index.js 文件中,編寫一個很是簡單的函數來檢查 this 是否指向全局對象:

function fat() {
  console.log(this === global)
}
fat()
複製代碼

若是咱們在 Node repl 環境執行上面的代碼,將會獲得 true,可是若是添加 use strict 到首行,將會獲得 false,由於這個時候 this 的值爲 undefined

爲了進一步說明這一點,讓咱們建立一個定義超級英雄的真實姓名和英雄姓名的簡單函數。

function Hero(heroName, realName) {
  this.realName = realName;
  this.heroName = heroName;
}
const superman= Hero("Superman", "Clark Kent");
console.log(superman);
複製代碼

請注意,這個函數不是在嚴格模式下執行的。代碼在 node 中運行將不會出現咱們預期的 SupermanClark Kent ,咱們將獲得 undefined

這背後的緣由是因爲該函數不是以嚴格模式編寫的,因此 this 引用了全局對象。

若是咱們在嚴格模式下運行這段代碼,會由於 JavaScript 不容許給 undefined 增長屬性而出現錯誤。這其實是一件好事,由於它阻止咱們建立全局變量。

最後,以大寫形式編寫函數的名稱意味着咱們須要使用 new 運算符將其做爲構造函數來調用。將上面的代碼片斷的最後兩行替換爲:

const superman = new Hero("Superman", "Clark Kent");
console.log(superman);
複製代碼

再次運行 node index.js 命令,您如今將得到預期的輸出。

構造函數中的 this

Constructor Pattern

JavaScript 沒有任何特殊的構造函數。咱們所能作的就是使用 new 運算符將函數調用轉換爲構造函數調用,如上一節所示。

進行構造函數調用時,將建立一個新對象並將其設置爲函數的 this 參數。而後,從函數隱式返回該對象,除非咱們有另外一個要顯式返回的對象。

hero 函數內部編寫如下 return 語句:

return {
  heroName: "Batman",
  realName: "Bruce Wayne",
};
複製代碼

若是如今運行 node 命令,咱們將看到 return 語句將覆蓋構造函數調用。

return 語句嘗試返回不是對象的任何東西時,將隱式返回 this

方法中的 this

Method Invocation Pattern

當將函數做爲對象的方法調用時,this 指向該對象,而後將該對象稱爲該函數調用的接收者。

在下面代碼中,有一個 dialogue 方法在 hero 對象內。經過 hero.dialogue() 形式調用時,dialogue 中的 this 就會指向 hero 自己。這裏,hero 就是 dialogue 方法調用的接收者。

const hero = {
  heroName: "Batman",
  dialogue() {
    console.log(`I am ${this.heroName}!`);
  }
};
hero.dialogue();
複製代碼

上面的代碼很是簡單,可是實際開發時有可能方法調用的接收者並非原對象。看下面的代碼:

const saying = hero.dialogue();
saying();
複製代碼

這裏,咱們把方法賦值給一個變量,而後執行這個變量指向的函數,你會發現 this 的值是 undefined。這是由於 dialogue 方法已經沒法跟蹤原來的接收者對象,函數如今指向的是全局對象。

當咱們將一個方法做爲回調傳遞給另外一個方法時,一般會發生接收器的丟失。咱們能夠經過添加包裝函數或使用 bind 方法將 this 綁定到特定對象來解決此問題。

call、apply

Apply Pattern

儘管函數的 this 值是隱式設置的,但咱們也能夠經過 call()apply() 顯式地綁定 this

讓咱們像這樣重組前面的代碼片斷:

function dialogue () {
  console.log (`I am ${this.heroName}`);
}
const hero = {
  heroName: 'Batman',
};
複製代碼

咱們須要將hero 對象做爲接收器與 dialogue 函數鏈接。爲此,咱們可使用 call()apply() 來實現鏈接:

dialogue.call(hero)
// or
dialogue.apply(hero)
複製代碼

須要注意的是,在非嚴格模式下,若是傳遞 null 或者 undefinedcallapply 做爲上下文,將會致使 this 指向全局對象。

function dialogue() {
  console.log('this', this)
}
const hero = {
  heroName: 'Batman',
}
console.log(dialogue.call(null))
複製代碼

上述代碼,在嚴格模式下輸出 null,非嚴格模式下輸出全局對象。

bind

當咱們將一個方法做爲回調傳遞給另外一個函數時,始終存在丟失該方法的預期接收者的風險,致使將 this 參數設置爲全局對象。

bind() 方法容許咱們將 this 參數永久綁定到函數。所以,在下面的代碼片斷中,bind 將建立一個新 dialogue 函數並將其 this 值設置爲 hero

const hero = {
  heroName: "Batman",
  dialogue() {
    console.log(`I am ${this.heroName}`);
  }
};
// 1s 後打印:I am Batman
setTimeout(hero.dialogue.bind(hero), 1000);
複製代碼

注意:對於用 bind 綁定 this 以後新生成的函數,使用 call 或者 apply 方法沒法更改這個新函數的 this

箭頭函數中的 this

箭頭函數和普通函數有很大的不一樣,引用阮一峯 ES6入門第六章中的介紹:

  1. 函數體內的 this 對象,就是定義時所在的對象,而不是使用時所在的對象
  2. 不能夠看成構造函數,也就是說,不可使用 new 命令,不然會拋出一個錯誤
  3. 不可使用 arguments 對象,該對象在函數體內不存在。若是要用,能夠用 rest 參數代替
  4. 不可使用 yield 命令,所以箭頭函數不能用做 Generator 函數

上面四點中,第一點尤爲值得注意。this 對象的指向是可變的,可是在箭頭函數中,它是固定的,它只指向箭頭函數定義時的外層 this箭頭函數沒有本身的 this,全部綁定 this 的操做,如 call apply bind 等,對箭頭函數中的 this 綁定都是無效的

讓們看下面的代碼:

const batman = this;
const bruce = () => {
  console.log(this === batman);
};
bruce();
複製代碼

在這裏,咱們將 this 的值存儲在變量中,而後將該值與箭頭函數內部的 this 值進行比較。node index.js 執行時將會輸出 true

那箭頭函數中的 this 能夠作哪些事情呢?

箭頭函數能夠幫助咱們在回調中訪問 this。看一下我在下面寫的 counter 對象:

const counter = {
  count: 0,
  increase() {
    setInterval(function() {
      console.log(++this.count);
    }, 1000);
  }
}
counter.increase();
複製代碼

運行上面的代碼,會打印 NaN。這是由於 this.count 沒有指向 counter 對象。它實際上指向全局對象。

要使此計數器工做,能夠用箭頭函數重寫,下面代碼將會正常運行:

const counter = {
  count: 0,
  increase () {
    setInterval (() => {
      console.log (++this.count);
    }, 1000);
  },
};
counter.increase ();
複製代碼

類中的 this

類是全部 JavaScript 應用程序中最重要的部分之一。讓咱們看看類內部 this 的行爲。

一個類一般包含一個 constructor,其中 this 將指向新建立的對象。

可是,在使用方法的狀況下,若是該方法以普通函數的形式調用,則 this 也能夠指向任何其餘值。就像一個方法同樣,類也可能沒法跟蹤接收者。

咱們用類重寫上面的 Hero 函數。此類將包含構造函數和 dialogue() 方法。最後,咱們建立此類的實例並調用該 dialogue 方法。

class Hero {
  constructor(heroName) {
    this.heroName = heroName;
  }
  dialogue() {
    console.log(`I am ${this.heroName}`)
  }
}
const batman = new Hero("Batman");
batman.dialogue();
複製代碼

constructor 中的 this 指向新建立的類實例。batman.dialogue() 調用時,咱們將 dialogue() 做爲 batman 接收器的方法調用。

可是,若是咱們存儲對 dialogue() 方法的引用,而後將其做爲函數調用,則咱們將再次失去方法的接收者,而 this 如今指向 undefined

爲何是指向 undefined 呢?這是由於 JavaScript 類內部隱式以嚴格模式運行。咱們將 say() 做爲一個函數調用而沒有進行綁定。因此咱們要手動的綁定。

const say = batman.dialogue.bind(batman);
say();
複製代碼

固然,咱們也能夠在構造函數內部綁定:

class Hero {
  constructor(heroName) {
    this.heroName = heroName
    this.dialogue = this.dialogue.bind(this)
  }
  dialogue() {
    console.log(`I am ${this.heroName}`)
  }
}
複製代碼

加餐:手寫 call、apply、bind

callapply 的模擬實現大同小異,注意 apply 的參數是一個數組,綁定 this 都採用的是對象調用方法的形式。

Function.prototype.call = function(thisObj) {
  thisObj = thisObj || window
  const funcName = Symbol('func')
  const that = this // func
  thisObj[funcName] = that
  const result = thisObj[funcName](...arguments)
  delete thisObj[funcName]
  return result
}

Function.prototype.apply = function(thisObj) {
  thisObj = thisObj || window
  const funcName = Symbol('func')
  const that = this // func
  const args = arguments[1] || []
  thisObj[funcName] = that
  const result = thisObj[funcName](...[thisObj, ...args])
  delete thisObj[funcName]
  return result
}

Function.prototype.bind = function(thisObj) {
  thisObj = thisObj || window
  const that = this // func
  const outerArgs = [...arguments].slice(1)
  return function(...innerArgs) {
    return that.apply(thisObj, outerArgs.concat(innerArgs))
  }
}
複製代碼

最後

往期精彩:

關注公衆號能夠看更多哦。

感謝閱讀,歡迎關注個人公衆號 雲影 sky,帶你解讀前端技術,掌握最本質的技能。關注公衆號能夠拉你進討論羣,有任何問題都會回覆。

公衆號
相關文章
相關標籤/搜索