JS中的this詳解

原文閱讀javascript

  js中的this是很容易讓人以爲困惑的地方,這篇文章打算說一下this綁定的幾種狀況,相信能夠解決大部分關於this的疑惑。java

this是在運行的時候綁定的,不是在編寫的時候綁定的,函數調用的方式不一樣,就可能使this所綁定的對象不一樣。git


1.幾種綁定規則

  函數調用的位置對this的指向有着很大的影響,但卻不是徹底取決於它。下面是幾種this的綁定規則:github

1.1.默認綁定

  默認規則的意思就是在通常狀況下,若是沒有別的規則出現,就將this綁定到全局對象上,咱們看以下代碼:app

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

  這段代碼中,this是被默認綁定到了全局對象上,因此this.a獲得的是2。咱們如何判斷這裏應用了默認綁定呢?foo在調用的時候直接使用不帶任何修飾的函數引用,只能使用默認綁定。有人會誤認爲結果是3this常有的幾種錯誤理解之一就是認爲this指向當前函數的詞法做用域,this與詞法做用域以及做用域對象是徹底不一樣的東西,做用域對象是在引擎內部的,js代碼是沒法訪問的。還有本文咱們不討論嚴格模式下的狀況,嚴格模式這裏的this會綁定到undefined函數

1.2.隱式綁定

  若是在調用位置有上下文對象,說簡單點就是這個函數調用時是用一個對象.出來的。就像下邊這樣,它就遵循隱式綁定:this

function foo() {
    console.log(this.a);
}
var obj = {
    a: 2,
    foo: foo
}
var a = "opps, gloabl";                //全局對象的屬性
obj.foo();                    //2
var bar = obj.foo;
bar();                        //"opps, gloabl"

  第9行代碼,就是函數在調用的時候,是用前邊的對象加上.操做符調用出來的,此時就用到了隱式綁定規則,隱式綁定規則會將函數調用中的this綁定到這個上下文對象,此時的this.aobj.a是同樣的。
  而隱式綁定會出現一個問題,就是隱式丟失,上邊的第10行代碼,是新建一個foo函數的引用,即bar,在最後一行調用的時候,這個函數不具備上下文對象,此時採用默認綁定規則,獲得的結果天然是opps, gloabl;
  綁定丟失也會發生在函數做爲參數傳遞的狀況下,即傳入回調函數時,由於參數傳遞就是一種隱式賦值,看以下代碼:編碼

function foo() {
    console.log( this.a );
}
function doFoo(fn) {
    fn();            //在此處調用,參數傳遞是隱式賦值,丟失this綁定
}
var obj = {
    a: 2,
    foo: foo
};
var a = "opps, global";
doFoo( obj.foo );        //看似是隱式綁定,輸出opps, global

  javascript環境中內置的函數,在具備接受一個函數做爲參數的功能的時候,也會發生像上邊這種情況。例如setTimeout函數的實現就相似於下邊的僞代碼:code

function setTimeout(fn, delay) {
    //等待delay毫秒
    fn();//在此處調用
}

  因此回調函數丟失this綁定是很是常見的,後邊咱們再看如何經過固定this來修復這個問題。對象

1.3.顯式綁定

  在此以前,相信你已經用過不少次applycall函數了,使用這兩個函數能夠直接爲你要執行的函數指定this,因此這種方式稱爲顯式綁定。

function foo() {        //顯式綁定this,這種方式依然沒法解決綁定丟失的問題
    console.log( this.a );
}
var obj = {
    a: 2
};
foo.call( obj );        //2

  經過像上邊這樣調用,咱們能夠將foothis強制綁定到obj上。若是給call傳入的是一個基本類型數據,這個基本類型數據將會被轉換成對應的基本包裝類型。不過這種方式依然沒法解決上邊的丟失綁定問題。

1.3.1.硬綁定

  爲了解決這個問題,咱們能夠寫像下邊這樣的代碼:

