雖然標(biāo)題是 this
、new
、bind
、call
、apply
,但實(shí)際上這些都離不開 this
,因此本文將著重討論 this
,在此過程中分別講解其他知識(shí)點(diǎn)。
注意: 本文屬于基礎(chǔ)篇,如果你已經(jīng)對(duì)本文相關(guān)知識(shí)點(diǎn)已經(jīng)很了解了,那么可以跳過本文。如果你不夠了解,或者了解的還不完整,那么可以通過本文來復(fù)習(xí)一下 ~
1. this 指向的類型
剛開始學(xué)習(xí) JavaScript 的時(shí)候,this
總是最能讓人迷惑,下面我們一起看一下在 JavaScript 中應(yīng)該如何確定 this
的指向。
this
是在函數(shù)被調(diào)用時(shí)確定的,它的指向完全取決于函數(shù)調(diào)用的地方,而不是它被聲明的地方(除箭頭函數(shù)外)。當(dāng)一個(gè)函數(shù)被調(diào)用時(shí),會(huì)創(chuàng)建一個(gè)執(zhí)行上下文,它包含函數(shù)在哪里被調(diào)用(調(diào)用棧)、函數(shù)的調(diào)用方式、傳入的參數(shù)等信息,this
就是這個(gè)記錄的一個(gè)屬性,它會(huì)在函數(shù)執(zhí)行的過程中被用到。
this
在函數(shù)的指向有以下幾種場(chǎng)景:
- 作為構(gòu)造函數(shù)被
new
調(diào)用; - 作為對(duì)象的方法使用;
- 作為函數(shù)直接調(diào)用;
- 被
call
、apply
、bind
調(diào)用; - 箭頭函數(shù)中的
this
;
下面我們分別來討論一下這些場(chǎng)景中 this
的指向。
1.1 new 綁定
函數(shù)如果作為構(gòu)造函數(shù)使用 new
調(diào)用時(shí), this
綁定的是新創(chuàng)建的構(gòu)造函數(shù)的實(shí)例。
function Foo() {
console.log(this)
}
var bar = new Foo() // 輸出: Foo 實(shí)例,this 就是 bar
實(shí)際上使用 new
調(diào)用構(gòu)造函數(shù)時(shí),會(huì)依次執(zhí)行下面的操作:
- 創(chuàng)建一個(gè)新對(duì)象;
- 構(gòu)造函數(shù)的
prototype
被賦值給這個(gè)新對(duì)象的__proto__
; - 將新對(duì)象賦給當(dāng)前的
this
; - 執(zhí)行構(gòu)造函數(shù);
- 如果函數(shù)沒有返回其他對(duì)象,那么
new
表達(dá)式中的函數(shù)調(diào)用會(huì)自動(dòng)返回這個(gè)新對(duì)象,如果返回的不是對(duì)象將被忽略;
1.2 顯式綁定
通過 call
、apply
、bind
我們可以修改函數(shù)綁定的 this
,使其成為我們指定的對(duì)象。通過這些方法的第一個(gè)參數(shù)我們可以顯式地綁定 this
。
function foo(name, price) {
this.name = name
this.price = price
}
function Food(category, name, price) {
foo.call(this, name, price) // call 方式調(diào)用
// foo.apply(this, [name, price]) // apply 方式調(diào)用
this.category = category
}
new Food('食品', '漢堡', '5塊錢')
// 瀏覽器中輸出: {name: "漢堡", price: "5塊錢", category: "食品"}
call
和 apply
的區(qū)別是 call
方法接受的是參數(shù)列表,而 apply
方法接受的是一個(gè)參數(shù)數(shù)組。
func.call(thisArg, arg1, arg2, ...) // call 用法
func.apply(thisArg, [arg1, arg2, ...]) // apply 用法
而 bind
方法是設(shè)置 this
為給定的值,并返回一個(gè)新的函數(shù),且在調(diào)用新函數(shù)時(shí),將給定參數(shù)列表作為原函數(shù)的參數(shù)序列的前若干項(xiàng)。
func.bind(thisArg[, arg1[, arg2[, ...]]]) // bind 用法
舉個(gè)例子:
var food = {
name: '漢堡',
price: '5塊錢',
getPrice: function(place) {
console.log(place + this.price)
}
}
food.getPrice('KFC ') // 瀏覽器中輸出: "KFC 5塊錢"
var getPrice1 = food.getPrice.bind({ name: '雞腿', price: '7塊錢' }, '肯打雞 ')
getPrice1() // 瀏覽器中輸出: "肯打雞 7塊錢"
關(guān)于 bind
的原理,我們可以使用 apply
方法自己實(shí)現(xiàn)一個(gè) bind
看一下:
// ES5 方式
Function.prototype.bind = Function.prototype.bind || function() {
var self = this
var rest1 = Array.prototype.slice.call(arguments)
var context = rest1.shift()
return function() {
var rest2 = Array.prototype.slice.call(arguments)
return self.apply(context, rest1.concat(rest2))
}
}
// ES6 方式
Function.prototype.bind = Function.prototype.bind || function(...rest1) {
const self = this
const context = rest1.shift()
return function(...rest2) {
return self.apply(context, [...rest1, ...rest2])
}
}
ES6 方式用了一些 ES6 的知識(shí)比如 rest
參數(shù)、數(shù)組解構(gòu),感興趣的話可以看看后面的文章 <基礎(chǔ)篇:ES6 中可能遇到的知識(shí)點(diǎn)> 中的詳細(xì)介紹。
注意: 如果你把 null
或 undefined
作為 this
的綁定對(duì)象傳入 call
、apply
、bind
,這些值在調(diào)用時(shí)會(huì)被忽略,實(shí)際應(yīng)用的是默認(rèn)綁定規(guī)則。
var a = 'hello'
function foo() {
console.log(this.a)
}
foo.call(null) // 瀏覽器中輸出: "hello"
1.3 隱式綁定
函數(shù)是否在某個(gè)上下文對(duì)象中調(diào)用,如果是的話 this
綁定的是那個(gè)上下文對(duì)象。
var a = 'hello'
var obj = {
a: 'world',
foo: function() {
console.log(this.a)
}
}
obj.foo() // 瀏覽器中輸出: "world"
上面代碼中,foo
方法是作為對(duì)象的屬性調(diào)用的,那么此時(shí) foo
方法執(zhí)行時(shí),this
指向 obj
對(duì)象。也就是說,此時(shí) this
指向調(diào)用這個(gè)方法的對(duì)象,如果嵌套了多個(gè)對(duì)象,那么指向最后一個(gè)調(diào)用這個(gè)方法的對(duì)象:
var a = 'hello'
var obj = {
a: 'world',
b:{
a:'China',
foo: function() {
console.log(this.a)
}
}
}
obj.b.foo() // 瀏覽器中輸出: "China"
最后一個(gè)對(duì)象是 obj
上的 b
,那么此時(shí) foo
方法執(zhí)行時(shí),其中的 this
指向的就是 b
對(duì)象。
1.4 默認(rèn)綁定
函數(shù)獨(dú)立調(diào)用,直接使用不帶任何修飾的函數(shù)引用進(jìn)行調(diào)用,也是上面幾種綁定途徑之外的方式。非嚴(yán)格模式下 this
綁定到全局對(duì)象(瀏覽器下是 winodw
,node 環(huán)境是 global
),嚴(yán)格模式下 this
綁定到 undefined
(因?yàn)閲?yán)格模式不允許 this
指向全局對(duì)象)。
var a = 'hello'
function foo() {
var a = 'world'
console.log(this.a)
console.log(this)
}
foo() // 相當(dāng)于執(zhí)行 window.foo()
// 瀏覽器中輸出: "hello"
// 瀏覽器中輸出: Window 對(duì)象
上面代碼中,變量 a
被聲明在全局作用域,成為全局對(duì)象 window
的一個(gè)同名屬性。函數(shù) foo
被執(zhí)行時(shí),this
此時(shí)指向的是全局對(duì)象,因此打印出來的 a
是全局對(duì)象的屬性。
注意有一種情況:
var a = 'hello'
var obj = {
a: 'world',
foo: function() {
console.log(this.a)
}
}
var bar = obj.foo
bar() // 瀏覽器中輸出: "hello"
此時(shí) bar
函數(shù),也就是 obj
上的 foo
方法為什么又指向了全局對(duì)象呢,是因?yàn)?bar
方法此時(shí)是作為函數(shù)獨(dú)立調(diào)用的,所以此時(shí)的場(chǎng)景屬于默認(rèn)綁定,而不是隱式綁定。這種情況和把方法作為回調(diào)函數(shù)的場(chǎng)景類似:
var a = 'hello'
var obj = {
a: 'world',
foo: function() {
console.log(this.a)
}
}
function func(fn) {
fn()
}
func(obj.foo) // 瀏覽器中輸出: "hello"
參數(shù)傳遞實(shí)際上也是一種隱式的賦值,只不過這里 obj.foo
方法是被隱式賦值給了函數(shù) func
的形參 fn
,而之前的情景是自己賦值,兩種情景實(shí)際上類似。這種場(chǎng)景我們遇到的比較多的是 setTimeout
和 setInterval
,如果回調(diào)函數(shù)不是箭頭函數(shù),那么其中的 this
指向的就是全局對(duì)象.
其實(shí)我們可以把默認(rèn)綁定當(dāng)作是隱式綁定的特殊情況,比如上面的 bar()
,我們可以當(dāng)作是使用 window.bar()
的方式調(diào)用的,此時(shí) bar
中的 this
根據(jù)隱式綁定的情景指向的就是 window
。
2. this 綁定的優(yōu)先級(jí)
this
存在多個(gè)使用場(chǎng)景,那么多個(gè)場(chǎng)景同時(shí)出現(xiàn)的時(shí)候,this
到底應(yīng)該如何指向呢。這里存在一個(gè)優(yōu)先級(jí)的概念,this
根據(jù)優(yōu)先級(jí)來確定指向。優(yōu)先級(jí):new 綁定 > 顯示綁定 > 隱式綁定 > 默認(rèn)綁定
所以 this
的判斷順序:
- new 綁定: 函數(shù)是否在
new
中調(diào)用?如果是的話this
綁定的是新創(chuàng)建的對(duì)象; - 顯式綁定: 函數(shù)是否是通過
bind
、call
、apply
調(diào)用?如果是的話,this
綁定的是指定的對(duì)象; - 隱式綁定: 函數(shù)是否在某個(gè)上下文對(duì)象中調(diào)用?如果是的話,
this
綁定的是那個(gè)上下文對(duì)象; - 如果都不是的話,使用默認(rèn)綁定。如果在嚴(yán)格模式下,就綁定到
undefined
,否則綁定到全局對(duì)象;
3. 箭頭函數(shù)中的 this
箭頭函數(shù) 是根據(jù)其聲明的地方來決定 this
的,它是 ES6 中出現(xiàn)的知識(shí)點(diǎn),在后文 <基礎(chǔ)篇:ES6 中可能遇到的知識(shí)點(diǎn)> 中會(huì)有更詳細(xì)講解。
箭頭函數(shù)的 this
綁定是無法通過 call
、apply
、bind
被修改的,且因?yàn)榧^函數(shù)沒有構(gòu)造函數(shù) constructor
,所以也不可以使用 new
調(diào)用,即不能作為構(gòu)造函數(shù),否則會(huì)報(bào)錯(cuò)。
var a = 'hello'
var obj = {
a: 'world',
foo: () => {
console.log(this.a)
}
}
obj.foo() // 瀏覽器中輸出: "hello"
我們可以看看 ECMAScript 標(biāo)準(zhǔn)中對(duì)箭頭函數(shù)的描述:
原文: An ArrowFunction does not define local bindings for arguments, super, this, or new.target. Any reference to arguments, super, this, or new.target within an ArrowFunction must resolve to a binding in a lexically enclosing environment. Typically this will be the Function Environment of an immediately enclosing function.
翻譯: 箭頭函數(shù)不為arguments
、super
、this
或new.target
定義本地綁定。箭頭函數(shù)中對(duì)arguments
、super
、this
或new.target
的任何引用都解析為當(dāng)前所在詞法作用域中的綁定。通常,這是箭頭函數(shù)所在的函數(shù)作用域。— ECMAScript Language Specification - Arrow Function | ECMA 標(biāo)準(zhǔn) - 箭頭函數(shù)
4. 一個(gè) this 的小練習(xí)
用一個(gè)小練習(xí)來實(shí)戰(zhàn)一下:
var a = 20
var obj = {
a: 40,
foo:() => {
console.log(this.a)
function func() {
this.a = 60
console.log(this.a)
}
func.prototype.a = 50
return func
}
}
var bar = obj.foo() // 瀏覽器中輸出: 20
bar() // 瀏覽器中輸出: 60
new bar() // 瀏覽器中輸出: 60
稍微解釋一下:
var a = 20
這句在全局變量 window 上創(chuàng)建了個(gè)屬性a
并賦值為 20;- 首先執(zhí)行的是
obj.foo()
,這是一個(gè)箭頭函數(shù),箭頭函數(shù)不創(chuàng)建新的函數(shù)作用域直接沿用語句外部的作用域,因此obj.foo()
執(zhí)行時(shí)箭頭函數(shù)中this
是全局 window,首先打印出 window 上的屬性a
的值 20,箭頭函數(shù)返回了一個(gè)原型上有個(gè)值為 50 的屬性a
的函數(shù)對(duì)象func
給bar
; - 繼續(xù)執(zhí)行的是
bar()
,這里執(zhí)行的是剛剛箭頭函數(shù)返回的閉包func
,其內(nèi)部的this
指向 window,因此this.a
修改了window.a
的值為 60 并打印出來; - 然后執(zhí)行的是
new bar()
,根據(jù)之前的表述,new 操作符會(huì)在func
函數(shù)中創(chuàng)建一個(gè)繼承了func
原型的實(shí)例對(duì)象并用this
指向它,隨后this.a = 60
又在實(shí)例對(duì)象上創(chuàng)建了一個(gè)屬性a
,在之后的打印中已經(jīng)在實(shí)例上找到了屬性a
,因此就不繼續(xù)往對(duì)象原型上查找了,所以打印出第三個(gè) 60;
如果把上面例子的箭頭函數(shù)換成普通函數(shù)呢,結(jié)果會(huì)是什么樣?
var a = 20
var obj = {
a: 40,
foo: function() {
console.log(this.a)
function func() {
this.a = 60
console.log(this.a)
}
func.prototype.a = 50
return func
}
}
var bar = obj.foo() // 瀏覽器中輸出: 40
bar() // 瀏覽器中輸出: 60
new bar() // 瀏覽器中輸出: 60
這個(gè)例子就不詳細(xì)講解了。
如果把上面兩個(gè)例子弄懂原理,基本上 this
的指向就掌握的差不多啦~
推介閱讀: