揭開 this & that 之迷

新手在入門 JavaScript 的過程中,一定會踩很多關于 this 的坑,出現問題的本質就是 this 指針的指向和自己想的不一樣。筆者在入門學習的過程中,也踩了很多坑,于是便寫下本篇文章記錄自己“踩坑”歷程。

一. this 在哪里?

在上篇《從 JavaScript 作用域說開去》分析中,我們知道,在 Execution Context 中有一個屬性是 this,這里的 this 就是我們所說的 this 。this 與上下文中可執行代碼的類型有直接關系,** this 的值在進入執行上下文時確定,并且在執行上下文運行期間永久不變。**

this 到底取何值?this 的取值是動態的,是在函數真正被調用執行的時候確定的,函數定義的時候確定不了。因為 this 的取值是執行上下文環境的一部分,每次調用函數,都會產生一個新的執行上下文環境。

所以 this 的作用就是用來指明執行上下文是在哪個上下文中被觸發的對象。令人迷惑的地方就在這里,同一個函數,當在不同的上下文進行調用的時候,this 的值就可能會不同。也就是說,this 的值就是函數調用表達式(也就是函數被調用的方式)的 caller。

二. this & that 具體值得是誰?

目前接觸的有以下14種情況,筆者打算一一列舉出來,以后如果遇到了更多的情況,還會繼續增加。

既然 this 是執行上下文確定的,那么從執行上下文的種類進行分類,可以分為3種:

那么接下來我們就從 Global Execution Context 全局執行上下文,Function Execution Context 函數執行上下文,Eval Execution Context Eval執行上下文 這三類,具體談談 this 究竟指的是誰。

(一). 全局執行上下文

1. 非嚴格模式下的函數調用

這是函數的最通常用法,屬于全局性調用,因此 this 就代表全局對象 Global。


var name = 'halfrost';
function test() {
    console.log(this); // window
    console.log(this.name); // halfrost
}                                                                                                                                                                                                                                                                                                                              
test();


在全局上下文(Global Context)中,this 總是 global object,在瀏覽器中就是 window 對象。

2. 嚴格模式下的函數調用

嚴格模式由 ECMAScript 5.1 引進,用來限制 JavaScript 的一些異常處理,提供更好的安全性和更強壯的錯誤檢查機制。使用嚴格模式,只需要將 'use strict' 置于函數體的頂部。這樣就可以將上下文環境中的 this 轉為 undefined。這樣執行上下文環境不再是全局對象,與非嚴格模式剛好相反。

在嚴格模式下,情況并不是僅僅是 undefined 這么簡單,有可能嚴格模式夾雜著非嚴格模式。

先看嚴格模式的情況:


'use strict';
function test() {
  console.log(this); //undefined
};
test();

上面的這個情況比較好理解,還有一種情況也是嚴格模式下的:


    function execute() {  
      'use strict'; // 開啟嚴格模式
      function test() {
        // 內部函數也是嚴格模式
        console.log(this); // undefined
      }
      // 在嚴格模式下調用 test()
      // this 在 test() 下是 undefined
      test(); // undefined
    }
    execute();  

如果嚴格模式在外層,那么在執行作用域內部聲明的函數,它會繼承嚴格模式。

接下來就看看嚴格模式和非嚴格模式混合的情況。


    function nonStrict() {  
      // 非嚴格模式
      console.log(this); // window
    }
    function strict() {  
      'use strict';
      // 嚴格模式
      console.log(this); // undefined
    }



這種情況就比較簡單了,各個模式下分別判斷就可以了。

(二).函數執行上下文

3. 函數調用

當通過正常的方式調用一個函數的時候,this 的值就會被設置為 global object(瀏覽器中的 window 對象)。

嚴格模式和非嚴格模式的情況和上述全局執行上下文的情況一致,嚴格模式對應的 undefined ,非嚴格模式對應的 window 這里就不再贅述了。

4. 方法作為對象的屬性被調用


var person = {
    name: "halfrost",
    func: function () {
        console.log(this + ":" + this.name);
    }
};

person.func(); // halfrost

在這個例子里面的 this 調用的是函數的調用者 person,所以會輸出 person.name 。

