理解 JavaScript 中的 this

前言

理解this是咱們要深刻理解 JavaScript 中必不可少的一個步驟,同時只有理解了 this,你才能更加清晰地寫出與本身預期一致的 JavaScript 代碼。javascript

本文是這系列的第三篇,往期文章:java

  1. 理解 JavaScript 中的做用域
  2. 理解 JavaScript 中的閉包

什麼是 this

消除誤解

在解釋什麼是this以前,須要先糾正大部分人對this的誤解,常見的誤解有:數組

  1. 指向函數自身。
  2. 指向它所在的做用域。

關於爲什麼會誤解的緣由這裏很少講,這裏只給出結論,有興趣能夠自行查詢資料。瀏覽器

this 在任何狀況下都不指向函數的詞法做用域。你不能使用 this 來引用一個詞法做用域內部的東西。閉包

this 究竟是什麼

排除了一些錯誤理解以後,咱們來看看 this究竟是一種什麼樣的機制。app

this是在運行時(runtime)進行綁定的,而不是在編寫時綁定的,它的上下文(對象)取決於函數調用時的各類條件。this的綁定和函數聲明的位置沒有任何關係,只取決於函數的調用方式函數

當一個函數被調用時,會建立一個活動記錄(有時候也稱爲執行上下文)。這個記錄會包含函數在哪裏被調用(調用棧)、函數的調用方法、傳入的參數等信息。this就是記錄的其中一個屬性,會在函數執行的過程當中用到。(PS:因此this並不等價於執行上下文)工具

this 全面解析

前面 咱們排除了一些對於 this的錯誤理解而且明白了每一個函數的this是在調用時被綁定的,徹底取決於函數的調用位置。post

調用位置

一般來講,尋找調用位置就是尋找「函數被調用的位置「,其中最重要的是要分析調用棧(就是爲了到達當前執行位置所調用的全部函數)。咱們關心的調用位置就在當前正在執行的函數的前一個調用中。ui

下面咱們來看看到底什麼是調用棧和調用位置:

function foo(){
    // 當前調用棧是:foo
    // 所以,當前調用位置是全局做用域
    console.log("foo");
    bar(); // <-- bar的調用位置
}
function bar(){
    // 當前調用棧是foo -> bar
    console.log("bar");
}
foo(); // <-- foo 的調用位置
複製代碼

你能夠把調用棧想象成一個函數調用鏈, 就像咱們在前面代碼段的註釋中所寫的同樣。可是這種方法很是麻煩而且容易出錯。 另外一個查看調用棧的方法是使用瀏覽器的調試工具。 絕大多數現代桌面瀏覽器都內置了開發者工具,其中包含 JavaScript 調試器。

綁定規則

在找到調用位置後,則須要斷定代碼屬於下面四種綁定規則中的哪種,而後才能對this進行綁定。 注意: this綁定的是上下文對象,並非函數自身也不是函數的詞法做用域

默認綁定

這是最多見的函數調用類型:獨立函數調用

對函數直接使用而不帶任何修飾的函數引用進行調用,簡單點一個函數直接是func()這樣調用,不一樣於經過對象屬性調用例如obj.func(),也沒有經過new關鍵字new Function(),也沒有經過applycallbind強制改變this指向。

當被用做獨立函數調用時(不論這個函數在哪被調用,無論全局仍是其餘函數內),this默認指向到Window。(注意:在嚴格模式下this再也不默認指向全局,而是undefined)。

示例代碼:

function foo(){
    console.log(this.name);
}
var name = "window";
foo(); // window
複製代碼

隱式綁定

函數被某個對象擁有或者包含,也就是函數被做爲對象的屬性所引用,例如obj.func(),此時this會綁定到該對象上,這就是隱式綁定。

示例代碼:

var obj = {
    name : "obj",
    foo : function(){
        console.log(this.name);
    }
}
obj.foo(); // obj
複製代碼

隱式丟失

大部分的this綁定問題就是被「隱式綁定」的函數會丟失綁定對象,也就是說它會應用「默認綁定」,從而把this綁定到Windowundefined上,這取決因而否是嚴格模式。

最多見的狀況就是把對象方法做爲回調函數進行傳遞時:

