js中的this學習

在以前的對象原型的文章中,咱們講到了在函數前面加new而後進行調用以後發生的4件事情,當時只把跟原型有關的東西介紹了一下。如今咱們來學習一下其餘的內容。數組

首先先來回顧一下會有哪四件事情發生吧:app

  • 一個全新的對象被建立
  • 這個新的對象會被接入到原型鏈
  • 這個新的對象被設置爲函數調用的this綁定
  • 除非函數有返回對象,不然這個被new調用的函數將自動返回這個新的對象

這裏有個新的東西this綁定,也就是接下來咱們要介紹的東西啦。函數

第一個問題就是this是什麼?(如下回答摘自You-Dont-Know-JS)工具

this是在函數被調用的時候創建的一個綁定,指向的內容徹底由函數被調用的調用點來決定的。

簡單點說,this就是一個綁定,指向一個內容。學習

那麼this指向的內容又是什麼呢?前面說到這個內容由函數被調用的調用點來決定。所謂的調用點,就是函數在代碼中被調用的地方。也就是說,咱們須要找到函數在哪裏被調用,從而肯定this指向的內容。考慮這個問題還須要瞭解一個概念:調用棧(到達當前執行位置而被調用的全部方法的堆棧)。
看段代碼來深刻理解一下調用棧和調用點這兩個概念:this

function foo() {
    // 調用棧是: `foo`
    // 調用點是global scope(全局做用域)
    console.log( "foo" );
    bar(); // <-- `bar`的調用點
}
function bar() {
    // 調用棧是: `foo` -> `bar`
    // 調用點位於`foo`
    console.log( "bar" );
    baz(); // <-- `baz`的調用點
}
function baz() {
    // 調用棧是: `foo` -> `bar` -> `baz`
    // 調用點位於`bar`
    console.log( "baz" );
}
foo(); // <-- `foo`的調用點

上面這個代碼跟註釋應該已經很清楚瞭解釋了調用棧和調用點這兩個概念了。prototype

搞清楚這些概念以後,咱們仍是不知道this會指向什麼。既然說this指向的內容徹底由調用點決定,那麼調用點又是怎麼決定的呢?code

還記得文章最開始提到的東西麼,關於new的4件事情,第三點講的是新對象被設置爲函數調用的this綁定。
看下代碼:對象

function foo(){
      this.a = a;
  }
  var bar = new foo(2); //調用foo函數來建立一個新對象bar
  console.log(bar.a);

使用new來調用函數foo的時候,咱們建立了一個新對象bar而且把bar綁定到了foo()裏面的this.這就是所謂的new綁定。ip

那麼在JavaScript中,關於this綁定,除了new綁定,還有3種其它的規則:

  • 默認綁定
  • 隱式綁定
  • 顯示綁定

下面咱們依次來一一介紹。

  1. 默認綁定
    看名字咱們就能看出來,這是最普通最基礎的綁定。通常來講,獨立函數調用的時候this就是默認綁定。
    來看個例子:

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

    代碼很簡單,咱們主要關心的是this。咱們先看結果:this綁定到了全局變量。
    具體分析一下也很簡單,這裏的函數調用就是咱們日常在使用的最簡單的獨立函數的調用,跟前面介紹的規則也很符合。
    這裏有一個要注意的小細節就是若是是在嚴格模式下,默認綁定的值會變成undefined。若是是非嚴格模式的話,就是綁定到全局變量了。

  2. 隱式綁定
    這個規則通常是看函數調用的位置是否有上下文對象,或者說是否被某個對象擁有或者包含。
    經過代碼來深刻理解一下:

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

    代碼一樣很好理解,函數foo()做爲引用屬性添加在對象obj裏面,但這並不能說明函數foo()屬於obj對象。可是從調用的位置上看,會使用obj這個對象來引用函數foo,那麼函數在被調用的時候,是被obj這個對象擁有或者包含的。
    簡單點說,函數在被調用的時候,是經過對象來引用的,那麼函數裏的this就會綁定到這個對象上面。
    再來看一個稍微複雜一點的例子:

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

    這裏的話,咱們會發現多了一個obj1這個對象,並且這個對象裏有屬性a和對象obj。而後咱們調用的時候會發現結果輸出的是obj裏面的屬性a的值。
    簡單的結論就是,在多層的對象引用屬性中,只有最頂層或者說最後一層纔會影響調用位置。

  3. 顯式綁定
    經過上面隱式綁定的規則介紹能夠知道,它是經過對象間接綁定this的,那麼很明顯顯式綁定就是直接的,或者說就是強行指定咱們想要讓this綁定的對象。那麼怎麼來進行強行綁定呢?
    通常來講,是使用函數的call()和apply()方法(絕大部分函數都會有這兩個方法)。
    這兩個方法的做用都是同樣的,就是替換this指向。惟一不一樣的就是接收參數的方法不同。apply()方法接收兩個參數,第一個參數是一個對象(也就是咱們想要讓this指向的新的對象,不填的話就是全局對象),第二個參數一個參數數組。call()方法的話第一個參數跟apply是同樣的,可是後面要把傳遞的參數所有都列舉出來。
    簡單來看個例子:

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

    最後一行代碼,函數foo調用了call方法,強行把this綁定到了obj對象上。