function foo() {
    console.log( this.a );
}
var obj = {
    a: 2
};
var bar = function() {         //建立一個包裹函數,以確保obj的綁定
    foo.call( obj );
};
bar();                       //2
setTimeout( bar, 100 );           //2
bar.call( window );           //2

  上邊這樣的函數確實解決了綁定丟失的問題,每次調用bar就能夠確保obj的綁定,不過還不能爲函數傳參,並且這種方法複用率低,因此又出現了這樣的輔助綁定函數:

function foo(something) {
    console.log( this.a, something );
    return this.a + something;
}
function bind(fn, obj) {        //輔助綁定函數
    return function() {
        return fn.apply( obj, arguments );
    };
}
var obj = {
    a: 2
};
var bar = bind( foo, obj );
var b = bar(3);
console.log(b);

  由於這種模式很經常使用,因此ES5內置了這個方法,就是bindbind(...)返回一個硬編碼的新函數,將你指定的對象綁定到調用它的函數的this上。

function foo(something) {
    console.log( this.a, something );
    return this.a + something;
}
var obj = {
    a: 2
};
var bar = foo.bind( obj );    //bind返回一個綁定到obj上的新函數
var b = bar(3);
console.log(b);
var a = "window's a";
foo("!");                       //對原來的函數不產生影響

1.3.2.API調用參數指定this

  許多第三方庫裏的函數,以及許多語言內置的函數,都提供了一個可選的參數用來指定函數執行的this

function foo(el) {
    console.log( el, this.id );
}
var obj = {
    id: "awesome"
};
[1, 2, 3].forEach( foo, obj );   //forEach的第二個參數就是用來設置this

1.4.new綁定

js中的所謂的構造函數,其實和通常的普通函數沒有什麼區別,並不具備特殊性,它們只是被new操做符調用的普通函數而已。實際上並不存在什麼構造函數,只存在對於函數的構造調用

發生構造函數的調用時,會自動執行下邊的操做:

  1. 建立一個全新的對象。

  2. 這個對象會被執行[[Prototype]]鏈接。

  3. 這個新對象會綁定到函數調用的this

  4. 執行這個函數裏的代碼。

  5. 若是函數沒有返回其餘對象,則自動返回這個新對象。

這個在執行new操做的時候對this的綁定就叫作new綁定。

function fun() {
  this.a = 1;
  this.b = 2;
}
var instance = new fun();
console.log(instance.a);

1.5.箭頭函數的this

  ES6中的箭頭函數是沒法使用以上幾種規則的,它是根據外層的做用域來決定this,即取決於外層的函數做用域或全局做用域,並且箭頭函數的綁定沒法修改,即便是new綁定也不能夠。

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

2.綁定規則的優先級

  前邊咱們已經說了this的幾種綁定規則,當函數調用的位置可使用多條綁定規則的時候,咱們就須要肯定這幾種規則的優先級。

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

  從上邊的代碼能夠看出來,顯式綁定的優先級要高於隱式綁定,下邊再看看顯式綁定和new綁定的優先級:

function foo(something) {
    this.a = something;
}
var obj1 = {};
var bar = foo.bind( obj1 );
bar(2);
console.log( obj1.a );    //2
var baz = new bar(3);
console.log( obj1.a );    //2
console.log( baz.a );    //3

  仔細看這段代碼,barfoo綁定到obj1上返回的一個函數,對這個函數進行new操做,並傳入新的a值,發現改變的是新對象baz的屬性,和obj1已經脫離關係。說明new綁定的優先級高於硬綁定。

  綜上所述,咱們在遇到this時,若是不是箭頭函數,就能夠以這種順序判斷它的指向:

  1. 若是函數在new中調用,綁定到新建的對象。

  2. 函數經過callapply或者硬綁定調用,this綁定到指定的對象上。

  3. 函數在某個上下文對象中調用,綁定到這個上下文對象上。

  4. 採用默認綁定規則。


  以上就是這篇博客的全部內容了,若是有什麼理解的不對的地方歡迎討論,這裏是個人博客以及github,歡迎來訪。

參考書籍:《你不知道的JavaScript(上卷)》

相關文章
相關標籤/搜索