簡(jiǎn)析JavaScript中的最復(fù)雜的機(jī)制之一——this

this是JavaScript中最復(fù)雜的機(jī)制之一,被自動(dòng)定義在所有函數(shù)的作用域中。“When a function of an object was called , the object will be passed to the execution context as 'this' value.”當(dāng)一個(gè)函數(shù)被調(diào)用時(shí),擁有這個(gè)函數(shù)的對(duì)象會(huì)作為this傳入,所以this可以是全局對(duì)象,當(dāng)前對(duì)象乃至任何對(duì)象
例如:

function f(){
    var name = "Funny";
    console.log(this.name);  //undefined
    console.log(this);   //window
}
f();    //f();與window.f();效果相同

調(diào)用函數(shù)f,因?yàn)楹瘮?shù)f是最外層的函數(shù),所以它擁有全局作用域,換句話說(shuō),它是全局對(duì)象(window)的一個(gè)方法(全局變量是全局對(duì)象的屬性或方法)。console.log(this.name),程序執(zhí)行到這里時(shí),會(huì)對(duì)全局作用域進(jìn)行RHS查詢名為name的變量,由于不存在,所以控制臺(tái)輸出‘undefined’,console.log(this),輸出全局對(duì)象window。
?為什么要用this這個(gè)關(guān)鍵字?倘若我們不用this:

var me = {
    name: "Kyle"
};
var you = {
    name: "Reader"
};
function identify(context){
    return context.name.toUpperCase();
}

function speak(context){
    var greeting = "Hello, I'm " + identify(context);
    console.log(greeting);
}
identify(you);  //READER
speak(me);  //Hello,I'm KYLE

如果不用this來(lái)傳遞上下文對(duì)象,而采用顯式傳遞上下文對(duì)象,這會(huì)讓代碼變得越來(lái)越混亂,尤其當(dāng)使用模式越來(lái)越復(fù)雜的時(shí)候(上面的例子代碼特別簡(jiǎn)單)。函數(shù)自動(dòng)引用合適的上下文對(duì)象十分重要:

var you = {
    name: "Reader";
};
var me = {
    name: "Kyle"
};
function identify(){
    return this.name.toUpperCase();
}
function speak(){
    var greeting = "Hello,I'm " + identify.call(this);
    console.log(greeting);
}
identify.call(you); //READER
speak.call(me); //Hello,I'm KYLE

this并非指向函數(shù)本身

function foo(num){
    console.log("foo: " + num);
    this.count++;
}
foo.count = 0;
var i;
for(i = 0;i < 10; i++){
    if(i > 5){
        foo(i); //foo: 6;foo: 7;foo: 8;foo: 9;
    }
}
console.log(foo.count);     //0

按道理,count是計(jì)算函數(shù)foo調(diào)用的次數(shù),foo.count = 0的確為函數(shù)對(duì)象foo添加一個(gè)名為count的屬性,但是foo函數(shù)內(nèi)部的this.count中的this并非指向foo函數(shù)對(duì)象,而是指向window(全局對(duì)象)。在非嚴(yán)格模式下,this.count++;,會(huì)對(duì)count進(jìn)行LHS查詢,然而并沒(méi)有在全局作用域中找到,所以就創(chuàng)建一個(gè)名為count的全局變量(嚴(yán)格模式下,程序會(huì)拋出引用異常);隨著函數(shù)foo的調(diào)用,全局對(duì)象的屬性count就遞增,而函數(shù)對(duì)象的屬性count則不變化,當(dāng)然,console.log(count);會(huì)輸出4。
如果要從函數(shù)對(duì)象內(nèi)部引用它本身,那么只使用this是不夠的。我們可以強(qiáng)制this指向foo函數(shù)本身:

function foo(num){
    console.log("foo: " + num);
    this.count++;
}
foo.count = 0;
var i;
for(i = 0; i < 10; i++){
    if(i > 5){
        foo.call(foo,i);
    }
}
console.log(foo.count);     //4

大多數(shù)情況下this并非指向函數(shù)的作用域
this在任何情況下都不會(huì)指向函數(shù)的詞法作用域,作用域和對(duì)象類似,可見(jiàn)的標(biāo)識(shí)符都是它的屬性,但是作用域無(wú)法通過(guò)JS代碼訪問(wèn),它存在于JS引擎內(nèi)部。下面是個(gè)經(jīng)典的錯(cuò)誤例子:

