一文讀懂 JavaScript 中的 this 關(guān)鍵字
this 是一個(gè)令無(wú)數(shù) JavaScript 編程者又愛(ài)又恨的知識(shí)點(diǎn)。它的重要性毋庸置疑,然而真正想掌握它卻并非易事。希望本文可以幫助大家理解 this。
JavaScript 中的 this
JavaScript 引擎在查找 this 時(shí)不會(huì)通過(guò)原型鏈一層一層的查找,因?yàn)?this 完全是在函數(shù)調(diào)用時(shí)才可以確定的,讓我們來(lái)看下面幾種函數(shù)調(diào)用的形式。
1. Function Invocation Pattern
普通的函數(shù)調(diào)用,這是我們使用較多的一種, foo 是以單獨(dú)的變量出現(xiàn)而不是屬性。其中的 this 指向全局對(duì)象。
- function foo() {
- console.log(this)
- }
- foo() // Window
函數(shù)作為對(duì)象的方法調(diào)用,會(huì)通過(guò) obj.func 或者 obj[func] 的形式調(diào)用。其中的 this 指向調(diào)用它的對(duì)象。
- const obj = {
- name: 'lxfriday',
- getName(){
- console.log(this.name)
- }
- }
- obj.getName() // lxfriday
2. Constructor Pattern
通過(guò) new Constructor() 的形式調(diào)用,其 this 會(huì)指向新生成的對(duì)象。
- function Person(name){
- this.name = name
- }
- const person = new Person('lxfriday')
- console.log(person.name) // lxfriday
3. Apply Pattern
通過(guò) foo.apply(thisObj) 或者 foo.call(thisObj) 的形式調(diào)用,其中的 this 指向 thisObj。如果 thisObj 是 null 或者 undefined ,其中的 this 會(huì)指向全局上下文 Window(在瀏覽器中)。
掌握以上的幾種函數(shù)調(diào)用形式就基本可以覆蓋開(kāi)發(fā)中遇到的常見(jiàn)問(wèn)題了,下面我翻譯了一篇文章,幫助你更深入的理解 this。
如果你已經(jīng)使用過(guò)一些 JavaScript 庫(kù),你一定會(huì)注意到一個(gè)特殊的關(guān)鍵字 this。
this 在 JavaScript 中很常見(jiàn),但是有很多開(kāi)發(fā)人員花了很多時(shí)間來(lái)完全理解 this 關(guān)鍵字的確切功能以及在代碼中何處使用。
在這篇文章中,我將幫助您深入了解 this 其機(jī)制。
在深入了解之前,請(qǐng)確保已在系統(tǒng)上安裝了 Node 。然后,打開(kāi)命令終端并運(yùn)行 node 命令。
全局環(huán)境中的 this
this 的工作機(jī)制并不容易理解。為了理解 this 是如何工作的,我們將探索不同環(huán)境中的 this。首先我們從全局上下文開(kāi)始。
在全局層面中,this 等同于全局對(duì)象,在 Node repl(交互式命令行) 環(huán)境中叫 global。
- $ node
- > this === global
- true
但上述情況只出現(xiàn)在 Node repl 環(huán)境中,如果我們?cè)?JS 文件中跑相同的代碼,我們將會(huì)得到不同的答案。
為了測(cè)試,我們創(chuàng)建一個(gè) index.js 的文件,并添加下面的代碼:
- console.log(this === global);
然后通過(guò) node 命令運(yùn)行:
- $ node index.js
- false
出現(xiàn)上面情況的原因是在 JS 文件中, this 指向 module.exports,并不是指向 global。
函數(shù)中的 this
Function Invocation Pattern
在函數(shù)中 this 的指向取決于函數(shù)的調(diào)用形式。所以,函數(shù)每次執(zhí)行的時(shí)候,可能擁有不同的 this 指向。
在 index.js 文件中,編寫(xiě)一個(gè)非常簡(jiǎn)單的函數(shù)來(lái)檢查 this 是否指向全局對(duì)象:
- function fat() {
- console.log(this === global)
- }
- fat()
如果我們?cè)?Node repl 環(huán)境執(zhí)行上面的代碼,將會(huì)得到 true,但是如果添加 use strict 到首行,將會(huì)得到 false,因?yàn)檫@個(gè)時(shí)候 this 的值為 undefined。
為了進(jìn)一步說(shuō)明這一點(diǎn),讓我們創(chuàng)建一個(gè)定義超級(jí)英雄的真實(shí)姓名和英雄姓名的簡(jiǎn)單函數(shù)。
- function Hero(heroName, realName) {
- this.realName = realName;
- this.heroName = heroName;
- }
- const superman= Hero("Superman", "Clark Kent");
- console.log(superman);
請(qǐng)注意,這個(gè)函數(shù)不是在嚴(yán)格模式下執(zhí)行的。代碼在 node 中運(yùn)行將不會(huì)出現(xiàn)我們預(yù)期的 Superman 和 Clark Kent ,我們將得到 undefined。
這背后的原因是由于該函數(shù)不是以嚴(yán)格模式編寫(xiě)的,所以 this 引用了全局對(duì)象。
如果我們?cè)趪?yán)格模式下運(yùn)行這段代碼,會(huì)因?yàn)?JavaScript 不允許給 undefined 增加屬性而出現(xiàn)錯(cuò)誤。這實(shí)際上是一件好事,因?yàn)樗柚刮覀儎?chuàng)建全局變量。
最后,以大寫(xiě)形式編寫(xiě)函數(shù)的名稱(chēng)意味著我們需要使用 new 運(yùn)算符將其作為構(gòu)造函數(shù)來(lái)調(diào)用。將上面的代碼片段的最后兩行替換為:
- const superman = new Hero("Superman", "Clark Kent");
- console.log(superman);
再次運(yùn)行 node index.js 命令,您現(xiàn)在將獲得預(yù)期的輸出。
構(gòu)造函數(shù)中的 this
Constructor Pattern
JavaScript 沒(méi)有任何特殊的構(gòu)造函數(shù)。我們所能做的就是使用 new 運(yùn)算符將函數(shù)調(diào)用轉(zhuǎn)換為構(gòu)造函數(shù)調(diào)用,如上一節(jié)所示。
進(jìn)行構(gòu)造函數(shù)調(diào)用時(shí),將創(chuàng)建一個(gè)新對(duì)象并將其設(shè)置為函數(shù)的 this 參數(shù)。然后,從函數(shù)隱式返回該對(duì)象,除非我們有另一個(gè)要顯式返回的對(duì)象。
在 hero 函數(shù)內(nèi)部編寫(xiě)以下 return 語(yǔ)句:
- return {
- heroName: "Batman",
- realName: "Bruce Wayne",
- };
如果現(xiàn)在運(yùn)行 node 命令,我們將看到 return 語(yǔ)句將覆蓋構(gòu)造函數(shù)調(diào)用。
當(dāng) return 語(yǔ)句嘗試返回不是對(duì)象的任何東西時(shí),將隱式返回 this。
方法中的 this
Method Invocation Pattern
當(dāng)將函數(shù)作為對(duì)象的方法調(diào)用時(shí),this 指向該對(duì)象,然后將該對(duì)象稱(chēng)為該函數(shù)調(diào)用的接收者。
在下面代碼中,有一個(gè) dialogue 方法在 hero 對(duì)象內(nèi)。通過(guò) hero.dialogue() 形式調(diào)用時(shí),dialogue 中的 this 就會(huì)指向 hero 本身。這里,hero 就是 dialogue 方法調(diào)用的接收者。
- const hero = {
- heroName: "Batman",
- dialogue() {
- console.log(`I am ${this.heroName}!`);
- }
- };
- hero.dialogue();
上面的代碼非常簡(jiǎn)單,但是實(shí)際開(kāi)發(fā)時(shí)有可能方法調(diào)用的接收者并不是原對(duì)象。看下面的代碼:
- const saying = hero.dialogue();
- saying();
這里,我們把方法賦值給一個(gè)變量,然后執(zhí)行這個(gè)變量指向的函數(shù),你會(huì)發(fā)現(xiàn) this 的值是 undefined。這是因?yàn)?dialogue 方法已經(jīng)無(wú)法跟蹤原來(lái)的接收者對(duì)象,函數(shù)現(xiàn)在指向的是全局對(duì)象。
當(dāng)我們將一個(gè)方法作為回調(diào)傳遞給另一個(gè)方法時(shí),通常會(huì)發(fā)生接收器的丟失。我們可以通過(guò)添加包裝函數(shù)或使用 bind 方法將 this 綁定到特定對(duì)象來(lái)解決此問(wèn)題。
call、apply
Apply Pattern
盡管函數(shù)的 this 值是隱式設(shè)置的,但我們也可以通過(guò) call()和 apply() 顯式地綁定 this。
讓我們像這樣重組前面的代碼片段:
- function dialogue () {
- console.log (`I am ${this.heroName}`);
- }
- const hero = {
- heroName: 'Batman',
- };
我們需要將hero 對(duì)象作為接收器與 dialogue 函數(shù)連接。為此,我們可以使用 call() 或 apply() 來(lái)實(shí)現(xiàn)連接:
- dialogue.call(hero)
- // or
- dialogue.apply(hero)
需要注意的是,在非嚴(yán)格模式下,如果傳遞 null 或者 undefined 給 call 、 apply 作為上下文,將會(huì)導(dǎo)致 this 指向全局對(duì)象。
- function dialogue() {
- console.log('this', this)
- }
- const hero = {
- heroName: 'Batman',
- }
- console.log(dialogue.call(null))
上述代碼,在嚴(yán)格模式下輸出 null,非嚴(yán)格模式下輸出全局對(duì)象。
bind
當(dāng)我們將一個(gè)方法作為回調(diào)傳遞給另一個(gè)函數(shù)時(shí),始終存在丟失該方法的預(yù)期接收者的風(fēng)險(xiǎn),導(dǎo)致將 this 參數(shù)設(shè)置為全局對(duì)象。
bind() 方法允許我們將 this 參數(shù)永久綁定到函數(shù)。因此,在下面的代碼片段中,bind 將創(chuàng)建一個(gè)新 dialogue 函數(shù)并將其 this 值設(shè)置為 hero。
- const hero = {
- heroName: "Batman",
- dialogue() {
- console.log(`I am ${this.heroName}`);
- }
- };
- // 1s 后打?。篒 am Batman
- setTimeout(hero.dialogue.bind(hero), 1000);
注意:對(duì)于用 bind 綁定 this 之后新生成的函數(shù),使用 call 或者 apply 方法無(wú)法更改這個(gè)新函數(shù)的 this。
箭頭函數(shù)中的 this
箭頭函數(shù)和普通函數(shù)有很大的不同,引用阮一峰 ES6入門(mén)第六章中的介紹:
- 函數(shù)體內(nèi)的 this 對(duì)象,就是定義時(shí)所在的對(duì)象,而不是使用時(shí)所在的對(duì)象;
- 不可以當(dāng)作構(gòu)造函數(shù),也就是說(shuō),不可以使用 new 命令,否則會(huì)拋出一個(gè)錯(cuò)誤;
- 不可以使用 arguments 對(duì)象,該對(duì)象在函數(shù)體內(nèi)不存在。如果要用,可以用 rest 參數(shù)代替;
- 不可以使用 yield 命令,因此箭頭函數(shù)不能用作 Generator 函數(shù);
上面四點(diǎn)中,第一點(diǎn)尤其值得注意。this 對(duì)象的指向是可變的,但是在箭頭函數(shù)中,它是固定的,它只指向箭頭函數(shù)定義時(shí)的外層 this,箭頭函數(shù)沒(méi)有自己的 this,所有綁定 this 的操作,如 call apply bind 等,對(duì)箭頭函數(shù)中的 this 綁定都是無(wú)效的。
讓們看下面的代碼:
- const batman = this;
- const bruce = () => {
- console.log(this === batman);
- };
- bruce();
在這里,我們將 this 的值存儲(chǔ)在變量中,然后將該值與箭頭函數(shù)內(nèi)部的 this 值進(jìn)行比較。node index.js 執(zhí)行時(shí)將會(huì)輸出 true。
那箭頭函數(shù)中的 this 可以做哪些事情呢?
箭頭函數(shù)可以幫助我們?cè)诨卣{(diào)中訪問(wèn) this??匆幌挛以谙旅鎸?xiě)的 counter 對(duì)象:
- const counter = {
- count: 0,
- increase() {
- setInterval(function() {
- console.log(++this.count);
- }, 1000);
- }
- }
- counter.increase();
運(yùn)行上面的代碼,會(huì)打印 NaN。這是因?yàn)?this.count 沒(méi)有指向 counter 對(duì)象。它實(shí)際上指向全局對(duì)象。
要使此計(jì)數(shù)器工作,可以用箭頭函數(shù)重寫(xiě),下面代碼將會(huì)正常運(yùn)行:
- const counter = {
- count: 0,
- increase () {
- setInterval (() => {
- console.log (++this.count);
- }, 1000);
- },
- };
- counter.increase ();
類(lèi)中的 this
類(lèi)是所有 JavaScript 應(yīng)用程序中最重要的部分之一。讓我們看看類(lèi)內(nèi)部 this 的行為。
一個(gè)類(lèi)通常包含一個(gè) constructor,其中 this 將指向新創(chuàng)建的對(duì)象。
但是,在使用方法的情況下,如果該方法以普通函數(shù)的形式調(diào)用,則 this 也可以指向任何其他值。就像一個(gè)方法一樣,類(lèi)也可能無(wú)法跟蹤接收者。
我們用類(lèi)重寫(xiě)上面的 Hero 函數(shù)。此類(lèi)將包含構(gòu)造函數(shù)和 dialogue() 方法。最后,我們創(chuàng)建此類(lèi)的實(shí)例并調(diào)用該 dialogue 方法。
- class Hero {
- constructor(heroName) {
- this.heroName = heroName;
- }
- dialogue() {
- console.log(`I am ${this.heroName}`)
- }
- }
- const batman = new Hero("Batman");
- batman.dialogue();
constructor 中的 this 指向新創(chuàng)建的類(lèi)實(shí)例。batman.dialogue() 調(diào)用時(shí),我們將 dialogue() 作為 batman 接收器的方法調(diào)用。
但是,如果我們存儲(chǔ)對(duì) dialogue() 方法的引用,然后將其作為函數(shù)調(diào)用,則我們將再次失去方法的接收者,而 this 現(xiàn)在指向 undefined。
為什么是指向 undefined 呢?這是因?yàn)?JavaScript 類(lèi)內(nèi)部隱式以嚴(yán)格模式運(yùn)行。我們將 say() 作為一個(gè)函數(shù)調(diào)用而沒(méi)有進(jìn)行綁定。所以我們要手動(dòng)的綁定。
- const say = batman.dialogue.bind(batman);
- say();
當(dāng)然,我們也可以在構(gòu)造函數(shù)內(nèi)部綁定:
- class Hero {
- constructor(heroName) {
- this.heroName = heroName
- thisthis.dialogue = this.dialogue.bind(this)
- }
- dialogue() {
- console.log(`I am ${this.heroName}`)
- }
- }
加餐:手寫(xiě) call、apply、bind
call 和 apply 的模擬實(shí)現(xiàn)大同小異,注意 apply 的參數(shù)是一個(gè)數(shù)組,綁定 this 都采用的是對(duì)象調(diào)用方法的形式。
- Function.prototype.call = function(thisObj) {
- thisObjthisObj = 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) {
- thisObjthisObj = 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) {
- thisObjthisObj = thisObj || window
- const that = this // func
- const outerArgs = [...arguments].slice(1)
- return function(...innerArgs) {
- return that.apply(thisObj, outerArgs.concat(innerArgs))
- }
- }