JS this機(jī)制

目錄

this 是什么

this 的四種綁定規(guī)則

綁定規(guī)則的優(yōu)先級(jí)

綁定例外

擴(kuò)展:箭頭函數(shù)

this 是什么

理解this之前, 先糾正一個(gè)觀點(diǎn),this 既不指向函數(shù)自身,也不指函數(shù)的詞法作用域。如果僅通過(guò)this的英文解釋,太容易產(chǎn)生誤導(dǎo)了。它實(shí)際是在函數(shù)被調(diào)用時(shí)才發(fā)生的綁定,也就是說(shuō)this具體指向什么,取決于你是怎么調(diào)用的函數(shù)。

this 的四種綁定規(guī)則

this的4種綁定規(guī)則分別是:默認(rèn)綁定、隱式綁定、顯示綁定、new 綁定。優(yōu)先級(jí)從低到高。

默認(rèn)綁定

什么叫默認(rèn)綁定,即沒(méi)有其他綁定規(guī)則存在時(shí)的默認(rèn)規(guī)則。這也是函數(shù)調(diào)用中最常用的規(guī)則。

來(lái)看這段代碼:

function foo() { 
}       console.log( this.a );

var a = 2; 
foo(); //打印的是什么?

foo() 打印的結(jié)果是2。

因?yàn)閒oo()是直接調(diào)用的(獨(dú)立函數(shù)調(diào)用),沒(méi)有應(yīng)用其他的綁定規(guī)則,這里進(jìn)行了默認(rèn)綁定,將全局對(duì)象綁定this上,所以this.a 就解析成了全局變量中的a,即2。

注意:在嚴(yán)格模式下(strict mode),全局對(duì)象將無(wú)法使用默認(rèn)綁定,即執(zhí)行會(huì)報(bào)undefined的錯(cuò)誤

function foo() { 
    "use strict";
   console.log( this.a );
}

var a = 2; 
foo(); // Uncaught TypeError: Cannot read property 'a' of undefined

隱式綁定

除了直接對(duì)函數(shù)進(jìn)行調(diào)用外,有些情況是,函數(shù)的調(diào)用是在某個(gè)對(duì)象上觸發(fā)的,即調(diào)用位置上存在上下文對(duì)象。

function foo() { 
    console.log( this.a );
}

var a = 2;

var obj = { 
    a: 3,
    foo: foo 
};

obj.foo(); // ?

obj.foo() 打印的結(jié)果是3。

這里foo函數(shù)被當(dāng)做引用屬性,被添加到obj對(duì)象上。這里的調(diào)用過(guò)程是這樣的:

獲取obj.foo屬性 -> 根據(jù)引用關(guān)系找到foo函數(shù),執(zhí)行調(diào)用

所以這里對(duì)foo的調(diào)用存在上下文對(duì)象obj,this進(jìn)行了隱式綁定,即this綁定到了obj上,所以this.a被解析成了obj.a,即3。

多層調(diào)用鏈

function foo() { 
    console.log( this.a );
}

var a = 2;

var obj1 = { 
    a: 4,
    foo: foo 
};

var obj2 = { 
    a: 3,
    obj1: obj1
};

obj2.obj1.foo(); //?

obj2.obj1.foo() 打印的結(jié)果是4。

同樣,我們看下函數(shù)的調(diào)用過(guò)程:

先獲取obj1.obj2 -> 通過(guò)引用獲取到obj2對(duì)象,再訪問(wèn) obj2.foo -> 最后執(zhí)行foo函數(shù)調(diào)用

這里調(diào)用鏈不只一層,存在obj1、obj2兩個(gè)對(duì)象,那么隱式綁定具體會(huì)綁哪個(gè)對(duì)象。這里原則是獲取最后一層調(diào)用的上下文對(duì)象,即obj2,所以結(jié)果顯然是4(obj2.a)。

隱式丟失(函數(shù)別名)

注意:這里存在一個(gè)陷阱,大家在分析調(diào)用過(guò)程時(shí),要特別小心

先看個(gè)代碼:

function foo() { 
    console.log( this.a );
}

var a = 2;

var obj = { 
    a: 3,
    foo: foo 
};

var bar = obj.foo;
bar(); //?

bar() 打印的結(jié)果是2。

為什么會(huì)這樣,obj.foo 賦值給bar,那調(diào)用bar()為什么沒(méi)有觸發(fā)隱式綁定,使用的是默認(rèn)綁定呢。

這里有個(gè)概念要理解清楚,obj.foo 是引用屬性,賦值給bar的實(shí)際上就是foo函數(shù)(即:bar指向foo本身)。