至此,關於this綁定的基礎的4種規則就介紹得差很少了,實際上有些規則在應用的時候可能不那麼盡如人意,咱們依舊從代碼入手:

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

一開始咱們可能都會以爲輸出的結果應該是2。由於bar這個對象在建立的時候調用了obj裏面的foo函數。但實際上只是另外一個foo本身的引用。並且bar函數在調用的時候是做爲獨立函數調用的,跟咱們前面講的默認綁定的規則很符合,因此這裏的this就綁定到了全局對象。
這種狀況在回調函數裏更容易發生,好比下面的代碼:

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

最後一行代碼實際上就是f = obj.foo,天然結果就跟上面是同樣的。
那麼有什麼方法能夠解決這個問題呢?
在顯示綁定中,有一個它的變種,咱們稱之爲硬綁定,能夠解決上面的問題。
繼續看代碼:

function foo(){
     console.log(this.a);
 }
 var obj = {
    a:2
 };
 var bar = function(){
    foo.call(obj);
 }
 bar(); //2
 setTimeout(bar,1000);
 bar.call(window); //2

這段代碼解釋了硬綁定的工做原理:它建立了一個函數bar,而後在函數裏面經過foo.call(..)強行把this綁定到了obj對象上面。以後只要調用函數bar,就會調用函數foo,綁定的值始終不變。

而後咱們稍微改變一下,讓它變成一個可複用的幫助函數:

function foo(){
     console.log(this.a);
 }
 function bind(f,obj){
     return function(){
         return f.apply(obj,arguments);
     };
 }
 var obj = {
    a:2
 };
 var bar = bind(foo,obj);
 var b = bar(3);  
 console.log(b);  //2

因爲硬綁定常常被使用,因此它在ES5的時候就做爲內建工具了:Function.prototype.bind。上面的代碼就是bind方法的原理。
bind方法的做用和call和apply同樣,都是替換this指向,它的參數也和call同樣。不同的就是bind方法返回的是一個函數。

而後咱們要介紹一個比較特殊的函數,由於它不能根據前面介紹的4條規則來判斷this的指向。就是ES6中新增的函數:箭頭函數(=>)。它是根據外層做用域或者全局做用域來決定this指向的。
看段代碼:

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

foo()內部建立的箭頭函數會捕獲調用時foo()的this。由於foo使用了call方法,因此foo()的this綁定到了obj1。而後bar對象被建立的時候引用了箭頭函數,因此bar的this也被綁定到了obj1上面。並且箭頭函數的綁定是沒法被修改的。因此最後輸出的結果是1而不是2。

最後,雖然咱們已經瞭解了this綁定的基本規則,可是若是說咱們找到了函數在哪裏調用,而後又發現4種規則裏有多種規則能夠適用,那咱們應該選擇哪種呢?

這就涉及到了這些規則的優先級:

  1. 首先看是否是有new調用,若是是的話就綁定到新建立的對象;
  2. 而後看是否是有call或者apply或者bind調用,若是是那就綁定到指定對象;
  3. 再以後看是否是由上下文調用,若是是就綁定到那個上下文對象;
  4. 最後的話就只剩下默認綁定了(注意嚴格模式下是undefined,非嚴格模式下綁定到全局對象)。
相關文章
相關標籤/搜索