深刻理解 js this 綁定 ( 無需死記硬背,尾部有總結和麪試題解析 )

js 的 this 綁定問題,讓多數新手懵逼,部分老手以爲噁心,這是由於this的綁定 ‘難以捉摸’,出錯的時候還每每不知道爲何,至關反邏輯。
讓咱們考慮下面代碼:面試

var people = {
    name : "海洋餅乾",
    getName : function(){
        console.log(this.name);
    }
};
window.onload = function(){
    xxx.onclick =  people.getName;
};

在平時搬磚時比較常見的this綁定問題,你們可能也寫給或者遇到過,當xxx.onclick觸發時,輸出什麼呢 ?數組

爲了方便測試,我將代碼簡化:app

var people = {
    Name: "海洋餅乾",
    getName : function(){
        console.log(this.Name);
    }
};
var bar = people.getName;

bar();    // undefined

經過這個小例子帶你們感覺一下this噁心的地方,我最開始遇到這個問題的時候也是一臉懵逼,由於代碼裏的this在建立時指向很是明顯啊,指向本身 people 對象,可是實際上指向 window 對象,這就是我立刻要和你們說的 this 綁定規則函數

1 . this

什麼是this ?在討論this綁定前,咱們得先搞清楚this表明什麼。測試

  1. this是JavaScript的關鍵字之一。它是 對象 自動生成的一個內部對象,只能在 對象 內部使用。隨着函數使用場合的不一樣,this的值會發生變化。
  2. this指向什麼,徹底取決於 什麼地方以什麼方式調用,而不是 建立時。(比較多人誤解的地方)(它很是語義化,this在英文中的含義就是 這,這個 ,但這其實起到了必定的誤導做用,由於this並非一成不變的,並不必定一直指向當前 這個

2 . this 綁定規則

掌握了下面介紹的4種綁定的規則,那麼你只要看到函數調用就能夠判斷 this 的指向了this

2 .1 默認綁定

考慮下面代碼:prototype

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

這種就是典型的默認綁定,咱們看看foo調用的位置,」光桿司令「,像 這種直接使用而不帶任何修飾的函數調用 ,就 默認且只能 應用 默認綁定。code

那默認綁定到哪呢,通常是window上,嚴格模式下 是undefined對象

2 .2 隱性綁定

代碼說話:繼承

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

obj.foo();            // ?

答案 : undefined 10

foo()的這個寫法熟悉嗎,就是咱們剛剛寫的默認綁定,等價於打印window.a,故輸出undefined ,
下面obj.foo()這種你們應該常常寫,這其實就是咱們立刻要討論的 隱性綁定

函數foo執行的時候有了上下文對象,即 obj。這種狀況下,函數裏的this默認綁定爲上下文對象,等價於打印obj.a,故輸出10

若是是鏈性的關係,好比 xx.yy.obj.foo();, 上下文取函數的直接上級,即緊挨着的那個,或者說對象鏈的最後一個。

2 .3 顯性綁定

2 .3 .1 隱性綁定的限制

在咱們剛剛的 隱性綁定中有一個致命的限制,就是上下文必須包含咱們的函數 ,例:var obj = { foo : foo },若是上下文不包含咱們的函數用隱性綁定明顯是要出錯的,不可能每一個對象都要加這個函數 ,那樣的話擴展,維護性太差了,咱們接下來聊的就是直接 給函數強制性綁定this

2 .3 .2 call apply bind

這裏咱們就要用到 js 給咱們提供的函數 call 和 apply,它們的做用都是改變函數的this指向第一個參數都是 設置this對象

兩個函數的區別:

  1. call從第二個參數開始全部的參數都是 原函數的參數。
  2. apply只接受兩個參數,且第二個參數必須是數組,這個數組表明原函數的參數列表。

例如:

function foo(a,b){
    console.log(a+b);
}
foo.call(null,'海洋','餅乾');        // 海洋餅乾  這裏this指向不重要就寫null了
foo.apply(null, ['海洋','餅乾'] );     // 海洋餅乾

除了 call,apply函數之外,還有一個改變this的函數 bind ,它和call,apply都不一樣。

bind只有一個函數,且不會馬上執行,只是將一個值綁定到函數的this上,並將綁定好的函數返回。例:

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

foo = foo.bind(obj);
foo();                    // 10

(bind函數很是特別,下次和你們一塊兒討論它的源碼)

2 .3 .2 顯性綁定

開始正題,上代碼,就用上面隱性綁定的例子 :

function foo(){
    console.log(this.a);
}
var obj = {
    a : 10            //去掉裏面的foo
}
foo.call(obj);        // 10

咱們將隱性綁定例子中的 上下文對象 裏的函數去掉了,顯然如今不能用 上下文.函數 這種形式來調用函數,你們看代碼裏的顯性綁定代碼foo.call(obj),看起來很怪,和咱們以前所瞭解的函數調用不同。

其實call 是 foo 上的一個函數,在改變this指向的同時執行這個函數。

(想要深刻理解 [call apply bind this硬綁定,軟綁定,箭頭函數綁定 ] 等更多黑科技 的小夥伴歡迎關注我或本文的評論,最近我會單獨作一期放到一塊兒寫一篇文章)(不想看的小夥伴不用擔憂,不影響對本文的理解

2 .4 new 綁定

2 .4 .1 什麼是 new

學過面向對象的小夥伴對new確定不陌生,js的new和傳統的面嚮對象語言的new的做用都是建立一個新的對象,可是他們的機制徹底不一樣。

建立一個新對象少不了一個概念,那就是構造函數,傳統的面向對象 構造函數 是類裏的一種特殊函數,要建立對象時使用new 類名()的形式去調用類中的構造函數,而js中就不同了。

js中的只要用new修飾的 函數就是'構造函數',準確來講是 函數的構造調用,由於在js中並不存在所謂的'構造函數'。

那麼用new 作到函數的構造調用後,js幫咱們作了什麼工做呢:

  1. 建立一個新對象。
  2. 把這個新對象的__proto__屬性指向 原函數的prototype屬性。(即繼承原函數的原型)
  3. 將這個新對象綁定到 此函數的this上
  4. 返回新對象,若是這個函數沒有返回其餘對象

第三條就是咱們下面要聊的new綁定

2 .4 .2 new 綁定

不嗶嗶,看代碼:

function foo(){
    this.a = 10;
    console.log(this);
}
foo();                    // window對象
console.log(window.a);    // 10   默認綁定

var obj = new foo();      // foo{ a : 10 }  建立的新對象的默認名爲函數名
                          // 而後等價於 foo { a : 10 };  var obj = foo;
console.log(obj.a);       // 10    new綁定

使用new調用函數後,函數會 以本身的名字 命名 和 建立 一個新的對象,並返回。

特別注意 : 若是原函數返回一個對象類型,那麼將沒法返回新對象,你將丟失綁定this的新對象,例:

function foo(){
    this.a = 10;
    return new String("搗蛋鬼");
}
var obj = new foo();
console.log(obj.a);       // undefined
console.log(obj);         // "搗蛋鬼"

2 .5 this綁定優先級

過程是些無聊的代碼測試,我直接寫出優先級了

new 綁定 > 顯示綁定 > 隱式綁定 > 默認綁定

3 . 總結

  1. 若是函數被new 修飾

    this綁定的是新建立的對象,例:var bar = new foo();  函數 foo 中的 this 就是一個叫foo的新建立的對象 , 而後將這個對象賦給bar , 這樣的綁定方式叫 new綁定 .
  2. 若是函數是使用call,apply,bind來調用的

    this綁定的是 call,apply,bind 的第一個參數.例: foo.call(obj); , foo 中的 this 就是 obj , 這樣的綁定方式叫 顯性綁定 .
  3. 若是函數是在某個 上下文對象 下被調用

    this綁定的是那個上下文對象,例 : var obj = { foo : foo };    obj.foo();  foo 中的 this 就是 obj . 這樣的綁定方式叫 隱性綁定 .
  4. 若是都不是,即便用默認綁定

    例:function foo(){...} foo() ,foo 中的 this 就是 window.(嚴格模式下默認綁定到undefined).
       這樣的綁定方式叫 默認綁定 .

4 . 面試題解析

1.

var x = 10;
var obj = {
    x: 20,
    f: function(){
        console.log(this.x);        // ?
        var foo = function(){ 
            console.log(this.x);    
            }
        foo();                      // ?
    }
};
obj.f();

-----------------------答案---------------------
答案 : 20 10
解析 :考點 1. this默認綁定 2. this隱性綁定

var x = 10;
var obj = {
    x: 20,
    f: function(){
        console.log(this.x);    // 20
                                // 典型的隱性綁定,這裏 f 的this指向上下文 obj ,即輸出 20
        function foo(){ 
            console.log(this.x); 
            }
        foo();       // 10
                     //有些人在這個地方就想固然的以爲 foo 在函數 f 裏,也在 f 裏執行,
                     //那 this 確定是指向obj 啊 , 仔細看看咱們說的this綁定規則 , 對應一下很容易
                     //發現這種'光桿司令',是咱們一開始就示範的默認綁定,這裏this綁定的是window
    }
};
obj.f();

2.

function foo(arg){
    this.a = arg;
    return this
};

var a = foo(1);
var b = foo(10);

console.log(a.a);    // ?
console.log(b.a);    // ?

-----------------------答案---------------------

答案 : undefined 10
解析 :考點 1. 全局污染 2. this默認綁定

這道題頗有意思,問題基本上都集中在第一undefined上,這實際上是題目的小陷阱,可是追棧的過程絕對精彩
讓咱們一步步分析這裏發生了什麼:

  1. foo(1)執行,應該不難看出是默認綁定吧 , this指向了window,函數裏等價於 window.a = 1,return window;
  2. var a = foo(1) 等價於 window.a = window , 不少人都忽略了var a 就是window.a ,將剛剛賦值的 1 替換掉了。
  3. 因此這裏的 a 的值是 window , a.a 也是window , 即window.a = window ; window.a.a = window;
  4. foo(10) 和第一次同樣,都是默認綁定,這個時候,將window.a 賦值成 10 ,注意這裏是關鍵,原來window.a = window ,如今被賦值成了10,變成了值類型,因此如今 a.a = undefined。(驗證這一點只須要將var b = foo(10);刪掉,這裏的 a.a 仍是window)
  5. var b = foo(10); 等價於 window.b = window;

本題中全部變量的值,a = window.a = 10 , a.a = undefined , b = window , b.a = window.a = 10;

3.

var x = 10;
var obj = {
    x: 20,
    f: function(){ console.log(this.x); }
};
var bar = obj.f;
var obj2 = {
    x: 30,
    f: obj.f
}
obj.f();
bar();
obj2.f();

-----------------------答案---------------------
答案:20 10 30
解析:傳說中的送分題,考點,辨別this綁定

var x = 10;
var obj = {
    x: 20,
    f: function(){ console.log(this.x); }
};
var bar = obj.f;
var obj2 = {
    x: 30,
    f: obj.f
}
obj.f();    // 20
            //有上下文,this爲obj,隱性綁定
bar();      // 10
            //'光桿司令' 默認綁定  ( obj.f 只是普通的賦值操做 )
obj2.f();   //30
            //無論 f 函數怎麼折騰,this只和 執行位置和方式有關,即咱們所說的綁定規則

4. 壓軸題了

function foo() {
    getName = function () { console.log (1); };
    return this;
}
foo.getName = function () { console.log(2);};
foo.prototype.getName = function () { console.log(3);};
var getName = function () { console.log(4);};
function getName () { console.log(5);}
 
foo.getName ();                // ?
getName ();                    // ?
foo().getName ();              // ?
getName ();                    // ?
new foo.getName ();            // ?
new foo().getName ();          // ?
new new foo().getName ();      // ?

-----------------------答案---------------------
答案:2 4 1 1 2 3 3
解析:考點 1. new綁定 2.隱性綁定 3. 默認綁定 4.變量污染

function foo() {
    getName = function () { console.log (1); }; 
            //這裏的getName 將建立到全局window上
    return this;
}
foo.getName = function () { console.log(2);};   
        //這個getName和上面的不一樣,是直接添加到foo上的
foo.prototype.getName = function () { console.log(3);}; 
        // 這個getName直接添加到foo的原型上,在用new建立新對象時將直接添加到新對象上 
var getName = function () { console.log(4);}; 
        // 和foo函數裏的getName同樣, 將建立到全局window上
function getName () { console.log(5);}    
        // 同上,可是這個函數不會被使用,由於函數聲明的提高優先級最高,因此上面的函數表達式將永遠替換
        // 這個同名函數,除非在函數表達式賦值前去調用getName(),可是在本題中,函數調用都在函數表達式
        // 以後,因此這個函數能夠忽略了
        
        // 經過上面對 getName的分析基本上答案已經出來了

foo.getName ();                // 2
                               // 下面爲了方便,我就使用輸出值來簡稱每一個getName函數
                               // 這裏有小夥伴疑惑是在 2 和 3 之間,以爲應該是3 , 但其實直接設置
                               // foo.prototype上的屬性,對當前這個對象的屬性是沒有影響的,若是要使
                               // 用的話,能夠foo.prototype.getName() 這樣調用 ,這裏須要知道的是
                               // 3 並不會覆蓋 2,二者不衝突 ( 當你使用new 建立對象時,這裏的
                               // Prototype 將自動綁定到新對象上,即用new 構造調用的第二個做用)
                               
getName ();                    // 4 
                               // 這裏涉及到函數提高的問題,不知道的小夥伴只須要知道 5 會被 4 覆蓋,
                               // 雖然 5 在 4 的下面,其實 js 並非徹底的自上而下,想要深刻了解的
                               // 小夥伴能夠看文章最後的連接
                               
foo().getName ();              // 1 
                               // 這裏的foo函數執行完成了兩件事, 1. 將window.getName設置爲1,
                               // 2. 返回window , 故等價於 window.getName(); 輸出 1
getName ();                    // 1
                               // 剛剛上面的函數剛把window.getName設置爲1,故同上 輸出 1
                               
new foo.getName ();            // 2
                               // new 對一個函數進行構造調用 , 即 foo.getName ,構造調用也是調用啊
                               // 該執行仍是執行,而後返回一個新對象,輸出 2 (雖然這裏沒有接收新
                               // 建立的對象可是咱們能夠猜到,是一個函數名爲 foo.getName 的對象
                               // 且__proto__屬性裏有一個getName函數,是上面設置的 3 函數)
                               
new foo().getName ();          // 3
                               // 這裏特別的地方就來了,new 是對一個函數進行構造調用,它直接找到了離它
                               // 最近的函數,foo(),並返回了應該新對象,等價於 var obj = new foo();
                               // obj.getName(); 這樣就很清晰了,輸出的是以前綁定到prototype上的
                               // 那個getName  3 ,由於使用new後會將函數的prototype繼承給 新對象
                               
new new foo().getName ();      // 3
                               // 哈哈,這個看上去很嚇人,讓咱們來分解一下:
                               // var obj = new foo();
                               // var obj1 = new obj.getName();
                               // 好了,仔細看看, 這不就是上兩題的合體嗎,obj 有getName 3, 即輸出3
                               // obj 是一個函數名爲 foo的對象,obj1是一個函數名爲obj.getName的對象

5 . 箭頭函數的this綁定 (2017.9.18更新)

箭頭函數,一種特殊的函數,不使用function關鍵字,而是使用=>,學名 胖箭頭(2333),它和普通函數的區別:

  1. 箭頭函數不使用咱們上面介紹的四種綁定,而是徹底根據外部做用域來決定this。(它的父級是使用咱們的規則的哦)
  2. 箭頭函數的this綁定沒法被修改 (這個特性很是爽(滑稽))

先看個代碼鞏固一下:

function foo(){
    return ()=>{
        console.log(this.a);
    }
}
foo.a = 10;

// 1. 箭頭函數關聯父級做用域this

var bar = foo();            // foo默認綁定
bar();                      // undefined 哈哈,是否是有小夥伴想固然了

var baz = foo.call(foo);    // foo 顯性綁定
baz();                      // 10 

// 2. 箭頭函數this不可修改
//這裏咱們使用上面的已經綁定了foo 的 baz
var obj = {
    a : 999
}
baz.call(obj);              // 10

來來來,實戰一下,還記得咱們以前第一個例子嗎,將它改爲箭頭函數的形式(能夠完全解決噁心的this綁定問題):

var people = {
    Name: "海洋餅乾",
    getName : function(){
        console.log(this.Name);
    }
};
var bar = people.getName;

bar();    // undefined

====================修改後====================

var people = {
    Name: "海洋餅乾",
    getName : function(){
        return ()=>{
            console.log(this.Name);
        }
    }
};
var bar = people.getName(); //得到一個永遠指向people的函數,不用想this了,豈不是美滋滋?

bar();    // 海洋餅乾

可能會有人不解爲何在箭頭函數外面再套一層,直接寫不就好了嗎,搞這麼麻煩幹嗎,其實這也是箭頭函數不少人用很差的地方

var obj= {
    that : this,
    bar : function(){
        return ()=>{
            console.log(this);
        }
    },
    baz : ()=>{
        console.log(this);
    }
}
console.log(obj.that);  // window
obj.bar()();            // obj
obj.baz();              // window
  1. 咱們先要搞清楚一點,obj的當前做用域是window,如 obj.that === window。
  2. 若是不用function(function有本身的函數做用域)將其包裹起來,那麼默認綁定的父級做用域就是window。
  3. 用function包裹的目的就是將箭頭函數綁定到當前的對象上。函數的做用域是當前這個對象,而後箭頭函數會自動綁定函數所在做用域的this,即obj。

美滋滋,溜了溜了




參考書籍:你不知道的JavaScript<上卷> KYLE SIMPSON 著 (推薦)

相關文章
相關標籤/搜索