1. 程式人生 > >js中this指向全面解析——四種繫結規則

js中this指向全面解析——四種繫結規則

this

this指向什麼?不瞭解this時,一看到函式中有this,就以為this指的是函式自身,這是錯的!!
首先要明確:
this既不指向函式自身也不指向函式的詞法作用域。
this是執行時進行繫結的,而不是在編寫時繫結,它的上下文取決於函式呼叫的各種條件。
this的繫結和函式宣告的位置沒有任何關係,只取決於函式的呼叫方式,完全取決於函式在哪裡被呼叫
好吧,還是好迷??不要急,我們往下看,慢慢理解,看完這篇文章會豁然開朗。
本文將會講解三個內容:1.呼叫位置、2.繫結規則、3.優先順序

1.呼叫位置

呼叫位置:函式被呼叫的位置 (但是有時候會隱藏真正的呼叫位置,所以要學會利用呼叫棧

進行分析)
什麼是呼叫棧? 用下面這個程式碼分析呼叫棧

function baz() {
        // 當前呼叫棧是:baz
        // 因此,當前呼叫位置是全域性作用域
        console.log( "baz" );
        bar(); // <-- bar 的呼叫位置
}
function bar() {
       // 當前呼叫棧是 baz -> bar
       // 因此,當前呼叫位置在 baz 中
       console.log( "bar" );
       foo(); // <-- foo 的呼叫位置
}
function
foo() { // 當前呼叫棧是 baz -> bar -> foo // 因此,當前呼叫位置在 bar 中 console.log( "foo" ); } baz(); // <-- baz 的呼叫位置

還可以用開發者工具得到呼叫棧
要判斷被呼叫函式的呼叫棧,就在被呼叫函式第一行程式碼設定個斷點,棧中的第二個元素就是真正的呼叫位置,例如:foo函式的呼叫棧是?在foo函式中第一行程式碼打個斷點
在這裡插入圖片描述
呼叫棧的第二個元素就是真正的呼叫位置

想要知道函式this繫結的物件,那麼要先找出這個函式直接呼叫位置,再判斷用什麼規則來判斷繫結物件。也就是:
this繫結的物件?呼叫位置=>規則=>繫結物件
繫結規則有:預設繫結、隱式繫結、顯示繫結、new繫結

2.繫結規則

先簡單介紹一下繫結規則:
【new繫結】由new呼叫?繫結到新建立的物件
【顯示繫結】由call或者apply(或者bind)呼叫?繫結到制定的物件
【隱式繫結】由上下文物件呼叫?繫結到那個上下文物件
【預設繫結】{嚴格模式下}繫結到undefined;{非嚴格模式下}繫結到全域性物件

【預設繫結】

最常用的函式呼叫模型,無法使用其他規則時的預設規則
獨立函式呼叫,可能有些讀者會不懂什麼叫“獨立”,下面會解釋
非嚴格模式下可以使用預設繫結,嚴格模式下不能使用

一、非【嚴格模式】下: this指向全域性物件
首先,先思考一下以下程式碼會輸出什麼

//程式碼一
function foo() {
           var  a = 4;
            console.log( a );
        }
 var a = 2; 
foo(); 

再思考一下以下程式碼

//程式碼二
function foo() {
    var  a = 4; //這裡是foo函式裡的a
    console.log( this.a );
}
var a = 2;    //這裡是全域性物件的a
foo(); 

答案:
程式碼一:輸出4
程式碼二:輸出2
程式碼一的輸出很容易知道,程式碼二多了個this輸出就不同了??
因為程式碼二中,輸出的是全域性物件a,因為呼叫foo()沒有任何修飾進行呼叫,使用了預設繫結,此時的this指向了全域性物件

此時產生了疑惑,如果函式不在全域性呼叫在另一個函式裡面呼叫並且這個函式是有a的,那此時的this指向誰呢?

function foo() {
            var  a=4;
            console.log('foo函式:'+this.a );
            lo();   ====》沒有任何修飾呼叫
 }
function lo() {
             var  a=6;
            console.log('lo函式:'+this.a );
            go();  ====》沒有任何修飾呼叫

 }
function go() {
            var  a=8;
            console.log('go函式:'+this.a );
 }
 var a = 2;
foo();   ====》沒有任何修飾呼叫

輸出結果:
在這裡插入圖片描述
從結果看出,被呼叫的函式foo()、lo()、go()函式的this指向的都是全域性物件a=2,所以就算函式不在全域性呼叫,this也會指向全域性物件

二、【嚴格模式】this會繫結到undefined
!!這裡要注意所說的嚴格模式下,不是指在嚴格模式下呼叫函式,this會繫結到undefined,說的是this的呼叫位置
看下面程式碼

function foo() {
    "use strict";  
    console.log( this.a ); //呼叫this.a的函式體是嚴格模式
}
var a = 2;
foo(); // TypeError: this is undefined
function foo() {
    console.log( this.a );  //呼叫this.a的函式體是非嚴格模式
}
var a = 2;
(function(){
    "use strict";
    foo(); // 2  呼叫foo()的位置是嚴格模式
})();

從上面兩個程式碼可以看出,【程式碼一】在foo()函式體內採用嚴格模式並使用了this.a,this會繫結到undefined;【程式碼二】foo()在嚴格模式下呼叫this不會繫結到undefined

【隱式繫結】

呼叫位置是否有上下文物件
一個物件,這個物件有指向函式的屬性,通過這個屬性間接引用函式,從而把函式裡的this(間接)(隱式)繫結到這個物件上

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

obj.foo()呼叫函式時,this指向的是obj這個上下文物件,this被繫結到了obj,此時this.a和obj.a是一樣的。——這就是隱式繫結

現在可以解釋什麼叫獨立函式呼叫
obj.foo() 、foo() 前者是有 obj. 後者是直接 foo()沒有任何修飾,也就是獨立函式呼叫
所以foo()使用的是預設繫結,此時的this指向了全域性物件a

隱式繫結常見的問題: 隱式丟失
隱式丟失:被隱式繫結的函式會丟失繫結物件,也就是它會應用預設繫結,從而吧this繫結到全域性物件或者undefined上(取決於是否是嚴格模式)

下面看看這段程式碼理解一下“被隱式繫結的函式會丟失繫結物件”這句話

function foo() {
        console.log( this.a );
}
 var obj = {
        a: 2,
        foo: foo
 };
 var bar = obj.foo; // 函式別名!
 var a = "oops, global"; // a 是全域性物件的屬性
 obj.foo(); //2
 bar(); // "oops, global"

bar = obj.foo 看上去bar()函式的this跟obj.foo的this是一樣的,也就是看上去bar函式的this繫結的是obj物件
呼叫bar()了之後輸出的是“oops, global”,實際上bar的this被繫結到了全域性物件
分析一下
obj.foo:foo函式的this繫結obj(隱式),
bar = obj.foo:被bar引用了之後丟失了obj物件,實際上是bar=foo ——這就是“被隱式繫結的函式會丟失繫結物件”
所以 bar(); 其實是一個不帶任何修飾的函式呼叫,用了預設繫結

下面的情況更加常見,發生在傳入回撥函式時:

function foo() {
    console.log( this.a );
}
function doFoo(fn) {
    // fn 其實引用的是 foo
    fn(); // <-- 呼叫位置!
}
var obj = {
    a: 2,
    foo: foo
};
var a = "oops, global"; // a 是全域性物件的屬性
doFoo( obj.foo ); // "oops, global
doFoo( foo );// "oops, global

看上面的結果,發現doFoo( obj.foo );和doFoo( foo );結果是一樣的,說明obj.foo作為引數傳入給函式時也會丟失obj物件

那如果傳的函式不是自己宣告的,而是內建的函式(如:setTimeout( )內建函式)結果會如何呢?
看以下程式碼

function foo() {
    console.log( this.a );
}
var obj = {
    a: 2,
    foo: foo
};
var a = "oops, global"; // a 是全域性物件的屬性
setTimeout( obj.foo, 100 ); // "oops, global"

發現結果也是"oops, global",obj.foo引數傳入給內建函式時也丟失了obj(即只剩下foo傳給setTimeout)

【顯示繫結】

js提供的絕大多數函式以及我們自己建立的所有函式都可以使用call(…)和apply(…)方法來進行顯示繫結

在這裡簡單提一下call和apply
call和apply的作用完全一樣,可以用來改變this 的物件,接受引數的方式不太一樣,第一個引數都是一個物件(this繫結的物件),call可以依次傳入,apply要放進數組裡傳進去
如:通過call和apply傳引數給function foo(arg1,arg2){ … }
foo.call(obj, arg1, arg2);
foo.apply(obj, [arg1, arg2]);

function foo() {
    console.log( this.a );
}
var obj = {
    a:2  //obj中的a
};
var a=4;  //window中的a
foo.call( obj ); // 2  輸出obj中的a
foo(); //4 輸出window中的a

顯式地把obj繫結到foo上

顯式繫結無法解決丟失繫結的問題,但是可以用顯示繫結的一個變種來解決

硬繫結

function foo() {
    console.log( this.a );
}
var obj = {
    a:2  //obj中的a
}; 
var a=4;   //widow的a
var bar = function() {
    foo.call( obj ); //硬繫結,將obj繫結到foo上,
};
bar(); // 2
setTimeout( bar, 100 ); // 2
// 硬繫結的 bar 不可能再修改它的 this
bar.call( window ); // 2    輸出的還是obj中的a,說明無法修改已繫結的物件

用一個函式bar,將foo的this繫結到obj上,以後不論怎麼呼叫都不會改變this已經繫結到obj的事實,每次呼叫時都會在obj上呼叫foo。
這種顯式的強制繫結——硬繫結

典型應用場景,建立一個包裹函式,傳入所有的引數並返回接收到的所有值

function foo(something) {
    console.log( this.a, something );  //輸出接收到的全部引數
    return this.a + something;   //將接收到的全部引數加起來,並返回
}
var obj = {
    a:2
};
var bar = function() {
    return foo.apply( obj, arguments );  //將foo的this繫結到obj上
};
var b = bar( 3 ); // 2 3
console.log( b ); // 5

還可以用bind繫結,在ES5中提供的內建的方法Function.prototype.bind

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

var obj = {
    a:2  //obj中的a
};
var  a=4;  //window的a
var bar = foo.bind( obj );  //返回一個foo中this繫結到obj上的新函式,並沒有改變原函式
foo();//4   發現果然沒有改變原來的foo
var b = bar( 3 ); // 2 3
console.log( b ); // 5

注意,bind和call()、apply()雖然都可以用來將this繫結到某個物件上,但是不太相同

【new繫結】

function foo(a) {
    this.a = a; //把a賦值給物件中的a屬性
}

var bar = new foo(2);
console.log(typeof(bar)); //object     bar的型別是object
console.log( bar.a ); // 2

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

console.log( bar.a ); // 2

分析上述程式碼
使用new呼叫foo(…)時,會構造一個新物件並幫到foo(…)呼叫的this上。
var bar = new foo(2);將bar繫結到foo(…)的this上 ,將2賦值給bar中的a屬性
var tree = new foo(4);將tree繫結到foo(…)的this上 ,將4賦值給bar中的a屬性

3.優先順序

四種規則的優先順序: new繫結 > 顯式繫結 > 隱式繫結 > 預設繫結

this繫結的物件?呼叫位置=》規則=》繫結物件
可以按照這個順序判斷:
1、函式是否在new中呼叫(new繫結)?如果是的話this繫結的是新建立的物件
var bar = new foo()
foo中的this繫結在bar物件上
2、函式是否通過call、apply(顯式繫結)?如果是this繫結的是指定的物件
var bar = foo.call(obj2)
foo中的this繫結在obj2上
3、函式是否在某個上下文物件紅呼叫(隱式繫結)?如果是的話,this繫結的是那個上下文物件
var bar = obj1.foo()
foo的this繫結到obj1上
4、如果都不是的話,使用預設繫結。在嚴格模式下,繫結到undefined;否則。繫結全域性物件

這篇文章是在看《你不知道的JavaScript》(上卷) this這部分內容之後再加上自己的一些嘗試和理解寫下的讀書筆記。
參考:《你不知道的JavaScript》(上卷)