當然如果函數的調用者是一個全局對象的話,那么這里的 this 指向又會發生變化。


var name = "YDZ";
var person = {
    name: "halfrost",
    func: function () {
        console.log(this + ":" + this.name);
    }
};

temp = person.func;
temp(); // YDZ


在上面這個例子里面,由于函數被賦值到了另一個變量中,并沒有作為 person 的一個屬性被調用,那么 this 的值就是 window。

上述現象其實可以描述為,“從一個類中提取方式時丟失了 this 對象”。針對這個現象可以再舉一個例子:


var counter = {
      count: 0,
      inc: function() {
          this.count ++;
      }
}


var func = counter.inc;
func();
counter.count;   // 輸出0,會發現func函數根本不起作用

這里我們雖然把 counter.inc 函數提取出來了,但是函數里面的 this 變成了全局對象了,所以 func() 函數執行的結果是 window.count++。然而 window.count 根本不存在,且值是 undefined,對 undefined 操作,得到的結果只能是 NaN。

驗證一下,我們打印全局的 count:


count  // 輸出是 NaN

那么這種情況我們應該如何解決呢?如果就是想提取出一個有用的方法給其他類使用呢?這個時候的正確做法是使用 bind 函數。


var func2 = counter.inc.bind(counter);
func2();
counter.count; // 輸出是1,函數生效了!

5. 構造函數的調用

所謂構造函數就是用來 new 對象的函數。嚴格的來說,所有的函數都可以 new 一個對象,但是有些函數的定義是為了 new 一個對象,而有些函數則不是。另外注意,構造函數的函數名第一個字母大寫(規則約定)。例如:Object、Array、Function等。



function person() {
    this.name = "halfrost";
    this.age = 18;
    console.log(this);
}

var ydz = new person();  // person {name: "halfrost", age: 18}
console.log(ydz.name, ydz.age); // halfrost 18

如果是構造函數被調用的話,this 其實指向的是 new 出來的那個對象。

如果不是被當做構造函數調用的話,情況有所區別:



function person() {
    this.name = "halfrost";
    this.age = 18;
    console.log(this);
}

person(); // Window {stop: function, open: function, alert: function, confirm: function, prompt: function…}

如果不是被當做構造函數調用的話,那就變成了普通函數調用的情況,那么這里的 this 就是 window。

構造函數里面如果還定義了 prototype,this 會指向什么呢?


function person() {
    this.name = "halfrost";
    this.age = 18;
    console.log(this);
}

person.prototype.getName = function() {
    console.log(this.name); // person {name: "halfrost", age: 18} "halfrost"
}

var ydz = new person();  // person {name: "halfrost", age: 18}
ydz.getName();

在 person.prototype.getName 函數中,this 指向的是 ydz 對象。因此可以通過 this.name 獲取 ydz.name 的值。

其實,不僅僅是構造函數的 prototype,即便是在整個原型鏈中,this 代表的也都是當前對象的值。

6. 內部函數 / 匿名函數 的調用

如果在一個對象的屬性是一個方法,這個方法里面又定義了內部函數和匿名函數,那么它們的 this 又是怎么樣的呢?



var context = "global";

var test = {  
    context: "inside",
    method: function () {  
        console.log(this + ":" +this.context);
        
        function f() {
            var context = "function";
            console.log(this + ":" +this.context); 
        };
        f(); 
        
        (function(){
            var context = "function";
            console.log(this + ":" +this.context); 
        })();
    }
};

test.method();

// [object Object]:object
// [object Window]:global
// [object Window]:global

從輸出可以看出,內部函數和匿名函數里面的 this 都是指向外面的 window。

7. call() / apply() / bind() 的方式調用

this 本身是不可變的,但是 JavaScript 中提供了 call() / apply() / bind() 三個函數來在函數調用時設置 this 的值。

這三個函數的原型如下:


// Sets obj1 as the value of this inside fun() and calls fun() passing elements of argsArray as its arguments.
fun.apply(obj1 [, argsArray])

// Sets obj1 as the value of this inside fun() and calls fun() passing arg1, arg2, arg3, ... as its arguments.
fun.call(obj1 [, arg1 [, arg2 [,arg3 [, ...]]]])

// Returns the reference to the function fun with this inside fun() bound to obj1 and parameters of fun bound to the parameters specified arg1, arg2, arg3, ....
fun.bind(obj1 [, arg1 [, arg2 [,arg3 [, ...]]]])


在這3個函數里面,this 都是對應的第一個參數。


    var rabbit = { name: 'White Rabbit' };  
    function concatName(string) {  
      console.log(this === rabbit); // => true
      return string + this.name;
    }
    // 間接調用
    concatName.call(rabbit, 'Hello ');  // => 'Hello White Rabbit'  
    concatName.apply(rabbit, ['Bye ']); // => 'Bye White Rabbit'  


apply() 和 call() 能夠強制改變函數執行時的當前對象,讓 this 指向其他對象。apply() 和 call() 的區別在于,apply() 的入參是一個數組,call() 的入參是一個參數列表。

apply() 和 call(),它倆都立即執行了函數,而 bind() 函數返回了一個新的函數,它允許創建預先設置好 this 的函數 ,并可以延后調用。


    function multiply(number) {  
      'use strict';
      return this * number;
    }
    // 創建綁定函數,綁定上下文2
    var double = multiply.bind(2);  
    // 調用間接調用
    double(3);  // => 6  
    double(10); // => 20


bind() 函數實質其實是實現了,原始綁定函數共享相同的代碼和作用域,但是在執行時擁有不同的上下文環境。

bind() 函數創建了一個永恒的上下文鏈并不可修改。一個綁定函數即使使用 call() 或者 apply()傳入其他不同的上下文環境,也不會更改它之前連接的上下文環境,重新綁定也不會起任何作用。

只有在構造器調用時,綁定函數可以改變上下文,然而這并不是特別推薦的做法。



    function getThis() {  
      'use strict';
      return this;
    }
    var one = getThis.bind(1);  
    // 綁定函數調用
    one(); // => 1  
    // 使用 .apply() 和 .call() 綁定函數
    one.call(2);  // => 1  
    one.apply(2); // => 1  
    // 重新綁定
    one.bind(2)(); // => 1  
    // 利用構造器方式調用綁定函數
    new one(); // => Object 


只有 new one() 時可以改變綁定函數的上下文環境,其他類型的調用結果是 this 永遠指向 1。

8. setTimeout、setInterval 中的 this

《 javascript 高級程序設計》中寫到:“超時調用的代碼需要調用 window 對象的 setTimeout 方法”。setTimeout/setInterval 執行的時候,this 默認指向 window 對象,除非手動改變 this 的指向。


var name = 'halfrost';
function Person(){
    this.name = 'YDZ';
    this.sayName=function(){
        console.log(this); // window
        console.log(this.name); // halfrost
        };
    setTimeout(this.sayName, 10);
    }
var person=new Person();

上面這個例子如果想改變 this 的指向,可是使用 apply/call 等,也可以使用 that 保存 this。

值得注意的是:setTimeout 中的回調函數在嚴格模式下也指向 window 而不是 undefined !


'use strict';
function test() {
  console.log(this);  //window
}
setTimeout(test, 0);

因為 setTimeout 的回調函數如果沒有指定的 this ,會做一個隱式的操作,將全局上下文注入進去,不管是在嚴格還是非嚴格模式下。

9. DOM event

當一個函數被當作event handler的時候,this會被設置為觸發事件的頁面元素(element)。


var body = document.getElementsByTagName("body")[0];
body.addEventListener("click", function(){
    console.log(this);
});
// <body>…</body>

10. in-line 的方式調用

當代碼通過 in-line handler 執行的時候,this 同樣指向擁有該
handler 的頁面元素。

看下面的代碼:


document.write('<button onclick="console.log(this)">Show this</button>');
// <button onclick="console.log(this)">Show this</button>
document.write('<button onclick="(function(){console.log(this);})()">Show this</button>');
// window

在第一行代碼中,正如上面 in-line handler 所描述的,this 將指向 "button" 這個 element。但是,對于第二行代碼中的匿名函數,是一個上下文無關(context-less)的函數,所以 this 會被默認的設置為
window。

