深入理解this機制系列第一篇——this的4種繫結規則

小火柴的藍色理想發表於2016-08-04

前面的話

  如果要問javascript中哪兩個知識點容易混淆,作用域查詢和this機制絕對名列前茅。前面的作用域系列已經詳細介紹過作用域的知識。本系列開始將介紹javascript的另一大山脈——this機制。本文是該系列的第一篇——this的4種繫結規則

 

預設繫結

  全域性環境中,this預設繫結到window

console.log(this === window);//true

  函式獨立呼叫時,this預設繫結到window

function foo(){
    console.log(this === window);
}
foo(); //true

  被巢狀的函式獨立呼叫時,this預設繫結到window

//雖然test()函式被巢狀在obj.foo()函式中,但test()函式是獨立呼叫,而不是方法呼叫。所以this預設繫結到window
var a = 0;
var obj = {
    a : 2,
    foo:function(){
            function test(){
                console.log(this.a);
            }
            test();
    }
}
obj.foo();//0

【IIFE】

  IIFE立即執行函式實際上是函式宣告後直接呼叫執行

var a = 0;
function foo(){
    (function test(){
        console.log(this.a);
    })()
};
var obj = {
    a : 2,
    foo:foo
}
obj.foo();//0
//等價於上例
var a = 0;
var obj = {
    a : 2,
    foo:function(){
            function test(){
                console.log(this.a);
            }
            test();
    }
}
obj.foo();//0

【閉包】

  類似地,test()函式是獨立呼叫,而不是方法呼叫,所以this預設繫結到window

  [注意]函式共有4種呼叫方式,函式呼叫相關內容移步至此

var a = 0;
function foo(){
    function test(){
        console.log(this.a);
    }
    return test;
};
var obj = {
    a : 2,
    foo:foo
}
obj.foo()();//0

  由於閉包的this預設繫結到window物件,但又常常需要訪問巢狀函式的this,所以常常在巢狀函式中使用var that = this,然後在閉包中使用that替代this,使用作用域查詢的方法來找到巢狀函式的this值 

var a = 0;
function foo(){
    var that = this;
    function test(){
        console.log(that.a);
    }
    return test;
};
var obj = {
    a : 2,
    foo:foo
}
obj.foo()();//2

 

隱式繫結

  一般地,被直接物件所包含的函式呼叫時,也稱為方法呼叫,this隱式繫結到該直接物件

function foo(){
    console.log(this.a);
};
var obj1 = {
    a:1,
    foo:foo,
    obj2:{
        a:2,
        foo:foo
    }
}

//foo()函式的直接物件是obj1,this隱式繫結到obj1
obj1.foo();//1

//foo()函式的直接物件是obj2,this隱式繫結到obj2
obj1.obj2.foo();//2

 

隱式丟失

  隱式丟失是指被隱式繫結的函式丟失繫結物件,從而預設繫結到window。這種情況容易出錯卻又常見

【函式別名】

var a = 0;
function foo(){
    console.log(this.a);
};
var obj = {
    a : 2,
    foo:foo
}
//把obj.foo賦予別名bar,造成了隱式丟失,因為只是把foo()函式賦給了bar,而bar與obj物件則毫無關係
var bar = obj.foo;
bar();//0
//等價於
var a = 0;
var bar = function foo(){
    console.log(this.a);
}
bar();//0

【引數傳遞】

var a = 0;
function foo(){
    console.log(this.a);
};
function bar(fn){
    fn();
}
var obj = {
    a : 2,
    foo:foo
}
//把obj.foo當作引數傳遞給bar函式時,有隱式的函式賦值fn=obj.foo。與上例類似,只是把foo函式賦給了fn,而fn與obj物件則毫無關係
bar(obj.foo);//0
//等價於
var a = 0;
function bar(fn){
    fn();
}
bar(function foo(){
    console.log(this.a);
});

【內建函式】

  內建函式與上例類似,也會造成隱式丟失

var a = 0;
function foo(){
    console.log(this.a);
};
var obj = {
    a : 2,
    foo:foo
}
setTimeout(obj.foo,100);//0
//等價於
var a = 0;
setTimeout(function foo(){
    console.log(this.a);
},100);//0

【間接引用】

   函式的"間接引用"一般都在無意間建立,最容易在賦值時發生,會造成隱式丟失

