函數(shù)中this的指向
先測一下你是否真的明白了this的指向
foo.count=0
function foo(num){
this.count++
}
for(let i=0;i<10;i++){
if(i>5){
foo(i)
}
}
console.log(foo.count) //0
你可能一臉茫然道:這不是應(yīng)該輸出4嗎??咋輸出0捏。(尼克楊臉)
foo是執(zhí)行了4次沒錯。但this在任何情況下都不指向函數(shù)的詞法作用域??!foo中的this指向全局,這是因為this是在運行時綁定的,并不是在編寫時綁定的。它的上下文取決于函數(shù)調(diào)用時的各種條件。this的綁定和函數(shù)聲明的位置沒有關(guān)系,它只取決于函數(shù)的調(diào)用方式。當(dāng)一個函數(shù)被調(diào)用時,會創(chuàng)建一個執(zhí)行上下文,它包含函數(shù)在哪里被調(diào)用、函數(shù)的調(diào)用方式、傳入的參數(shù)等信息,this就是這個記錄的一個屬性,在函數(shù)執(zhí)行的過程中會用到。
那我們回到上面的程序,要想讓控制臺正確打印foo被調(diào)用的次數(shù),只要把this.count++改為foo.count++就好啦~
總的來說呢,就是函數(shù)中的this指向什么,取決于這個函數(shù)在哪里被調(diào)用。
this的綁定規(guī)則
1.默認(rèn)綁定
這是最常用的函數(shù)調(diào)用類型:獨立函數(shù)調(diào)用,可以把這條規(guī)則看作是無法應(yīng)用其他規(guī)則時的默認(rèn)規(guī)則。思考一下下面的代碼:
function foo(){
console.log(this.a)
}
var a=1
foo() // 1
因為在全局環(huán)境中調(diào)用了foo,this.a被解析為全局對象a。這時沒有使用任何修飾的函數(shù)進行調(diào)用,所以實現(xiàn)了this的默認(rèn)綁定,指向全局對象。
2.隱式綁定
調(diào)用位置是否有上下文,換句話說是否被某個對象擁有或包含。思考下面的代碼:
var a=1
function foo(){
console.log(this.a)
}
const obj={a:2,foo}
obj.foo() // 2
當(dāng)函數(shù)引用由上下文對象時,隱式綁定規(guī)則會把函數(shù)調(diào)用中的this綁定到這個上下文對象。對象屬性的引用鏈只有最后一層在調(diào)用位置中起作用。如:
function foo(){
console.log(this.a)
}
const obj1={a:1,foo}
const obj2={a:2,obj1}
obj2.obj1.foo() //1
隱式丟失
還是看上面那段代碼,如果后面加一句:
const bar=obj1.foo
bar() //undefined
這就是隱式丟失,const bar=obj1.foo這句其實是一個引用,它引用的是foo函數(shù)本身。此時的bar其實是一個不帶任何修飾的函數(shù)調(diào)用,因此進行了默認(rèn)綁定,this指向全局。所以為undefined。
更常見的一種狀況發(fā)生在傳入回調(diào)函數(shù)時:
function doFoo(fn){
fn()
}
const obj={a:1,foo}
doFoo(obj.foo) //undefined
參數(shù)傳遞也是一種隱式賦值,我們傳入函數(shù)時fn也是引用的foo函數(shù)本身,原理同上個例子。
3.顯式綁定
顯示綁定一般是通過call和apply來實現(xiàn)。我們還是用foo函數(shù)來說明。
foo.call(obj1)
foo() //1
1.硬綁定
const bar=function (){
foo.call(obj1)
}
bar() //1
這里強制把foo的this綁定到obj上,無論之后如何調(diào)用bar,它總會手動在obj上調(diào)用foo。這種綁定是一種顯式的強制綁定,稱之為硬綁定。
硬綁定是一種非常常用的模式,es5提供了內(nèi)置方法Function.prototype.bind,它的用法如下:
function foo(something){
return this.a+something
}
const obj={a:1}
const bar=foo.bind(obj)
const b=bar(3)
console.log(b) // 4
2.API調(diào)用的上下文
第三方庫的許多函數(shù),以及js語言和宿主環(huán)境中許多新的內(nèi)置函數(shù),都提供了可選的參數(shù),通常被稱為上下文,其作用和bind一樣。
舉個例子:
function foo(el){
console.log(el,this.id)
}
const obj={id:'awesome'}
// 調(diào)用foo時把this綁定到obj
[1,2,3].forEach(foo,obj)
// 1 awesome 2 awesome 3 awesome
4.new綁定
內(nèi)置對象函數(shù)在內(nèi)的所有函數(shù)都可以用new來調(diào)用,這種函數(shù)調(diào)用被稱為構(gòu)造函數(shù)調(diào)用。使用new來調(diào)用函數(shù),會自動執(zhí)行下面的操作:
- 創(chuàng)建一個全新的對象
- 這個新對象會被執(zhí)行prototype連接
- 這個新對象會綁定到函數(shù)調(diào)用的this
- 如果函數(shù)沒有返回其他對象,那么new表達(dá)式中的函數(shù)調(diào)用會自動返回這個新對象。
思考下面的代碼:
function foo(a){
this.a=a
}
const bar=new foo(2)
console.log(bar.a) // 2
使用new來調(diào)用foo時,我們會構(gòu)造一個新對象并把它綁定到foo調(diào)用中的this上。
優(yōu)先級
new綁定 > 顯式綁定 > 隱式綁定 > 默認(rèn)綁定
在new中使用硬綁定,主要是預(yù)先設(shè)置函數(shù)的一些參數(shù),這樣在new初始化時,可以傳入其余的參數(shù)。bind的功能之一就是把除了第一個參數(shù)(用于綁定this)之外的其他參數(shù)都傳給下層的參數(shù),這種技術(shù)稱為柯里化。舉個例子:
function foo(p1,p2){
this.val=p1+p2
}
const bar=foo.bind(null,"hello")
const baz=new bar("world")
console.log(baz.val) // helloworld
綁定例外
1. 當(dāng)我們把null、undefined作為this傳入call、apply或bind,這些值在調(diào)用時會被忽略,實際應(yīng)用的是默認(rèn)綁定規(guī)則。
2. 間接引用的情況下會應(yīng)用默認(rèn)綁定規(guī)則。
舉個例子:
function foo(){
console.log(this.a)
}
var a=0
var obj1={a:1,foo}
var obj2={a:2}
obj1.foo() //1
(obj2.foo=obj1.foo)() // 0
表達(dá)式obj2.foo=obj1.foo的返回值為目標(biāo)函數(shù)的引用,這里會應(yīng)用默認(rèn)綁定。
3. 箭頭函數(shù)
前面介紹的四種規(guī)則適用于除了箭頭函數(shù)之外,所有的普通函數(shù)。我們先看看箭頭函數(shù)的詞法作用域:
function foo(){
// this繼承自foo
return a=>{console.log(this.a)}
}
var obj1={a:1}
var obj2={a:2}
const bar=foo.call(obj1)
bar.call(obj2) // 是1,不是2
因為箭頭函數(shù)會補貨調(diào)用時foo()的this。由于foo()的this綁定到obj1,bar的this也會綁定到obj1,箭頭函數(shù)的綁定無法被修改。