一文讀懂 JavaScript 中的 this 關鍵字


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

this 是一個令無數 JavaScript 編程者又愛又恨的知識點。它的重要性毋庸置疑,然而真正想掌握它卻並非易事。希望本文可以幫助大家理解 this。

JavaScript 中的 this

JavaScript 引擎在查找 this 時不會通過原型鏈一層一層的查找,因為 this 完全是在函數調用時才可以確定的,讓我們來看下面幾種函數調用的形式。

Function Invocation Pattern

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

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

foo() // Window

Method Invocation Pattern

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

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。如果 thisObj 是 null 或者 undefined ,其中的 this 會指向全局上下文 Window(在瀏覽器中)。


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

本文接下來的內容翻譯自 https://blog.bitsrc.io/what-is-this-in-javascript-3b03480514a7,作者 Rajat S,內容有刪改,標題有改動。

如果你已經使用過一些 JavaScript 庫,你一定會注意到一個特殊的關鍵字 this。

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 中運行將不會出現我們預期的 Superman 和 Clark 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 或者 undefined 給 call 、 apply 作為上下文,將會導致 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

call 和 apply 的模擬實現大同小異,注意 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))
}
}

最後

往期精彩:

  • 前端面試必會 | 一文讀懂現代 JavaScript 中的變量提升 - let、const 和 var
  • 前端面試必會 | 一文讀懂 JavaScript 中的閉包
  • 前端面試必會 | 一文讀懂 JavaScript 中的作用域和作用域鏈
  • 前端面試必會 | 一文讀懂 JavaScript 中的執行上下文
  • IntersectionObserver 和懶加載
  • 初探瀏覽器渲染原理
  • CSS 盒模型、佈局和包含塊
  • 詳細解讀 CSS 選擇器優先級


分享到:


相關文章: