一、this的綁定規(guī)則
針對(duì)于函數(shù)來(lái)來(lái)說(shuō),this的綁定和函數(shù)聲明的位置沒(méi)有任何關(guān)系,只取決于函數(shù)的調(diào)用方式,它指向什么完全取決于函數(shù)在哪里被調(diào)用。
當(dāng)一個(gè)函數(shù)被調(diào)用時(shí),會(huì)創(chuàng)建一個(gè)活動(dòng)記錄(有時(shí)候也稱為執(zhí)行上下文)。這個(gè)記錄會(huì)包含函數(shù)在哪里被調(diào)用(調(diào)用棧)、函數(shù)的調(diào)用方式、傳入的參數(shù)等信息。this就是這個(gè)記錄的一個(gè)屬性,會(huì)在函數(shù)執(zhí)行的過(guò)程中用到。
我們先來(lái)看下this的四條綁定規(guī)則,然后解釋多條規(guī)則都可用時(shí)它們的優(yōu)先級(jí)如何排列。
1.默認(rèn)綁定
即獨(dú)立函數(shù)調(diào)用。
function foo(){
console.log(this.a)
}
var a = 2;
foo() // 2
在上述代碼中,foo()是直接使用不帶任何修飾的函數(shù)引用進(jìn)行調(diào)用的,即默認(rèn)綁定到window,所以this.a 等于2.
如果foo內(nèi)部使用嚴(yán)格模式,則不能將全局對(duì)象用于默認(rèn)綁定,this會(huì)綁定到undefind。但是foo運(yùn)行在嚴(yán)格模式下則不影響默認(rèn)綁定。
2.隱式綁定
隱式綁定需要看函數(shù)調(diào)用位置是否有上下文對(duì)象。
function foo(){
console.log(this.a)
}
var obj = {
a:2,
foo:foo
}
obj.foo() // 2
上面的代碼片段中,函數(shù)的調(diào)用位置會(huì)使用obj上下文來(lái)引用函數(shù),也就是說(shuō),當(dāng)foo被調(diào)用時(shí),它的前面加上了對(duì)obj的一弄,當(dāng)函數(shù)引用有上下文對(duì)象時(shí),隱式綁定規(guī)則會(huì)把函數(shù)調(diào)用中的this綁定到這個(gè)上下文對(duì)象。所以this.a 就是obj.a.
但是要注意不要掉入一些陷阱里面。
function foo(){
console.log(this.a)
}
var obj = {
a:2,
foo:foo
}
var bar = obj.foo()
var a = 'oops,global' // a是全局對(duì)象的屬性
bar() // oops,global
雖然bar是obj.foo的一個(gè)引用,但是實(shí)際上,它引用的是foo函數(shù)本身,因此此時(shí)的bar()其實(shí)是一個(gè)不帶任何修飾的函數(shù)調(diào)用,因此應(yīng)用了默認(rèn)綁定。
function foo(){
console.log(this.a)
}
function doFoo(fn){
// fn其實(shí)引用的是foo
fn() // 調(diào)用位置
}
var obj = {
a:2,
foo:foo
}
var a = 'oops,global' // a是全局對(duì)象的屬性
doFoo(obj.foo() )
參數(shù)的傳遞其實(shí)就是一種隱性賦值,所以這個(gè)例子跟上面的一樣。
3.顯示綁定
JavaScript提供的絕大多數(shù)函數(shù)以及你自己創(chuàng)建的所有函數(shù)都可以使用call(..)和apply(..)方法。
它們的第一個(gè)參數(shù)是一個(gè)對(duì)象,是給this準(zhǔn)備的,接著在調(diào)用函數(shù)時(shí)將其綁定到this。兩者的區(qū)別在于給函數(shù)傳遞參數(shù)的方式上。
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上。
通過(guò)call和apply,我們也可以解決隱式綁定中將函數(shù)傳入回調(diào)函數(shù)中,this綁定丟失的問(wèn)題
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
這種綁定的常見(jiàn)應(yīng)用場(chǎng)景,創(chuàng)建一個(gè)可以重復(fù)使用的輔助函數(shù)
function foo(somethisthing){
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(5)
4.new綁定
JavaScript中的new用來(lái)調(diào)用構(gòu)造函數(shù)。使用new來(lái)調(diào)用函數(shù),或者說(shuō)發(fā)生構(gòu)造函數(shù)調(diào)用時(shí),會(huì)自動(dòng)執(zhí)行下面的操作
1.創(chuàng)建(或者說(shuō)構(gòu)造)一個(gè)全新的對(duì)象。
2.這個(gè)新對(duì)象會(huì)被執(zhí)行[[Prototype]]連接。
3.這個(gè)新對(duì)象會(huì)綁定到函數(shù)調(diào)用的this。
4.如果函數(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上。
二、優(yōu)先級(jí)
具體優(yōu)先級(jí)的測(cè)試案例就不多說(shuō)了,推薦大家看原版書中寫的很詳細(xì)。這里直接引用原文的結(jié)論。
判斷this,按照如下順序進(jìn)行判斷
1、函數(shù)是否在new中調(diào)用(new綁定)? 如果是的話this綁定的是最新創(chuàng)建的對(duì)象
var bar = new foo()
2、函數(shù)是否通過(guò)call,apply(顯示綁定),如果是的話,this綁定的是指定的對(duì)象
var bar = foo.call(obj2)
3、函數(shù)是否在某個(gè)上下文對(duì)象中調(diào)用(隱式綁定)?如果是的話,this綁定的是哪個(gè)上下文對(duì)象
var bar = obj1.foo()
4、如果都不是的話,使用默認(rèn)綁定,如果在嚴(yán)格模式下,就綁定undefind,否則綁定到全局對(duì)象
var bar = foo()
三、那些例外
凡事總有例外。
1、如果把null或者undefind作為this的綁定對(duì)象傳入call,apply或者bind,這些值在調(diào)用時(shí)會(huì)被忽略,應(yīng)用默認(rèn)綁定規(guī)則。
function foo(){
console.log(this.a)
}
var a = 2;
foo.call(null)
2、創(chuàng)建了一個(gè)函數(shù)的‘間接引用’,從而導(dǎo)致應(yīng)用了默認(rèn)規(guī)則
function foo(){
console.log(this.a)
}
var a = 2;
var o = { a:3, foo:foo};
o.foo() // 3
(p.foo = o.foo)() // 2
賦值表達(dá)式p.foo = o.foo的返回值是目標(biāo)函數(shù)的引用,因此調(diào)用位置是foo()而不是p.foo()或者o.foo()。根據(jù)我們之前說(shuō)過(guò)的,這里會(huì)應(yīng)用默認(rèn)綁定。
3、軟綁定
給默認(rèn)綁定指定一個(gè)全局對(duì)象和undefind以外的值,實(shí)現(xiàn)和硬綁定相同的效果,同時(shí)保留隱式綁定或者顯示綁定修改this的能力。
if(!Function.prototype.softBind){
Function.prototype.softBind = function(obj){
var fn = this;
// 捕獲所有curried參數(shù)
var curried = [].slice.call(arguments,1)
var bound = function(){
return fn.apply( ( !this || this === (window || global)) ? obj : this , curried.concat.apply(curried,arguments)
);
}
bound.prototype = Object.create(fn.prototype)
return bound;
}
}
softBind(..)的其他原理和ES5內(nèi)置的bind(..)類似。它會(huì)對(duì)指定的函數(shù)進(jìn)行封裝,首先檢查調(diào)用時(shí)的this,如果this綁定到全局對(duì)象或者undefined,那就把指定的默認(rèn)對(duì)象obj綁定到this,否則不會(huì)修改this。
function foo(){
console.log('name:'+this.name)
}
var obj = { name: 'obj'},obj2 = { name : 'obj2'}, obj3 = {name:'obj3'}
var fooOBJ = foo.softBind(obj)
fooOBJ() // name:obj
obj2.foo = foo.softBind(obj)
obj2.foo() // name:obj2
fooOBJ.call(obj3); // name:obj3
setTimeout(obj2.foo,10)
// name:obj 應(yīng)用了軟綁定
可以看到,軟綁定版本的foo()可以手動(dòng)將this綁定到obj2或者obj3上,但如果應(yīng)用默認(rèn)綁定,則會(huì)將this綁定到obj。