那么,實(shí)際的調(diào)用關(guān)系是:通過(guò)bar找到foo函數(shù),進(jìn)行調(diào)用。整個(gè)調(diào)用過(guò)程并沒(méi)有obj的參數(shù),所以是默認(rèn)綁定,全局屬性a。

隱式丟失(回調(diào)函數(shù))

function foo() { 
    console.log( this.a );
}

var a = 2;

var obj = { 
    a: 3,
    foo: foo 
};

setTimeout( obj.foo, 100 ); // ?

打印的結(jié)果是2。

同樣的道理,雖然參傳是obj.foo,因?yàn)槭且藐P(guān)系,所以傳參實(shí)際上傳的就是foo對(duì)象本身的引用。對(duì)于setTimeout的調(diào)用,還是 setTimeout -> 獲取參數(shù)中foo的引用參數(shù) -> 執(zhí)行 foo 函數(shù),中間沒(méi)有obj的參與。這里依舊進(jìn)行的是默認(rèn)綁定。

顯示綁定

相對(duì)隱式綁定,this值在調(diào)用過(guò)程中會(huì)動(dòng)態(tài)變化,可是我們就想綁定指定的對(duì)象,這時(shí)就用到了顯示綁定。

顯示綁定主要是通過(guò)改變對(duì)象的prototype關(guān)聯(lián)對(duì)象,這里不展開(kāi)講。具體使用上,可以通過(guò)這兩個(gè)方法call(…)或apply(…)來(lái)實(shí)現(xiàn)(大多數(shù)函數(shù)及自己創(chuàng)建的函數(shù)默認(rèn)都提供這兩個(gè)方法)。

call與apply是同樣的作用,區(qū)別只是其他參數(shù)的設(shè)置上

function foo() { 
    console.log( this.a );
}

var a = 2;

var obj1 = { 
    a: 3,
};

var obj2 = { 
    a: 4,
};
foo.call( obj1 ); // ?
foo.call( obj2 ); // ?

打印的結(jié)果是3, 4。

這里因?yàn)轱@示的申明了要綁定的對(duì)象,所以this就被綁定到了obj上,打印的結(jié)果自然就是obj1.a 和obj2.a。

硬綁定

function foo() { 
    console.log( this.a );
}

var a = 2;

var obj1 = { 
    a: 3,
};

var obj2 = { 
    a: 4,
};

var bar = function(){
    foo.call( obj1 );
}

setTimeout( bar, 100 ); // 3

bar.call( obj2 ); // 這是多少

前面兩個(gè)(函數(shù)別名、回調(diào)函數(shù))打印3,因?yàn)轱@示綁定了,沒(méi)什么問(wèn)題。

最后一個(gè)打印是3。

這里需要注意下,雖然bar被顯示綁定到obj2上,對(duì)于bar,function(){…} 中的this確實(shí)被綁定到了obj2,而foo因?yàn)橥ㄟ^(guò)foo.call( obj1 )已經(jīng)顯示綁定了obj1,所以在foo函數(shù)內(nèi),this指向的是obj1,不會(huì)因?yàn)閎ar函數(shù)內(nèi)指向obj2而改變自身。所以打印的是obj1.a(即3)。

new 綁定

js中的new操作符,和其他語(yǔ)言中(如JAVA)的new機(jī)制是不一樣的。js中,它就是一個(gè)普通函數(shù)調(diào)用,只是被new修飾了而已。

使用new來(lái)調(diào)用函數(shù),會(huì)自動(dòng)執(zhí)行如下操作:

如果函數(shù)沒(méi)有返回其他對(duì)象,那么new表達(dá)式中的函數(shù)調(diào)用會(huì)自動(dòng)返回這個(gè)新對(duì)象。
從第三點(diǎn)可以看出,this指向的就是對(duì)象本身。

看個(gè)代碼:

function foo(a) { 
    this.a = a;
}

var a = 2;

var bar1 = new foo(3);
console.log(bar1.a); // ?

var bar2 = new foo(4);
console.log(bar2.a); // ?

最后一個(gè)打印是3, 4。