var obj = {
    name : "obj",
    foo : function(){
        console.log(this.name);
    }
}
var name = "window";
setTimeout(obj.foo,1000); // 一秒後輸出 window
複製代碼

顯式綁定

咱們能夠經過applycallbind方法來顯示地修改this的指向。

關於這三個方法的定義(它們第一個參數都是接受this的綁定對象):

  1. apply:調用函數,第二個參數傳入一個參數數組。
  2. call:調用函數,其他參數正常傳遞。
  3. bind:返回一個已經綁定this的函數,其他參數正常傳遞。

好比咱們可使用bind方法解決上一節「隱式丟失」中的例子:

var obj = {
    name : "obj",
    foo : function(){
        console.log(this.name);
    }
}
var name = "window";
setTimeout(obj.foo.bind(obj),1000); // 一秒後輸出 obj
複製代碼

new 綁定

使用 new 來調用函數,或者說發生構造函數調用時,會自動執行下面的操做:

  1. 建立(或者說構造)一個全新的對象。
  2. 這個新對象會被執行[[原型]]鏈接。
  3. 這個新對象會綁定到函數調用的this
  4. 若是函數沒有返回其餘對象,那麼new表達式中的函數調用會自動返回這個新對象。

示例代碼:

function foo(a) { 
  this.a = a;
}
var bar = new foo(2); 
console.log( bar.a ); // 2
複製代碼

優先級

直接上結論:

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

判斷this: 如今咱們能夠根據優先級來判斷函數在某個調用位置應用的是哪條規則。能夠按照下面的順序來進行判斷:

  1. 使用new綁定,this綁定的是新建立的對象。

    var bar = new foo();
    複製代碼
  2. 經過call之類的顯式綁定,this綁定的是指定的對象。

    var bar = foo.call(obj2);
    複製代碼
  3. 在某個上下文對象中調用(隱式綁定),this 綁定的是那個上下文對象。

    var bar = obj1.foo();
    複製代碼
  4. 若是都不是的話,使用默認綁定。this綁定到Windowundefined上,這取決因而否是嚴格模式。

    var bar = foo();
    複製代碼

    對於正常的函數調用來講,理解了這些知識你就能夠明白 this 的綁定原理了。

this詞法

ES6 中介紹了一種沒法使用上面四條規則的特殊函數類型:箭頭函數

箭頭函數不使用 this 的四種標準規則,而是根據外層(函數或者全局)做用域來決定 this。(而傳統的this與函數做用域沒有任何關係,它只與調用位置的上下文對象有關)。

重要:

  • 箭頭函數最經常使用於回調函數中,例如事件處理器或者定時器.
  • 箭頭函數能夠像bind 同樣確保函數的this被綁定到指定對象
  • 箭頭函數用更常見的詞法做用域取代了傳統的this機制。

示例代碼:

var obj = {
    name : "obj",
    foo : function(){
        setTimeout(()=>{
            console.log(console.log(this.name)); // obj
        },1000);
    }
}
obj.foo();
複製代碼

這在 ES6 以前是這樣解決的:

var obj = {
    name : "obj",
    foo : function(){
        var self = this;
        setTimeout(function(){
            console.log(console.log(self.name)); // obj
        },1000);
    }
}
obj.foo();
複製代碼

總結

總之若是要判斷一個運行中函數的this綁定,就須要找到這個函數的直接調用位置。找到以後就能夠順序應用下面這四條規則來判斷this的綁定對象。

  1. 由new調用?綁定到新建立的對象。
  2. 由call或者apply(或者bind)調用?綁定到指定的對象。
  3. 由上下文對象調用?綁定到那個上下文對象。
  4. 默認:在嚴格模式下綁定到undefined,不然綁定到全局對象。

ES6 中的箭頭函數並不會使用四條標準的綁定規則,而是根據當前的詞法做用域來決定 this,具體來講,箭頭函數會繼承外層函數調用的 this綁定(不管 this綁定到什麼)。這其實和 ES6 以前代碼中的 self = this 機制同樣。

注:此文爲原創文章,如需轉載,請註明出處。

相關文章
相關標籤/搜索