function foo(){
    var a = 2;
    this.bar();
}
function bar(){
    console.log(this.a);
}
foo();  //ReferenceError: a is not defined

這段代碼首先通過(guò)this.bar()來(lái)引用函數(shù)bar(意外的成功了),通常省略前面的this,此外,開(kāi)發(fā)者還試圖用this聯(lián)通foo()和bar()的詞法作用域,使得bar()可以訪問(wèn)foo()作用域中的變量a,當(dāng)然這是不可能的。至于為什么拋出這樣一個(gè)異常,這里就不再贅述了。
this到底是一樣什么樣的機(jī)制???
this的綁定和函數(shù)聲明的位置沒(méi)有任何關(guān)系,只取決于函數(shù)的調(diào)用方式。(動(dòng)態(tài)作用域的定義瞬間浮現(xiàn)在腦海里)。當(dāng)一個(gè)函數(shù)被調(diào)用時(shí),會(huì)創(chuàng)建一個(gè)活動(dòng)記錄(有時(shí)也叫執(zhí)行上下文)。這個(gè)紀(jì)錄會(huì)包含函數(shù)在哪里被調(diào)用(調(diào)用棧)、函數(shù)調(diào)用方式、傳入的參數(shù)等信息。this就是這個(gè)記錄的一個(gè)屬性,會(huì)在函數(shù)執(zhí)行的過(guò)程中用到。

調(diào)用位置

要理解this的綁定過(guò)程,首先要理解調(diào)用位置。


D86C4F31-669D-4A41-BFFF-8A7A4647B25D.png

下面的例子中的注釋就分析了調(diào)用棧,顯然易懂,不再贅述。

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ī)則進(jìn)行應(yīng)用,如果多條規(guī)則都適用,則按優(yōu)先級(jí)。

默認(rèn)綁定

最常見(jiàn)的函數(shù)調(diào)用類型:獨(dú)立函數(shù)調(diào)用

function foo(){
    console.log(this.a);
}
var a = 0;
foo();      //0

函數(shù)foo調(diào)用時(shí)應(yīng)用了默認(rèn)綁定,this指向了全局對(duì)象window。foo()是直接使用不帶任何修飾的函數(shù)引用進(jìn)行調(diào)用的,因此只能應(yīng)用默認(rèn)綁定嚴(yán)格模式下,則不能將全局對(duì)象用于默認(rèn)綁定,因此this會(huì)被綁定到undefined

隱式綁定

調(diào)用位置是否有上下文對(duì)象?是否被某個(gè)對(duì)象擁有或者包含? function foo(){ console.log(this.a); } var obj = { a: 2, foo: foo }; obj.foo(); //2 無(wú)論直接在obj中定義還是先定義在添加為引用屬性,函數(shù)foo嚴(yán)格上來(lái)說(shuō)都不屬于這個(gè)對(duì)象,但是在函數(shù)被調(diào)用的時(shí)候,可以認(rèn)為obj擁有或者包含這個(gè)函數(shù),調(diào)用位置會(huì)使用obj上下文來(lái)引用函數(shù)。當(dāng)函數(shù)引用有上下文對(duì)象時(shí),隱式綁定會(huì)把函數(shù)調(diào)用中的this綁定到這個(gè)上下文對(duì)象。所以調(diào)用foo()時(shí),this被綁定到obj,因而這里的this.a和obj.a就是一樣的。對(duì)象屬性引用鏈只有上一層在調(diào)用位置起作用
被隱式綁定的函數(shù)會(huì)丟失綁定對(duì)象,然后它應(yīng)用默認(rèn)綁定(非嚴(yán)格模式下)。

function foo(){
    console.log(this.a);
}
function doFoo(fn){
    //fn其實(shí)引用的是foo
    fn();
}
var obj = {
    a: 2,
    foo: foo
};
var a = "oops,global";
doFoo(obj.foo); //“oops,global"

參數(shù)傳遞是一種隱式賦值,所以這種會(huì)造成隱式丟失(顯式賦值也會(huì)造成隱式丟失)。

顯式綁定

使用函數(shù)的call(...)和apply(...)方法。它們的第一個(gè)參數(shù)是對(duì)象,顯然是為this準(zhǔn)備的,在調(diào)用函數(shù)時(shí),將它綁定到this。

function foo(){
    console.log(this.a);
}
var obj = {
    a: 2
};
foo.call(obj);  //2