前面我們已經介紹過了 bind 函數,所以,通過下面的修改就能改變上面例子中第二行代碼的行為:



document.write('<button onclick="((function(){console.log(this);}).bind(this))()">Show this</button>');
// <button onclick="((function(){console.log(this);}).bind(this))()">Show this</button>

11. this & that

在 JavaScript 中,經常會存在嵌套函數,這是因為函數可以作為參數,并可以在合適的時候通過函數表達式創建。這會引發一些問題,如果一個方法包含一個普通函數,而你又想在后者的內部訪問到前者,方法中的 this 會被普通函數的 this 覆蓋,比如下面的例子:


var person = {
       name: 'halfrost',
       friends: [ 'AA', 'BB'],
       loop: function() {
            'use strict';
             this.friends.forEach(
                 function(friend) {   // (1)
                    console.log(this.name + ' knows ' + friend);   // (2)
                 }
          );
      }
};


上述這個例子中,假設(1)處的函數想要在(2)這一行訪問到 loop 方法里面的 this,該怎么做呢?

如果直接去調用 loop 方法是不行的,會發現報了下面這個錯誤。


person.loop();
// Uncaught TypeError: Cannot read property 'name' of undefined

因為(1)處的函數擁有自己的 this,是沒有辦法在里面調用外面一層的 this 的。那怎么辦呢?

解決辦法有3種:

(1) that = this
我們可以把外層的 this 保存一份,一般會使用 that ,self,me,這些變量名暫存 this。


var person = {
       name: 'halfrost',
       friends: [ 'AA', 'BB'],
       loop: function() {
            'use strict';
             var that = this;
             this.friends.forEach(
                 function(friend) {   // (1)
                    console.log(that.name + ' knows ' + friend);   // (2)
                 }
          );
      }
};

person.loop();

// halfrost knows AA
// halfrost knows BB

這樣就可以正確的輸出想要的答案了。

(2) bind()

借助 bind() 函數,直接給回調函數的 this 綁定一個固定值,即函數的 this:


var person = {
       name: 'halfrost',
       friends: [ 'AA', 'BB'],
       loop: function() {
            'use strict';
             var that = this;
             this.friends.forEach(
                 function(friend) {   // (1)
                    console.log(this.name + ' knows ' + friend);   // (2)
                 }.bind(this)
          );
      }
};

person.loop();

// halfrost knows AA
// halfrost knows BB

(3) forEach() 的 thisValue

這種方法特定于 forEach()中,因為在這個方法的回調函數里面提供了第二個參數,我們可以利用這個參數,讓它來為我們提供 this:



var person = {
       name: 'halfrost',
       friends: [ 'AA', 'BB'],
       loop: function() {
            'use strict';
             var that = this;
             this.friends.forEach(
                 function(friend) {   // (1)
                    console.log(this.name + ' knows ' + friend);   // (2)
                 }, this );
      }
};

person.loop();

// halfrost knows AA
// halfrost knows BB

12. 箭頭函數

箭頭函數是 ES6 增加的新用法。


    var numbers = [1, 2];  
    (function() {  
      var get = () => {
        console.log(this === numbers); // => true
        return this;
      };
      console.log(this === numbers); // => true
      get(); // => [1, 2]
      // 箭頭函數使用 .apply() 和 .call()
      get.call([0]);  // => [1, 2]
      get.apply([0]); // => [1, 2]
      // Bind
      get.bind([0])(); // => [1, 2]
    }).call(numbers);


從上面的例子可以看出:

  1. 箭頭函數里面的 this 對象就是定義時候所在的對象,而不是使用時所在的對象。

  2. 箭頭函數不能被用來當做構造函數,于是也不能使用 new 命令。否則會報錯TypeError: get is not a constructor

    this 指向的固化,并不是因為箭頭函數內部有綁定 this 的機制,實際原因是箭頭函數根本就沒有自己的 this ,導致內部的 this 就是外層代碼塊的 this。正因為它沒有 this,所以也就不能作為構造函數了。

  3. 箭頭函數也不能使用 arguments 對象,因為 arguments 對象在箭頭函數體內不存在,如果要使用,可以用 rest 參數代替。同樣的,super,new.target 在箭頭函數里面也是不存在的。所以,arguments、super、new.target 這3個變量在箭頭函數里面都不存在。

  4. 箭頭函數里面也不能使用 yield 命令,因此箭頭函數也不能用作 Generator 函數。

  5. 由于箭頭函數沒有自己的 this ,當然就不能用 call()、apply()、bind() 這些方法去改變 this 的指向。