function foo() {
    console.log( this.a );
}
var a = 2;
var o = { a: 3, foo: foo };
var p = { a: 4 };
o.foo(); // 3
//將o.foo函式賦值給p.foo函式,然後立即執行。相當於僅僅是foo()函式的立即執行
(p.foo = o.foo)(); // 2
function foo() {
    console.log( this.a );
}
var a = 2;
var o = { a: 3, foo: foo };
var p = { a: 4 };
o.foo(); // 3
//將o.foo函式賦值給p.foo函式,之後p.foo函式再執行,是屬於p物件的foo函式的執行
p.foo = o.foo;
p.foo();//4

 【其他情況】

  在javascript引擎內部,obj和obj.foo儲存在兩個記憶體地址,簡稱為M1和M2。只有obj.foo()這樣呼叫時,是從M1呼叫M2,因此this指向obj。但是,下面三種情況,都是直接取出M2進行運算,然後就在全域性環境執行運算結果(還是M2),因此this指向全域性環境

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

(obj.foo = obj.foo)();//0

(false || obj.foo)();//0

(1, obj.foo)();//0

 

顯式繫結

  通過call()、apply()、bind()方法把物件繫結到this上,叫做顯式繫結。對於被呼叫的函式來說,叫做間接呼叫

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

  普通的顯式繫結無法解決隱式丟失問題

var a = 0;
function foo(){
    console.log(this.a);
}
var obj1 = {
    a:1
};
var obj2 = {
    a:2
};
foo.call(obj1);//1
foo.call(obj2);//2

【硬繫結】

  硬繫結是顯式繫結的一個變種,使this不能再被修改

var a = 0;
function foo(){
    console.log(this.a);
}
var obj = {
    a:2
};
var bar= function(){
    foo.call(obj);
}
//在bar函式內部手動呼叫foo.call(obj)。因此,無論之後如何呼叫函式bar,它總會手動在obj上呼叫foo
bar();//2
setTimeout(bar,100);//2
bar.call(window);//2

【API】

  javascript中新增了許多內建函式,具有顯式繫結的功能,如陣列的5個迭代方法:map()、forEach()、filter()、some()、every()

var id = 'window';
function foo(el){
    console.log(el,this.id);
}
var obj = {
    id: 'fn'
};
[1,2,3].forEach(foo);//1 "window" 2 "window" 3 "window"
[1,2,3].forEach(foo,obj);//1 "fn" 2 "fn" 3 "fn"

 

new繫結

  如果函式或者方法呼叫之前帶有關鍵字new,它就構成建構函式呼叫。對於this繫結來說,稱為new繫結

  【1】建構函式通常不使用return關鍵字,它們通常初始化新物件,當建構函式的函式體執行完畢時,它會顯式返回。在這種情況下,建構函式呼叫表示式的計算結果就是這個新物件的值

function fn(){
    this.a = 2;
}
var test = new fn();
console.log(test);//{a:2}

  【2】如果建構函式使用return語句但沒有指定返回值,或者返回一個原始值,那麼這時將忽略返回值,同時使用這個新物件作為呼叫結果

function fn(){
    this.a = 2;
    return;
}
var test = new fn();
console.log(test);//{a:2}

  【3】如果建構函式顯式地使用return語句返回一個物件,那麼呼叫表示式的值就是這個物件

var obj = {a:1};
function fn(){
    this.a = 2;
    return obj;
}
var test = new fn();
console.log(test);//{a:1}

  [注意]儘管有時候建構函式看起來像一個方法呼叫,它依然會使用這個新物件作為this。也就是說,在表示式new o.m()中,this並不是o

var o = {
    m: function(){
        return this;
    }
}
var obj = new o.m();
console.log(obj,obj === o);//{} false
console.log(obj.constructor === o.m);//true

 

嚴格模式

  【1】嚴格模式下,獨立呼叫的函式的this指向undefined

function fn(){
    'use strict';
    console.log(this);//undefined
}
fn();

function fn(){
    console.log(this);//window
}
fn();

  【2】在非嚴格模式下,使用函式的call()或apply()方法時,null或undefined值會被轉換為全域性物件。而在嚴格模式下,函式的this值始終是指定的值

var color = 'red';
function displayColor(){
    console.log(this.color);
}
displayColor.call(null);//red

var color = 'red';
function displayColor(){
    'use strict';
    console.log(this.color);
}
displayColor.call(null);//TypeError: Cannot read property 'color' of null

 

最後

  this的四種繫結規則:預設繫結、隱式繫結、顯式繫結和new繫結,分別對應函式的四種呼叫方式:獨立呼叫、方法呼叫、間接呼叫和建構函式呼叫。

  分清這四種繫結規則不算難,比較麻煩的是需要練就火眼金睛,識別出隱式丟失的情況

  說到底,javascript如此複雜的原因是因為函式過於強大。因為,函式是物件,所以原型鏈比較複雜;因為函式可以作為值被傳遞,所以執行環境棧比較複雜;同樣地,因為函式具有多種呼叫方式,所以this的繫結規則也比較複雜

  只有理解了函式,才算理解了javascript

  以上

相關文章