顯式綁定一樣無(wú)法解決之前的丟失綁定問(wèn)題,但顯式綁定的變種可以解決這個(gè)問(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是無(wú)法再修改它的this
bar.call(window);   //2

首先我們創(chuàng)建了一個(gè)函數(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。硬綁定是一種非常常用的內(nèi)置方法。ES5提供了內(nèi)置的方法Function.prototype.bind:

function foo(something){
    console.log(this.a,something);
    return this.a + something;
}
var obj = {
    a: 2
};
var bar = foo.bind(obj);
var b =  bar(3);    //2 3;
console.(b);    //5
new綁定

使用new來(lái)調(diào)用函數(shù),會(huì)自動(dòng)執(zhí)行如下操作:
1、構(gòu)造一個(gè)新對(duì)象
2、這個(gè)新對(duì)象會(huì)被執(zhí)行[[Protorype]]連接
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)用函數(shù)foo,我們會(huì)構(gòu)造一個(gè)新對(duì)象并把它綁定到foo調(diào)用中的this上。

優(yōu)先級(jí)

正常情況:new綁定>顯式綁定>隱式綁定>默認(rèn)綁定
特殊情況:當(dāng)把null或者undefined作為綁定對(duì)象傳入call、apply、bind,這些操作會(huì)被忽略,最后應(yīng)用默認(rèn)綁定。創(chuàng)建一個(gè)函數(shù)的“間接引用”,即前面提的綁定丟失,最終也是應(yīng)用默認(rèn)綁定。

軟綁定

給默認(rèn)綁定指定一個(gè)全局對(duì)象和undefined以外的值,那么就可以實(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;
    };
}

首先檢查調(diào)用時(shí)的this,如果this綁定到全局對(duì)象或undefined,那就把指定的默認(rèn)對(duì)象obj綁定到this,否則不會(huì)修改this。

ES6中的新玩法

箭頭函數(shù)不是用function關(guān)鍵字定義的,而是用操作賦 => 定義的,這是ES6中新增的語(yǔ)法糖之一。箭頭函數(shù)不適用于this的綁定規(guī)則,而是根據(jù)外層作用域來(lái)決定this,無(wú)論最外層綁定到了什么,它都會(huì)繼承下來(lái)。
首先箭頭函數(shù)的詞法作用域:

function foo(){
    return (a) => {
        console.log(this.a);
    };
}
var obj1 = {
    a:2
};
var obj2 = {
    a:3
};
var bar = foo.call(obj1);
bar.call(obj2); //2

foo()內(nèi)部的箭頭函數(shù)會(huì)捕獲調(diào)用foo()時(shí)的this,這里是obj1,所以bar的this綁定到了obj1,箭頭函數(shù)的綁定無(wú)法修改。
箭頭函數(shù)常用于回調(diào)函數(shù)中,例如事件處理器或定時(shí)器:

function foo(){
    setTimeout( () => { //這里的this在此法上繼承自foo()
        console.log(this.a);
    },100);
}
var obj = {
    a:2
};
foo.call(obj);  //2
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 官方中文版原文鏈接 感謝社區(qū)中各位的大力支持,譯者再次奉上一點(diǎn)點(diǎn)福利:阿里云產(chǎn)品券,享受所有官網(wǎng)優(yōu)惠,并抽取幸運(yùn)大...
    HetfieldJoe閱讀 6,932評(píng)論 15 54
  • 1.函數(shù)調(diào)用棧和調(diào)用位置 在函數(shù)執(zhí)行的時(shí)候,會(huì)有一個(gè)活動(dòng)記錄(也叫執(zhí)行上下文)來(lái)記錄函數(shù)的調(diào)用順序,這個(gè)就是函數(shù)調(diào)...
    lightNate閱讀 541評(píng)論 1 14
  • 1. this之謎 在JavaScript中,this是當(dāng)前執(zhí)行函數(shù)的上下文。因?yàn)镴avaScript有4種不同的...
    百里少龍閱讀 1,023評(píng)論 0 3
  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 134,947評(píng)論 18 139
  • “就是要寵你寵你寵上了天”,每一次讓你逗我開(kāi)心,你總是用一些歌詞,我總是嫌棄你沒(méi)有誠(chéng)意,然而你的行動(dòng)卻比作詞人來(lái)...
    Dontsayhellosay閱讀 154評(píng)論 0 0