詳解前端 this
JavaScript 中的?this
,因其靈活的指向、復(fù)雜的使用場(chǎng)景一直是面試中的熱點(diǎn),不論是初級(jí)還是中高級(jí)開發(fā)者,這都是一道必考題。這個(gè)概念雖然基礎(chǔ),但是非常重要,是否能深刻理解?this
,是前端 JavaScript 中進(jìn)階的重要一環(huán)。this
?指向多變,很多隱蔽的 bug 都緣于它。與此同時(shí),this
?強(qiáng)大靈活,如果能熟練駕馭,就會(huì)寫出更簡(jiǎn)潔、優(yōu)雅的代碼。
社區(qū)上對(duì)于?this
?的講解雖然不少,但缺乏統(tǒng)一梳理。本節(jié)課,讓我們直面?this
?的方方面面,并通過例題真正領(lǐng)會(huì)與掌握?this
。
this
?相關(guān)知識(shí)點(diǎn)如下:
this 到底指向誰
曾經(jīng)在面試阿里某重點(diǎn)部門時(shí),面試官?gòu)亩鄠€(gè)角度考察過我對(duì)?this
?的理解:全局環(huán)境下的this
、箭頭函數(shù)的?this
、構(gòu)造函數(shù)的?this
、this
?的顯隱性和優(yōu)先級(jí),等等。盡管我能一一作答,可是最后的問題:請(qǐng)用一句話總結(jié)?this
?的指向,注意只用一句話。?我卻犯難了。
有一種廣泛流傳的說法是:
誰調(diào)用它,
this
?就指向誰。
也就是說,this
?的指向是在調(diào)用時(shí)確定的。這么說沒有太大的問題,可是并不全面。面試官要求我用更加規(guī)范的語言進(jìn)行總結(jié),那么他到底在等什么樣的回答呢?
我們還要回到 JavaScript 中一個(gè)最基本的概念分析——執(zhí)行上下文,這個(gè)概念,我們會(huì)在下一講《老司機(jī)也會(huì)在閉包相關(guān)知識(shí)點(diǎn)翻車》中進(jìn)行擴(kuò)展。
事實(shí)上,調(diào)用函數(shù)會(huì)創(chuàng)建新的屬于函數(shù)自身的執(zhí)行上下文。執(zhí)行上下文的調(diào)用創(chuàng)建階段會(huì)決定?this
?的指向。到此,我們可以得出的一個(gè)結(jié)論:
this
?的指向,是在調(diào)用函數(shù)時(shí)根據(jù)執(zhí)行上下文所動(dòng)態(tài)確定的。
具體環(huán)節(jié)和規(guī)則,可以先“死記硬背”以下幾條規(guī)律,后面再慢慢一一分析:
- 在函數(shù)體中,簡(jiǎn)單調(diào)用該函數(shù)時(shí)(非顯式/隱式綁定下),嚴(yán)格模式下?
this
?綁定到?undefined
,否則綁定到全局對(duì)象?window
/global
; - 一般構(gòu)造函數(shù)?
new
?調(diào)用,綁定到新創(chuàng)建的對(duì)象上; - 一般由?
call
/apply
/bind
?方法顯式調(diào)用,綁定到指定參數(shù)的對(duì)象上; - 一般由上下文對(duì)象調(diào)用,綁定在該對(duì)象上;
- 箭頭函數(shù)中,根據(jù)外層上下文綁定的?
this
?決定?this
?指向。
當(dāng)然,真實(shí)環(huán)境多樣,我們來逐一梳理。
實(shí)戰(zhàn)例題分析
例題組合 1:全局環(huán)境下的 this
這種情況相對(duì)簡(jiǎn)單直接,函數(shù)在瀏覽器全局環(huán)境中被簡(jiǎn)單調(diào)用,非嚴(yán)格模式下?this
?指向?window
;在?use strict
?指明嚴(yán)格模式的情況下就是?undefined
。我們來看例題,請(qǐng)描述打印結(jié)果:
function f1 () {
console.log(this)
}
function f2 () {
'use strict'
console.log(this)
}
f1() // window
f2() // undefined
這樣的題目比較基礎(chǔ),但是需要候選人格外注意其變種,請(qǐng)?jiān)倏匆坏李}目:
const foo = {
bar: 10,
fn: function() {
console.log(this)
console.log(this.bar)
}
}
var fn1 = foo.fn
fn1()
這里?this
?仍然指向的是?window
。雖然?fn
?函數(shù)在?foo
?對(duì)象中作為方法被引用,但是在賦值給?fn1
?之后,fn1
?的執(zhí)行仍然是在?window
?的全局環(huán)境中。因此輸出?window
?和?undefined
,它們相當(dāng)于:
console.log(window)
console.log(window.bar)
還是上面這道題目,如果調(diào)用改變?yōu)椋?/span>
const foo = {
bar: 10,
fn: function() {
console.log(this)
console.log(this.bar)
}
}
foo.fn()
將會(huì)輸出:
{bar: 10, fn: ?}
10
因?yàn)檫@個(gè)時(shí)候?this
?指向的是最后調(diào)用它的對(duì)象,在?foo.fn()
?語句中?this
?指向?foo
?對(duì)象。請(qǐng)記住:
在執(zhí)行函數(shù)時(shí),如果函數(shù)中的?this
?是被上一級(jí)的對(duì)象所調(diào)用,那么?this
?指向的就是上一級(jí)的對(duì)象;否則指向全局環(huán)境。
例題組合 2:上下文對(duì)象調(diào)用中的 this
如上結(jié)論,面對(duì)下題時(shí)我們便不再困惑:
const student = {
name: 'Lucas',
fn: function() {
return this
}
}
console.log(student.fn() === student)
最終結(jié)果將會(huì)返回?true
。
當(dāng)存在更復(fù)雜的調(diào)用關(guān)系時(shí),請(qǐng)看例題:
const person = {
name: 'Lucas',
brother: {
name: 'Mike',
fn: function() {
return this.name
}
}
}
console.log(person.brother.fn())
在這種嵌套的關(guān)系中,this
?指向最后調(diào)用它的對(duì)象,因此輸出將會(huì)是:Mike
。
到此,this
?的上下文對(duì)象調(diào)用已經(jīng)理解得比較清楚了。我們?cè)倏匆坏栏唠A的題目:
const o1 = {
text: 'o1',
fn: function() {
return this.text
}
}
const o2 = {
text: 'o2',
fn: function() {
return o1.fn()
}
}
const o3 = {
text: 'o3',
fn: function() {
var fn = o1.fn
return fn()
}
}
console.log(o1.fn())
console.log(o2.fn())
console.log(o3.fn())
答案是:o1
、o1
、undefined
,你答對(duì)了嗎?
我們來一一分析。
- 第一個(gè)?
console
?最簡(jiǎn)單,o1
?沒有問題。難點(diǎn)在第二個(gè)和第三個(gè)上面,關(guān)鍵還是看調(diào)用?this
?的那個(gè)函數(shù)。 - 第二個(gè)?
console
?的?o2.fn()
,最終還是調(diào)用?o1.fn()
,因此答案仍然是?o1
。 - 最后一個(gè),在進(jìn)行?
var fn = o1.fn
?賦值之后,是“裸奔”調(diào)用,因此這里的?this
?指向?window
,答案當(dāng)然是?undefined
。
如果面試者回答順利,可以緊接著追問,如果我們需要讓:
console.log(o2.fn())
輸出?o2
,該怎么做?
一般開發(fā)者可能會(huì)想到使用?bind/call/apply
?來對(duì)?this
?的指向進(jìn)行干預(yù),這確實(shí)是一種思路。但是我接著問,如果不能使用?bind/call/apply
,有別的方法嗎?
這樣可以考察候選人基礎(chǔ)掌握的深度以及隨機(jī)應(yīng)變的思維能力。答案為:
const o1 = {
text: 'o1',
fn: function() {
return this.text
}
}
const o2 = {
text: 'o2',
fn: o1.fn
}
console.log(o2.fn())
還是應(yīng)用那個(gè)重要的結(jié)論:this
?指向最后調(diào)用它的對(duì)象,在?fn
?執(zhí)行時(shí),掛到?o2
?對(duì)象上即可,我們提前進(jìn)行了賦值操作。
例題組合 3:bind/call/apply 改變 this 指向
上文提到 bind/call/apply,在這個(gè)概念上,比較常見的基礎(chǔ)考察點(diǎn)是:bind/call/apply 三個(gè)方法的區(qū)別。
這樣的問題相對(duì)基礎(chǔ),我們直接上答案:一句話總結(jié),他們都是用來改變相關(guān)函數(shù)?this
?指向的,但是?call/apply
?是直接進(jìn)行相關(guān)函數(shù)調(diào)用;bind
?不會(huì)執(zhí)行相關(guān)函數(shù),而是返回一個(gè)新的函數(shù),這個(gè)新的函數(shù)已經(jīng)自動(dòng)綁定了新的?this
?指向,開發(fā)者需要手動(dòng)調(diào)用即可。再具體的?call/apply
?之間的區(qū)別主要體現(xiàn)在參數(shù)設(shè)定上,這里不再展開。
用代碼來總結(jié):
const target = {}
fn.call(target, 'arg1', 'arg2')
相當(dāng)于:
const target = {}
fn.apply(target, ['arg1', 'arg2'])
相當(dāng)于:
const target = {}
fn.bind(target, 'arg1', 'arg2')()
具體基礎(chǔ)用法這里不再科普,如果讀者尚不清楚,需要自己補(bǔ)充一下知識(shí)點(diǎn)。
我們來看一道例題分析:
const foo = {
name: 'lucas',
logName: function() {
console.log(this.name)
}
}
const bar = {
name: 'mike'
}
console.log(foo.logName.call(bar))
將會(huì)輸出?mike
,這不難理解。但是對(duì) call/apply/bind 的高級(jí)考察往往會(huì)結(jié)合構(gòu)造函數(shù)以及組合式實(shí)現(xiàn)繼承。實(shí)現(xiàn)繼承的話題,我們會(huì)單獨(dú)講到。構(gòu)造函數(shù)的使用案例,我們結(jié)合接下來的例題組合進(jìn)行分析。
例題組合 4:構(gòu)造函數(shù)和 this
這方面最直接的例題為:
function Foo() {
this.bar = "Lucas"
}
const instance = new Foo()
console.log(instance.bar)
答案將會(huì)輸出?Lucas
。但是這樣的場(chǎng)景往往伴隨著下一個(gè)問題:new
?操作符調(diào)用構(gòu)造函數(shù),具體做了什么?以下供參考:
- 創(chuàng)建一個(gè)新的對(duì)象;
- 將構(gòu)造函數(shù)的?
this
?指向這個(gè)新對(duì)象; - 為這個(gè)對(duì)象添加屬性、方法等;
- 最終返回新對(duì)象。
以上過程,也可以用代碼表述:
var obj = {}
obj.__proto__ = Foo.prototype
Foo.call(obj)
當(dāng)然,這里對(duì)?new
?的模擬是一個(gè)簡(jiǎn)單基本版的,更復(fù)雜的情況我們會(huì)在原型、原型鏈相關(guān)的第2-5課《面向?qū)ο蠛驮汀啦贿^時(shí)的話題》中講述。
需要指出的是,如果在構(gòu)造函數(shù)中出現(xiàn)了顯式?return
?的情況,那么需要注意分為兩種場(chǎng)景:
function Foo(){
this.user = "Lucas"
const o = {}
return o
}
const instance = new Foo()
console.log(instance.user)
將會(huì)輸出?undefined
,此時(shí)?instance
?是返回的空對(duì)象?o
。
function Foo(){
this.user = "Lucas"
return 1
}
const instance = new Foo()
console.log(instance.user)
將會(huì)輸出?Lucas
,也就是說此時(shí)?instance
?是返回的目標(biāo)對(duì)象實(shí)例?this
。
結(jié)論:如果構(gòu)造函數(shù)中顯式返回一個(gè)值,且返回的是一個(gè)對(duì)象,那么?this
?就指向這個(gè)返回的對(duì)象;如果返回的不是一個(gè)對(duì)象,那么?this
?仍然指向?qū)嵗?/p>
例題組合 5:箭頭函數(shù)中的 this 指向
首先我們?cè)賮頊亓?xí)一下相關(guān)結(jié)論。
結(jié)論:箭頭函數(shù)使用?this
?不適用以上標(biāo)準(zhǔn)規(guī)則,而是根據(jù)外層(函數(shù)或者全局)上下文來決定。
來看題目:
const foo = {
fn: function () {
setTimeout(function() {
console.log(this)
})
}
}
console.log(foo.fn())
這道題中,this
?出現(xiàn)在?setTimeout()
?中的匿名函數(shù)里,因此?this
?指向?window
?對(duì)象。如果需要?this
?指向?foo
?這個(gè) object 對(duì)象,可以巧用箭頭函數(shù)解決:
const foo = {
fn: function () {
setTimeout(() => {
console.log(this)
})
}
}
console.log(foo.fn())
// {fn: ?}
單純箭頭函數(shù)中的?this
?非常簡(jiǎn)單,但是綜合所有情況,結(jié)合?this
?的優(yōu)先級(jí)考察,這時(shí)候?this
?指向并不好確定。請(qǐng)繼續(xù)閱讀。
例題組合 6:this 優(yōu)先級(jí)相關(guān)
我們常常把通過?call
、apply
、bind
、new
?對(duì)?this
?綁定的情況稱為顯式綁定;根據(jù)調(diào)用關(guān)系確定的?this
?指向稱為隱式綁定。
那么顯式綁定和隱式綁定誰的優(yōu)先級(jí)更高呢?
請(qǐng)看例題:
function foo (a) {
console.log(this.a)
}
const obj1 = {
a: 1,
foo: foo
}
const obj2 = {
a: 2,
foo: foo
}
obj1.foo.call(obj2)
obj2.foo.call(obj1)
輸出分別為 2、1,也就是說?call
、apply
?的顯式綁定一般來說優(yōu)先級(jí)更高。
function foo (a) {
this.a = a
}
const obj1 = {}
var bar = foo.bind(obj1)
bar(2)
console.log(obj1.a)
上述代碼通過?bind
,將?bar
?函數(shù)中的?this
?綁定為?obj1
?對(duì)象。執(zhí)行?bar(2)
?后,obj1.a
?值為 2。即經(jīng)過?bar(2)
?執(zhí)行后,obj1
?對(duì)象為:{a: 2}
。
當(dāng)再使用?bar
?作為構(gòu)造函數(shù)時(shí):
var baz = new bar(3)
console.log(baz.a)
將會(huì)輸出 3。我們看?bar
?函數(shù)本身是通過?bind
?方法構(gòu)造的函數(shù),其內(nèi)部已經(jīng)對(duì)將?this
?綁定為?obj1
,它再作為構(gòu)造函數(shù),通過?new
?調(diào)用時(shí),返回的實(shí)例已經(jīng)與?obj1
?解綁。 也就是說:
new
?綁定修改了?bind
?綁定中的?this
,因此?new
?綁定的優(yōu)先級(jí)比顯式?bind
?綁定更高。
我們?cè)倏矗?/p>
function foo() {
return a => {
console.log(this.a)
};
}
const obj1 = {
a: 2
}
const obj2 = {
a: 3
}
const bar = foo.call(obj1)
console.log(bar.call(obj2))
將會(huì)輸出 2。由于?foo()
?的?this
?綁定到?obj1
,bar
(引用箭頭函數(shù))的?this
?也會(huì)綁定到?obj1
,箭頭函數(shù)的綁定無法被修改。
如果將?foo
?完全寫成箭頭函數(shù)的形式:
var a = 123
const foo = () => a => {
console.log(this.a)
}
const obj1 = {
a: 2
}
const obj2 = {
a: 3
}
var bar = foo.call(obj1)
console.log(bar.call(obj2))
將會(huì)輸出?123
。
這里我再“抖個(gè)機(jī)靈”,僅僅將上述代碼的第一處變量?a
?的賦值改為:
const a = 123
const foo = () => a => {
console.log(this.a)
}
const obj1 = {
a: 2
}
const obj2 = {
a: 3
}
var bar = foo.call(obj1)
console.log(bar.call(obj2))
答案將會(huì)輸出為?undefined
,原因是因?yàn)槭褂?span>?const
?聲明的變量不會(huì)掛載到?window
?全局對(duì)象當(dāng)中。因此?this
?指向?window
?時(shí),自然也找不到?a
?變量了。關(guān)于?const
?或者?let
?等聲明變量的方式不再本課的主題當(dāng)中,我們后續(xù)也將專門進(jìn)行介紹。
到這里,讀者是否有“融會(huì)貫通”的感覺了呢?如果還有困惑,也不要灰心。進(jìn)階的關(guān)鍵就是基礎(chǔ),基礎(chǔ)需要反復(fù)學(xué)習(xí),“死記硬背”后才能慢慢體會(huì)。
開放例題分析
不知道實(shí)戰(zhàn)例題分析是否已經(jīng)把你繞暈了。事實(shí)上,this
?的指向涉及的規(guī)范繁多,優(yōu)先級(jí)也較為混亂。刻意刁難并不是很好的面試做法,一些細(xì)節(jié)候選人如果沒有記住也不是太大的問題。作為面試官,我往往會(huì)另辟蹊徑,出一些開放性題目。
其中,最典型的一道題目為:實(shí)現(xiàn)一個(gè)?bind
?函數(shù)。
作為面試者,我也曾經(jīng)在頭條的面試流程中被問到模擬?bind
。這道題并不新鮮,部分讀者也會(huì)有自己的解答思路,而且社區(qū)上關(guān)于原生?bind
?的研究也很多。但是,我們這里想強(qiáng)調(diào)的是,可能有一些細(xì)節(jié)被大家忽略了。在回答時(shí),我往往先實(shí)現(xiàn)一個(gè)初級(jí)版本,然后根據(jù) ES5-shim 源碼進(jìn)一步說明。
Function.prototype.bind = Function.prototype.bind || function (context) {
var me = this;
var args = Array.prototype.slice.call(arguments, 1);
return function bound () {
var innerArgs = Array.prototype.slice.call(arguments);
var finalArgs = args.concat(innerArgs);
return me.apply(context, finalArgs);
}
}
這樣的實(shí)現(xiàn)已經(jīng)非常不錯(cuò)了。但是,就如同之前?this
?優(yōu)先級(jí)分析所示:bind
?返回的函數(shù)如果作為構(gòu)造函數(shù),搭配?new
?關(guān)鍵字出現(xiàn)的話,我們的綁定?this
?就需要“被忽略”。
為了實(shí)現(xiàn)這樣的規(guī)則,開發(fā)者就應(yīng)該需要考慮如何區(qū)分這兩種調(diào)用方式。具體來講?bound
?函數(shù)中就要進(jìn)行?this instanceof
?的判斷。
另外一個(gè)細(xì)節(jié)是,函數(shù)具有?length
?屬性,表示形參的個(gè)數(shù)。上述實(shí)現(xiàn)方式形參的個(gè)數(shù)顯然會(huì)失真。我們的實(shí)現(xiàn)就需要對(duì)?length
?屬性進(jìn)行還原。可是難點(diǎn)在于:函數(shù)的?length
?屬性值是不可重寫的。
總結(jié)
通過本課的學(xué)習(xí),我們看到?this
?紛繁多象,確實(shí)不容易徹底掌握。本節(jié)盡可能系統(tǒng)地進(jìn)行講解、說明,例題盡可能地覆蓋更多 case。與此同時(shí),需要讀者在閱讀之外繼續(xù)進(jìn)行消化與吸收。只有“記死”,才能“用活”。