因?yàn)槊看握{(diào)用生成的是全新的對(duì)象,該對(duì)象又會(huì)自動(dòng)綁定到this上,所以答案顯而易見(jiàn)。

綁定規(guī)則優(yōu)先級(jí)

上面也說(shuō)過(guò),這里在重復(fù)一下。優(yōu)先級(jí)是這樣的,以按照下面的順序來(lái)進(jìn)行判斷:
數(shù)是否在new中調(diào)用(new綁定)?如果是的話this綁定的是新創(chuàng)建的對(duì)象。
數(shù)是否通過(guò)call、apply(顯式綁定)或者硬綁定調(diào)用?如果是的話,this綁定的是 指定的對(duì)象。
數(shù)是否在某個(gè)上下文對(duì)象中調(diào)用(隱式綁定)?如果是的話,this綁定的是那個(gè)上下文對(duì)象。
果都不是的話,使用默認(rèn)綁定。如果在嚴(yán)格模式下,就綁定到undefined,否則綁定到 全局對(duì)象。
var bar = foo()

規(guī)則例外

在顯示綁定中,對(duì)于nullundefined的綁定將不會(huì)生效。

代碼如下:

function foo() { 
    console.log( this.a );
}
foo.call( null ); // 2
foo.call( undefined ); // 2

這種情況主要是用在不關(guān)心this的具體綁定對(duì)象(用來(lái)忽略this),而傳入null實(shí)際上會(huì)進(jìn)行默認(rèn)綁定,導(dǎo)致函數(shù)中可能會(huì)使用到全局變量,與預(yù)期不符。

所以對(duì)于要忽略this的情況,可以傳入一個(gè)空對(duì)象?,該對(duì)象通過(guò)Object.create(null)創(chuàng)建。這里不用{}的原因是,?是真正意義上的空對(duì)象,它不創(chuàng)建Object.prototype委托,{}和普通對(duì)象一樣,有原型鏈委托關(guān)系。

這里傳null的一種具體使用場(chǎng)景是函數(shù)柯里化的使用

擴(kuò)展:箭頭函數(shù)

最后,介紹一下ES6中的箭頭函數(shù)。通過(guò)“=>”而不是function創(chuàng)建的函數(shù),叫做箭頭函數(shù)。它的this綁定取決于外層(函數(shù)或全局)作用域。

case 1 (正常調(diào)用)

普通函數(shù)

function foo(){     
    console.log( this.a );
}

var a = 2;

var obj = { 
    a: 3,
    foo: foo 
};

obj.foo(); //3

箭頭函數(shù)

var foo = () => {   
    console.log( this.a );
}

var a = 2;

var obj = { 
    a: 3,
    foo: foo 
};

obj.foo(); //2
foo.call(obj); //2 ,箭頭函數(shù)中顯示綁定不會(huì)生效

case 2 (函數(shù)回調(diào))

普通函數(shù)

function foo(){ 
    return function(){
        console.log( this.a );
    }   
}

var a = 2;

var obj = { 
    a: 3,
    foo: foo 
};

var bar = obj.foo();
bar(); //2

箭頭函數(shù)

function foo(){ 
    return () => {
        console.log( this.a );
    }   
}



var a = 2;

var obj = { 
    a: 3,
    foo: foo 
};

var bar = obj.foo();
bar(); //3

通過(guò)上面兩個(gè)列子,我們看到箭頭函數(shù)的this綁定只取決于外層(函數(shù)或全局)的作用域,對(duì)于前面的4種綁定規(guī)則是不會(huì)生效的。它也是作為this機(jī)制的一種替換,解決之前this綁定過(guò)程各種規(guī)則帶來(lái)的復(fù)雜性。

注意:對(duì)于ES6之前,箭頭函數(shù)的替換版本是這樣的

// es6
function foo(){ 
    return () => {
        console.log( this.a );
    }   
}

var a = 2;

var obj = { 
    a: 3,
    foo: foo 
};

var bar = obj.foo();
bar(); //3

通過(guò)上面兩個(gè)列子,我們看到箭頭函數(shù)的this綁定只取決于外層(函數(shù)或全局)的作用域,對(duì)于前面的4種綁定規(guī)則是不會(huì)生效的。它也是作為this機(jī)制的一種替換,解決之前this綁定過(guò)程各種規(guī)則帶來(lái)的復(fù)雜性。

注意:對(duì)于ES6之前,箭頭函數(shù)的替換版本是這樣的



// es6
function foo(){ 
    return () => {
        console.log( this.a );
    }   
}

// es6之前的替代方法
function foo(){ 
     var self = this;
    return () => {
        console.log( self.a );
    }   
}

總結(jié)

我們?cè)谑褂胘s的過(guò)程中,對(duì)于this的理解往往覺(jué)得比較困難,再調(diào)試過(guò)程中有時(shí)也會(huì)出現(xiàn)一些不符合預(yù)期的現(xiàn)象。很多時(shí)候,我們都是通過(guò)一些變通的方式(如:使用具體對(duì)象替換this)來(lái)規(guī)避的問(wèn)題??蓡?wèn)題一直存在那兒,我們沒(méi)有真正的去理解和解決它。

本文主要參考了《你不知道的JavaScript(上卷)》,對(duì)this到底是什么,具體怎么綁定的,有什么例外情況以及ES6中的一個(gè)優(yōu)化方向,來(lái)徹底搞清楚我們一直使用的this到底是怎么玩的。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容