13. 函數綁定

雖然在 ES6 中引入了箭頭函數可以綁定 this 對象,大大減少了顯示綁定 this 對象的寫法(call、apply、bind)。鑒于箭頭函數有上述說到的4個缺點(不能當做構造函數,不能使用 arguments 對象,不能使用 yield 命令,不能使用call、apply、bind),所以在 ES7 中又提出了函數綁定運算符。用來取代 call、apply、bind 的調用。

函數綁定運算符是并排的雙冒號(::),雙冒號左邊是一個對象,右邊是一個函數。該運算符會自動的將左邊的對象作為上下文環境(即
this 對象)綁定到右邊的函數上。


foo::bar  // 等同于 bar.bind(foo)

foo::bar(...arguments) // 等同于 bar.apply(foo,arguments)


(三). Eval執行上下文

14. Eval 函數

Eval 函數比較特殊,this 指向就是當前作用域的對象。



var name = 'halfrost';
var person = {
    name: 'YDZ',
    getName: function(){
        eval("console.log(this.name)");
    }
}
person.getName();  // YDZ

var getName=person.getName;
getName();  // halfrost


這里的結果和方法作為對象的屬性被調用的結果是一樣的。

總結

如果要判斷一個運行中函數的 this 綁定,就需要找到這個函數的直接調用位置。找到之后
就可以順序應用下面這四條規則來判斷 this 的綁定對象。

  1. 函數是否在new中調用(new綁定)?如果是的話this綁定的是新創建的對象。
    var bar = new foo()
  2. 函數是否通過call、apply(顯式綁定)或者硬綁定調用?如果是的話,this綁定的是 指定的對象。
    var bar = foo.call(obj2)
  3. 函數是否在某個上下文對象中調用(隱式綁定)?如果是的話,this綁定的是那個上 下文對象。
    var bar = obj1.foo()
  4. 如果都不是的話,使用默認綁定。如果在嚴格模式下,就綁定到undefined,否則綁定到 全局對象。
    var bar = foo()

ES6 中的箭頭函數并不會使用四條標準的綁定規則,而是根據當前的詞法作用域來決定 this,具體來說,箭頭函數會繼承外層函數調用的 this 綁定(無論 this 綁定到什么)。這 其實和ES6之前代碼中的self = this機制一樣。


Reference:
《ECMAScript 6 Primer》
《javascript 高級程序設計》
JavaScript This 之謎(譯文)原文https://rainsoft.io/gentle-explanation-of-this-in-javascript/
《你不知道的JavaScript(上卷)》

GitHub Repo:Halfrost-Field

Follow: halfrost · GitHub

Source: https://halfrost.com/javascript_this/

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,238評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,430評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,134評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,893評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,653評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,136評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,212評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,372評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,888評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,738評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,939評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,482評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,179評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,588評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,829評論 1 283
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,610評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,916評論 2 372

推薦閱讀更多精彩內容

  • 1. this之謎 在JavaScript中,this是當前執行函數的上下文。因為JavaScript有4種不同的...
    百里少龍閱讀 1,015評論 0 3
  • 與其他語言相比,函數的this關鍵字在JavaScript中的表現略有不同,此外,在嚴格模式和非嚴格模式之間也會有...
    codingC閱讀 581評論 0 0
  • 函數參數的默認值 基本用法 在ES6之前,不能直接為函數的參數指定默認值,只能采用變通的方法。 上面代碼檢查函數l...
    呼呼哥閱讀 3,427評論 0 1
  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,781評論 18 139
  • 官方中文版原文鏈接 感謝社區中各位的大力支持,譯者再次奉上一點點福利:阿里云產品券,享受所有官網優惠,并抽取幸運大...
    HetfieldJoe閱讀 6,929評論 15 54