JavaScript 中的作用域是
詞法作用域
。而JavaScript中的 this 卻更加類(lèi)似于
動(dòng)態(tài)作用域
的機(jī)制暴浦。
this是在調(diào)用時(shí)被綁定的,完全取決于函數(shù)的調(diào)用位置。
一贩猎、調(diào)用位置
嗲偶偶那個(gè)位置就是函數(shù)在代碼中被調(diào)用的位置(而不是聲明的位置)
尋找調(diào)用位置,就是要分析調(diào)用棧(即為了到達(dá)當(dāng)前執(zhí)行位置所調(diào)用的所有函數(shù))萍膛。
function baz() {
// 當(dāng)前調(diào)用棧是:baz
// 因此吭服,當(dāng)前調(diào)用位置是全局作用域
console.log('baz')
bar() // bar 的調(diào)用位置
}
function bar() {
// 當(dāng)前調(diào)用棧是 baz ---> bar
// 因此,當(dāng)前調(diào)用位置在 baz 中
console.log('bar')
foo() // foo 的調(diào)用位置
}
function foo() {
// 當(dāng)前調(diào)用棧是 baz -> bar -> foo
// 因此蝗罗,當(dāng)前調(diào)用位置在 bar 中
console.log('foo')
}
baz() // baz 的調(diào)用位置
二艇棕、綁定規(guī)則
找到調(diào)用位置后,需要分段綁定規(guī)則串塑。綁定規(guī)則有四條
2.1沼琉、默認(rèn)綁定
function foo() {
console.log(this.a)
}
var a = 2
foo() // 2
當(dāng)調(diào)用 foo() 是,this.a 被解析成立全局變量a拟赊。因?yàn)檫@里應(yīng)用了 this 的默認(rèn)綁定刺桃。this 指向了全局對(duì)象。而在全局對(duì)象中聲明的變量吸祟,就是全局對(duì)象的一個(gè)同名屬性瑟慈。
如果使用 嚴(yán)格模式(strict mode),則不能將全局對(duì)象用于弄人綁定屋匕,因此 this 會(huì)綁定到 undefined
function foo() {
"use strict"
console.log(this.a)
}
var a = 2
foo() // TypeError:Cannot read property 'a' of undefined
這里有一個(gè)微妙但是非常重要的細(xì)節(jié)葛碧,雖然 this 的綁定規(guī)則完全取決于調(diào)用位置,但是只有 foo() 運(yùn)行在非 strict mode 下時(shí)过吻,默認(rèn)綁定才能綁定到全局對(duì)象进泼;在嚴(yán)格模式下調(diào)用foo() 則不影響默認(rèn)綁定:
function foo() {
console.log(this.a)
}
var a = 2
(function() {
"use strict"
foo() // 2
})()
2.2蔗衡、隱式綁定
另一條需要考慮的規(guī)則是調(diào)用位置是否有上下文對(duì)象,或者說(shuō)是否被某個(gè)對(duì)象擁有或者包含乳绕。
function foo() {
console.log(this.a)
}
bar obj = {
a: 2,
foo: foo
}
obj.foo() // 2
需要注意的是绞惦,無(wú)論是直接在 obj 中定義還是先定義再添加為引用屬性,這個(gè)函數(shù)嚴(yán)格來(lái)說(shuō)都不屬于 obj 對(duì)象洋措。然而調(diào)用位置會(huì)使 obj 上下文來(lái)引用函數(shù)济蝉,可以使函數(shù)被調(diào)用時(shí) obj 對(duì)象 “擁有” 或者 “包含” 函數(shù)引用。
當(dāng) foo() 被調(diào)用時(shí)菠发,它的前面確實(shí)加上了對(duì) obj 的引用王滤。當(dāng)函數(shù)引用有上下文對(duì)象時(shí),隱式綁定規(guī)則會(huì)把函數(shù)調(diào)用中的this綁定到這個(gè)上下文對(duì)象滓鸠。
對(duì)象屬性引用鏈中只有上一層或者說(shuō)最后一層在調(diào)用位置中起作用雁乡。
function foo() {
console.log(this.a)
}
var obj2 = {
a: 42,
foo: foo
}
var obj1 = {
a: 2,
obj2: obj2
}
obj1.obj2.foo() // 42
隱式丟失
隱式綁定的函數(shù)丟失綁定對(duì)象,也就是說(shuō)它會(huì)應(yīng)用默認(rèn)綁定糜俗,從而把this綁定到全局對(duì)象或者undefined 上踱稍,取決于是否是嚴(yán)格模式。
function foo() {
console.log(this.a)
}
var obj = {
a: 2,
foo: foo
}
var bar = obj.foo // 函數(shù)別名
var a = "global"
bar() // global
雖然 bar 是 obj.foo 的一個(gè)引用吩跋,但實(shí)際上寞射,它引用的是 foo 函數(shù)本身,因此此時(shí)的 bar 其實(shí)是一個(gè)不帶任何修飾的函數(shù)調(diào)用锌钮,因此應(yīng)用了默認(rèn)綁定
桥温。
2.3、顯式綁定
JavaScript 中 “所有” 函數(shù)都有一些有用的特性梁丘,這和他們的 [[prototype]] 有關(guān)侵浸。具體點(diǎn)說(shuō),可以使用函數(shù)的 call(...) 和 apply(...)方法氛谜。這兩個(gè)方法第一個(gè)參數(shù)是一個(gè)對(duì)象掏觉,是給 this 準(zhǔn)備的,接著再調(diào)用函數(shù)時(shí)將其綁定到 this值漫。因?yàn)槟憧梢灾苯又付?this 的綁定對(duì)象澳腹,因此我們稱(chēng)之為顯式綁定。
function foo() {
console.log(this.a)
}
var obj = {
a: 2
}
foo.call( obj ) // 2
通過(guò) foo.call(...) 杨何,可以在調(diào)用foo時(shí)強(qiáng)制把它的this綁定到 obj 上
如果傳入了一個(gè)原始值來(lái)當(dāng)做 this 的綁定對(duì)象酱塔,這個(gè)原始值會(huì)被轉(zhuǎn)換成它的對(duì)象形式(也就是 new String(...)、new Number(...))危虱。這通常被稱(chēng)為“裝箱”
硬綁定
function foo() {
console.log( this.a )
}
var obj = {
a: 2
}
var bar = function() {
foo.call( obj )
}
bar() //2
setTimeout(bar, 100) // 2
// 硬綁定的bar 不可能在修改它的 this
bar.call( window ) // 2
我們創(chuàng)建了函數(shù) bar()羊娃,并在它的內(nèi)部調(diào)用了 foo.call(obj),因此強(qiáng)制把 foo 的 this 綁定到了 obj埃跷。無(wú)論之后如何調(diào)用函數(shù) bar蕊玷,他總會(huì)手動(dòng)在 obj 上調(diào)用 foo邮利。這種綁定是一種顯式的強(qiáng)制綁定,一般稱(chēng)之為硬綁定
硬綁定的典型應(yīng)用場(chǎng)景就是創(chuàng)建一個(gè)包裹的函數(shù)垃帅,負(fù)責(zé)接收參數(shù)并返回值:
function foo(something) {
console.log(this.a, something)
return this.a + something
}
var obj = {
a: 2
}
var bar = function() {
return foo.apply( obj, arguments )
}
var b = bar(3) //2 3
console.log(b) // 5
另一種使用法法是創(chuàng)建一個(gè)可以重復(fù)使用的輔助函數(shù):
function foo(something) {
console.log( this.a, something)
return this.a + something
}
// 簡(jiǎn)單的輔助綁定函數(shù)
function bind(fn, obj) {
return function() {
return fn.apply(obj, arguments)
}
}
var obj = {
a: 2
}
var bar = bind( foo, obj )
var b = bar(3) // 2 3
console.log(b) // 5
由于硬綁定是一種非常常用的模式延届,所以ES5提供了內(nèi)置的方法 Function.prototype.bind,bind() 會(huì)返回一個(gè)硬編碼的新函數(shù)挺智,它會(huì)把你指定的參數(shù)設(shè)置為 this 的上下文并調(diào)用原始函數(shù)祷愉。
API 調(diào)用的 “上下文”
第三方庫(kù)的許多函數(shù),以及 JavaScript 語(yǔ)言和宿主環(huán)境中許多新的內(nèi)置函數(shù)赦颇,都提供了一個(gè)可選的參數(shù),通常被稱(chēng)為“上下文”(context)赴涵,其作用和bind(...)一樣媒怯,確保你的回調(diào)函數(shù)使用指定的this
function foo(el) {
console.log( el, this.id )
}
var obj = {
id: 'awesome'
};
// 調(diào)用 foo(...) 時(shí)把this 綁定到 obj
[1, 2, 3].forEach( foo, obj )
// 1 awesome 2 awesome 3 awesome
這些函數(shù)實(shí)際上就是通過(guò) call(...) 或者 apply(...) 實(shí)現(xiàn)了顯式綁定,這樣可以少寫(xiě)一些代碼
2.4髓窜、new 綁定
最后一條 this 的綁定規(guī)則扇苞,再次之前需要知道,在傳統(tǒng)的面向類(lèi)的語(yǔ)言中寄纵,“構(gòu)造函數(shù)” 是類(lèi)中的一些特殊方法鳖敷,使用 new 初始化類(lèi)時(shí)會(huì)調(diào)用類(lèi)中的構(gòu)造函數(shù)。通常的形式是這樣的程拭;
something = new MyClass
JavaScript 中 new 的機(jī)制實(shí)際上和面向類(lèi)的語(yǔ)言完全不同定踱,在 JavaScript 中,構(gòu)造函數(shù)只是一些使用 new 操作符時(shí)被調(diào)用的函數(shù)恃鞋。它們并不屬于某個(gè)類(lèi)崖媚,也不會(huì)實(shí)例化一個(gè)類(lèi)。實(shí)際上它們甚至都不能說(shuō)是一種特殊的函數(shù)類(lèi)型恤浪,它們只是被 new 操作符調(diào)用的普通函數(shù)而已畅哑。所有函數(shù)都可以用 new 來(lái)調(diào)用,這種函數(shù)調(diào)用被稱(chēng)為構(gòu)造函數(shù)調(diào)用水由,這里有一個(gè)重要但是非常細(xì)微的區(qū)別:實(shí)際上并不存在所謂的“構(gòu)造函數(shù)”荠呐,只是對(duì)于函數(shù)的“構(gòu)造調(diào)用”
使用 new 來(lái)調(diào)用函數(shù),或者說(shuō)發(fā)生構(gòu)造函數(shù)調(diào)用時(shí)砂客,會(huì)自動(dòng)執(zhí)行下面的操作泥张。
- 創(chuàng)建(或者說(shuō)構(gòu)造)一個(gè)全新的對(duì)象
- 這個(gè)新對(duì)象會(huì)被執(zhí)行[[Prototype]] 連接
- 這個(gè)新對(duì)象會(huì)綁定到函數(shù)調(diào)用的this
- 如果函數(shù)沒(méi)有返回其他對(duì)象,那么new表達(dá)式中的函數(shù)調(diào)用會(huì)自動(dòng)返回這個(gè)新對(duì)象鞭盟。
function foo(a) {
this.a = a
}
var bar = new foo(2)
console.log(bar.a) // 2
使用 new 來(lái)調(diào)用 foo(...) 時(shí)圾结,會(huì)構(gòu)造一個(gè)新對(duì)象,把它綁定到 foo(...) 調(diào)用的this 上齿诉。new 是最后一種可以影響函數(shù)調(diào)用時(shí) this 綁定行為的方法筝野,我們稱(chēng)之為 new 綁定晌姚。
三、優(yōu)先級(jí)
現(xiàn)在已經(jīng)知道了 this綁定的 四條規(guī)則歇竟。但是挥唠,如果某個(gè)調(diào)用位置可以應(yīng)用多條規(guī)則怎么辦?那么就必須給這些規(guī)則設(shè)定優(yōu)先級(jí)焕议。
首先宝磨,毫無(wú)疑問(wèn)。默認(rèn)綁定 是四條規(guī)則中優(yōu)先級(jí)最低的盅安。
顯式綁定和隱式綁定哪個(gè)優(yōu)先級(jí)更高勒唤锉?
function foo() {
console.log(this.a)
}
var obj1 = {
a: 2,
foo: foo
}
var obj2 = {
a: 3,
foo: foo
}
obj1.foo() // 2
obj2.foo() // 3
obj1.foo.call(obj2) // 3
obj2.foo.call(obj1) // 2
可以看到,顯式綁定的優(yōu)先級(jí)高于隱式綁定
new 綁定 和 隱式綁定的優(yōu)先級(jí)
function foo(something) {
this.a = something
}
var obj1 = {
foo: foo
}
var obj2 = {}
obj1.foo(2)
console.log( obj1.a ) // 2
obj1.foo.call( obj2, 3 )
console.log( obj2.a ) // 3
var bar = new obj1.foo( 4 )
console.log( obj1.a ) // 2
console.log( bar.a ) // 4
可以看到别瞭, new 綁定 比 隱式綁定 優(yōu)先級(jí)高
new 綁定和 顯式綁定 比較:
function foo(something) {
this.a = something
}
var obj1 = {}
var bar = foo.bind( obj1 )
bar( 2 )
console.log( obj1.a ) // 2
var baz = new bar( 3 )
console.log( obj1.a ) // 2
console.log( baz.a ) // 3
new 修改了硬綁定(到 obj1的 ) 調(diào)用 bar(...) 中的 this窿祥。因?yàn)槭褂昧?new 綁定,得到了一個(gè)名字為 baz 的新對(duì)象蝙寨,并且 baz.a 的值是3.
實(shí)際上晒衩,ES5 中內(nèi)置的 Function.prototype.bind(...) 是很復(fù)雜的。bind() 會(huì)返回硬綁定函數(shù)是否被new 調(diào)用墙歪,如果是的話(huà)就會(huì)使用新創(chuàng)建的 this 來(lái)替換硬綁定的 this听系。
在 new 中使用硬綁定函數(shù),主要目的是預(yù)先設(shè)置函數(shù)的一些參數(shù)虹菲,這樣在使用 new 進(jìn)行初始化時(shí)就可以只傳入其余的參數(shù)靠胜。bind(...) 的功能之一就是可以把除了第一個(gè)參數(shù)(第一個(gè)參數(shù) 用于綁定 this)之外的其他參數(shù)都傳給下層的函數(shù)( 這種技術(shù)稱(chēng)為 “部分應(yīng)用”, 是 “柯里化” 的一種)届惋。
function foo(p1, p2) {
this.val = p1 + p2
}
// 之所以使用 null 是因?yàn)樵诒纠形覀儾⒉魂P(guān)心硬綁定的this是什么
// 反之使用 new 是 this會(huì)被修改
var bar = foo.bind(null, 'p1')
var baz = new bar('p2')
baz.val // p1, p2
被忽略的this
如果把 null 或者 undefined 作為 this 的綁定對(duì)象傳入 call髓帽、apply 或者 bind,這些值在調(diào)用時(shí)會(huì)被忽略脑豹,實(shí)際應(yīng)用的默認(rèn)綁定規(guī)則郑藏。創(chuàng)建的作用是使用 apply() 來(lái)展開(kāi)一個(gè)數(shù)組,bind() 可以對(duì)參數(shù)進(jìn)行柯里化(預(yù)先設(shè)置一些參數(shù))
function foo(a, b) {
console.log(`a: ${a}, b: $瘩欺`)
}
// 把數(shù)組 展開(kāi)成參數(shù)
foo.apply(null, [2, 3]) // a: 2, b: 3
// 使用bind() 進(jìn)行柯里化
var bar = foo.bind( null, 2 )
bar(3) // a: 2, b: 3
更安全的this
如果函數(shù)并不關(guān)心this 的話(huà)必盖,你可任然需要傳入一個(gè)占位值,這時(shí)null可以能是一個(gè)不錯(cuò)的選擇俱饿。一種“更安全” 的做法是傳入一個(gè)特殊對(duì)象歌粥,把 this 綁定到這個(gè)對(duì)象不會(huì)對(duì)你的程序產(chǎn)生任何副作用。我們可以創(chuàng)建一個(gè) “DMZ”(demilitarized zone)對(duì)象 —— 他就是一個(gè)空的非委托的對(duì)象拍埠。
在 JavaScript 中創(chuàng)建一個(gè)空對(duì)象最簡(jiǎn)單的方法都是 Object.create(null)失驶。 Object.create(null) 和 {} 很像,但是比奴會(huì)創(chuàng)建 Object.prototype 這個(gè)委托枣购,所以它比 {} “更空”
function foo(a, b) {
console.log(`a: ${a}, b: $嬉探`)
}
// DMZ 空對(duì)象
var DMZ = Object.create(null)
// 把數(shù)組展開(kāi)成參數(shù)
foo.apply(DMZ, [2, 3])
// bind() 柯里化
var bar = foo.bind(DMZ, 2)
bar(3)
現(xiàn)在我們可以根據(jù)優(yōu)先級(jí)來(lái)判斷函數(shù)在某個(gè)調(diào)用位置應(yīng)用的是哪條規(guī)則擦耀。
- 函數(shù)是否在 new 中調(diào)用(new綁定)?如果是的話(huà) this 綁定的是新創(chuàng)建的對(duì)象涩堤。
var bar = new foo() - 函數(shù)是否通過(guò) call眷蜓、apply(顯示綁定)或者硬綁定調(diào)用?如果是的話(huà)胎围,this綁定的是指定的對(duì)象
var bar = foo.calal(obj2) - 函數(shù)是否在某個(gè)上下文對(duì)象中調(diào)用(隱式綁定)吁系?如果是的話(huà),this 綁定的是那個(gè)上下文對(duì)象
var bar = obj1.foo() - 如果都不是的話(huà)白魂,使用(默認(rèn)綁定)汽纤。如果在嚴(yán)格模式下,就綁定到 undefined碧聪,否則綁定到全局對(duì)象冒版。
var bar = foo()