1. 程式人生 > >js中this的繫結與丟失

js中this的繫結與丟失

一直以來,this指向的問題都困擾著我,老是不清楚this到底是指向呼叫物件還是指向window,今天做個了結。

問題由下面這道題引出:

var obj={ 
id:"awesome",
cool:function coolFn(){
  console.log(this.id);
 }
};
obj.cool()//awesome
var id="not awesome";
setTimeout(obj.cool,0);//not awesome
關於以上,obj.cool輸出的為“not awesome”,而非“awesome”,可這裡的cool明明是obj呼叫的啊,問題就在於cool()函式失去了與this之間的繫結,即this指向丟失。

this總是指向呼叫的物件,就是說this指向誰與函式宣告的位置沒有關係,只與呼叫的位置有關。this的指向大概分為如下四種:

1.new繫結
new方式是優先順序最高的一種呼叫方式,只要是使用new方式來呼叫一個建構函式,this一定會指向new呼叫函式新建立的物件:

function() thisTo(a){
 this.a=a;
}
var data=new thisTo(2); //在這裡進行了new繫結
console.log(data.a);  //2
2.顯式繫結
顯示繫結指的是通過call()和apply()方法,強制指定某些物件對函式進行呼叫,this則強制指向呼叫函式的物件:
function thisTo(){
   console.log(this.a);
}
var data={
    a:2
}; 
thisTo.call(data));  //2
3.隱式繫結
隱式繫結是指通過為物件新增屬性,該屬性的值即為要呼叫的函式,進而使用該物件呼叫函式:
function thisTo(){
   console.log(this.a);
}
var data={
    a:2,
    foo:thisTo //通過屬性引用this所在函式 
};
data.foo(); //2
4.預設繫結
預設繫結是指當上面這三條繫結規則都不符合時,預設繫結會把this指向全域性物件window:
function thisTo(){
   console.log(this.a);
}
var a=2; //a是全域性物件的一個同名屬性
thisTo(); //2
以上四點是this在繫結呼叫物件時的規則,回到頭部的那道題,該題看上去好像滿足第三點誒,這不就是obj物件呼叫的函式麼?this不就是應該指向obj物件本身麼?實際則不然,在這個setTimeout的呼叫過程中,this的原本指向已經丟失了,丟失之後指向的即為全域性的window。為什麼會丟失呢?下面看下this在什麼情況下會發生丟失:

1.隱式丟失
當進行隱式繫結時,如果進行一次引用賦值或者傳參操作,會造成this的丟失,使this繫結到全域性物件中去。

1.1引用賦值丟失

function thisTo(){
   console.log(this.a);
}
var data={
    a:2,
    foo:thisTo //通過屬性引用this所在函式 
};
var a=3;//全域性屬性

var newData = data.foo; //這裡進行了一次引用賦值 
newData(); // 3
原理:因為newData實際上引用的是foo函式本身,這就相當於:var newData = thisTo;data物件只是一箇中間橋樑,data.foo只起到傳遞函式的作用,所以newData跟data物件沒有任何關係。而newData本身又不帶a屬性,最後a只能指向window。

1.2傳參丟失

function thisTo(){
   console.log(this.a);
}
var data={
    a:2,
    foo:thisTo //通過屬性引用this所在函式 
};
var a=3;//全域性屬性

setTimeout(data.foo,100);// 3
這就是本文開始的那個題目。所謂傳參丟失,就是在將包含this的函式作為引數在函式中傳遞時,this指向改變。setTimeout函式的本來寫法應該是setTimeout(function(){......},100);100ms後執行的函式都在“......”中,可以將要執行函式定義成var fun = function(){......},即:setTimeout(fun,100),100ms後就有:fun();所以此時此刻是data.foo作為一個引數,是這樣的:setTimeout(thisTo,100);100ms過後執行thisTo(),實際道理還跟1.1差不多,沒有呼叫thisTo的物件,this只能指向window。

1.3隱式丟失解決方法

為了解決隱式丟失(隱式丟失專用)的問題,ES5專門提供了bind方法,bind()會返回一個硬編碼的新函式,它會把引數設定為this的上下文並呼叫原始函式。(這個bind可跟$(selector).bind('click',function(){......})的用法不同)

function thisTo(){
   console.log(this.a);
}
var data={
    a:2
}; 
var a=3;
var bar=thisTo.bind(data);
console.log(bar()); //2
2.間接引用

間接引用是指一個定義物件的方法引用另一個物件存在的方法,這種情況下會使得this指向window:

function thisTo(){
   console.log(this.a);
}
var data={
  a:2,
  foo:thisTo
};
var newData={
  a:3
}
var a=4;
data.foo(); //2
(newData.foo=data.foo)() //4
newData.foo();  //3

這裡為什麼(newData.foo=data.foo)()的結果是4,與newData.foo()的結果不一樣呢?按照正常邏輯的思路,應該是先對newData.foo賦值,再對其進行呼叫,也就是等價於這樣的寫法:newData.foo=data.foo;newData.foo();然而這兩句的輸出結果就是3,這說明兩者不等價。

接著,當我們console.log(newData.foo=data.foo)的時候,發現列印的是thisTo這個函式,函式後立即執行括號將函式執行。這句話中,立即執行括號前的括號中的內容可單獨看做一部本,該部分雖然完成了賦值操作,返回值卻是一個函式,該函式沒有確切的呼叫者,故而立即執行的時候,其呼叫物件不是newData,而是window。下一句的newData.foo()是在給newData添加了foo屬性後,再對其呼叫foo(),注意這次的呼叫物件為newData,即我們上面說的隱式繫結的this,結果就為3。

3.ES6箭頭函式
ES6的箭頭函式在this這塊是一個特殊的改進,箭頭函式使用了詞法作用域取代了傳統的this機制,所以箭頭函式無法使用上面所說的這些this優先順序的原則,注意的是在箭頭函式中,是根據外層父親作用域來決定this的指向問題。
function thisTo(){
    setTimeout(function(){
    console.log(this.a);
},100);
}
var obj={
 a:2
}
var a=3;
thisTo.call(obj); //3
不用箭頭函式,發生this傳參丟失,最後的this預設繫結到全域性作用域,輸出3。
function thisTo(){
   setTimeout(()=>{
    console.log(this.a);
},100);
}
var obj={
 a:2
}
var a=3;加粗文字
thisTo.call(obj); //2
用了箭頭函式,不會發生隱式丟失,this繫結到外層父作用域thisTo(),thisTo的被呼叫者是obj物件,所以最後的this到obj物件中,輸出2。
如果不用箭頭函式實現相同的輸出,可以採用下面這種方式:
function thisTo(){
   var self=this; //在當前作用域中捕獲this 
   setTimeout(function(){
    console.log(self.a); //傳入self代替之前的this
},100);
}
var obj={
 a:2
}
var a=3;
thisTo.call(obj); //2