深刻淺出 JavaScript 中的 this

     JavaScript 是一種腳本語言,所以被不少人認爲是簡單易學的。然而狀況偏偏相反,JavaScript 支持函數式編程、閉包、基於原型的繼承等高級功能。本文僅採擷其中的一例:JavaScript 中的 this 關鍵字,深刻淺出的分析其在不一樣狀況下的含義,造成這種狀況的緣由以及 Dojo 等 JavaScript 工具中提供的綁定 this 的方法。能夠這樣說,正確掌握了 JavaScript 中的 this 關鍵字,纔算邁入了 JavaScript 這門語言的門檻。 javascript

    在 Java 等面向對象的語言中,this 關鍵字的含義是明確且具體的,即指代當前對象。通常在編譯期肯定下來,或稱爲編譯期綁定。而在 JavaScript 中,this 是動態綁定,或稱爲運行期綁定的,這就致使 JavaScript 中的 this 關鍵字有能力具有多重含義,帶來靈活性的同時,也爲初學者帶來很多困惑。本文僅就這一問題展開討論,閱罷本文,讀者若能正確回答 JavaScript 中的 What ’s this 問題,做爲做者,我就會以爲花費這麼多功夫,撰寫這樣一篇文章是值得的。
java

Java 語言中的 this

  在 Java 中定義類常常會使用 this 關鍵字,多數狀況下是爲了不命名衝突,好比在下面例子的中,定義一個 Point 類,很天然的,你們會使用 x,y 爲其屬性或成員變量命名,在構造函數中,使用 x,y 爲參數命名,相比其餘的名字,好比 a,b,也更有意義。這時候就須要使用 this 來避免命名上的衝突。另外一種狀況是爲了方便的調用其餘構造函數,好比定義在 x 軸上的點,其 x 值默認爲 0,使用時只要提供 y 值就能夠了,咱們能夠爲此定義一個只需傳入一個參數的構造函數。不管哪一種狀況,this 的含義是同樣的,均指當前對象。 程序員

清單 1. Point.java


public class Point { 
    private int x = 0; 
    private int y = 0; 
    
    public Point(x, y){ 
        this.x = x; 
        this.y = y; 
    } 
    
    public Point(y){ 
        this(0, y); 
    } 
 }

JavaScript 語言中的 this

  因爲其運行期綁定的特性,JavaScript 中的 this 含義要豐富得多,它能夠是全局對象、當前對象或者任意對象,這徹底取決於函數的調用方式。JavaScript 中函數的調用有如下幾種方式:做爲對象方法調用,做爲函數調用,做爲構造函數調用,和使用 apply 或 call 調用。下面咱們將按照調用方式的不一樣,分別討論 this 的含義。 編程

做爲對象方法調用

  在 JavaScript 中,函數也是對象,所以函數能夠做爲一個對象的屬性,此時該函數被稱爲該對象的方法,在使用這種調用方式時,this 被天然綁定到該對象。 數組

清單 2. point.js 瀏覽器

var point = { 
 x : 0, 
 y : 0, 
 moveTo : function(x, y) { 
     this.x = this.x + x; 
     this.y = this.y + y; 
     } 
 }; 

 point.moveTo(1, 1)//this 綁定到當前對象,即 point 對象

做爲函數調用

  函數也能夠直接被調用,此時 this 綁定到全局對象。在瀏覽器中,window 就是該全局對象。好比下面的例子:函數被調用時,this 被綁定到全局對象,接下來執行賦值語句,至關於隱式的聲明瞭一個全局變量,這顯然不是調用者但願的。 閉包

function makeNoSense(x) { 
 this.x = x; 
 } 

 makeNoSense(5); 
 x;// x 已經成爲一個值爲 5 的全局變量
對於內部函數,即聲明在另一個函數體內的函數,這種綁定到全局對象的方式會產生另一個問題。咱們仍然之前面提到的 point 對象爲例,此次咱們但願在 moveTo 方法內定義兩個函數,分別將 x,y 座標進行平移。結果可能出乎你們意料,不只 point 對象沒有移動,反而多出兩個全局變量 x,y。

清單 4. point.js app

var point = { 
 x : 0, 
 y : 0, 
 moveTo : function(x, y) { 
     // 內部函數
     var moveX = function(x) { 
     this.x = x;//this 綁定到了哪裏?
    }; 
    // 內部函數
    var moveY = function(y) { 
    this.y = y;//this 綁定到了哪裏?
    }; 

    moveX(x); 
    moveY(y); 
    } 
 }; 
 point.moveTo(1, 1); 
 point.x; //==>0 
 point.y; //==>0 
 x; //==>1 
 y; //==>1
這屬於 JavaScript 的設計缺陷,正確的設計方式是內部函數的 this 應該綁定到其外層函數對應的對象上,爲了規避這一設計缺陷,聰明的 JavaScript 程序員想出了變量替代的方法,約定俗成,該變量通常被命名爲 that。

清單 5. point2.js 框架

var point = { 
 x : 0, 
 y : 0, 
 moveTo : function(x, y) { 
      var that = this; 
     // 內部函數
     var moveX = function(x) { 
     that.x = x; 
     }; 
     // 內部函數
     var moveY = function(y) { 
     that.y = y; 
     } 
     moveX(x); 
     moveY(y); 
     } 
 }; 
 point.moveTo(1, 1); 
 point.x; //==>1 
 point.y; //==>1

做爲構造函數調用

  JavaScript 支持面向對象式編程,與主流的面向對象式編程語言不一樣,JavaScript 並無類(class)的概念,而是使用基於原型(prototype)的繼承方式。相應的,JavaScript 中的構造函數也很特殊,若是不使用 new 調用,則和普通函數同樣。做爲又一項約定俗成的準則,構造函數以大寫字母開頭,提醒調用者使用正確的方式調用。若是調用正確,this 綁定到新建立的對象上。 編程語言

清單 6. Point.js
function Point(x, y){ 
    this.x = x; 
    this.y = y; 
 }

使用 apply 或 call 調用

  讓咱們再一次重申,在 JavaScript 中函數也是對象,對象則有方法,apply 和 call 就是函數對象的方法。這兩個方法異常強大,他們容許切換函數執行的上下文環境(context),即 this 綁定的對象。不少 JavaScript 中的技巧以及類庫都用到了該方法。讓咱們看一個具體的例子:

清單 7. Point2.js

function Point(x, y){ this.x = x; this.y = y; this.moveTo = function(x, y){ this.x = x; this.y = y; } } var p1 = new Point(0, 0); var p2 = {x: 0, y: 0}; p1.moveTo(1, 1); p1.moveTo.apply(p2, [10, 10]); 

在上面的例子中,咱們使用構造函數生成了一個對象 p1,該對象同時具備 moveTo 方法;使用對象字面量建立了另外一個對象 p2,咱們看到使用 apply 能夠將 p1 的方法應用到 p2 上,這時候 this 也被綁定到對象 p2 上。另外一個方法 call 也具有一樣功能,不一樣的是最後的參數不是做爲一個數組統一傳入,而是分開傳入的。

換個角度理解

  若是像做者同樣,你們也以爲上述四種方式不方便記憶,過一段時間後,又搞不明白 this 究竟指什麼。那麼我向你們推薦 Yehuda Katz 的這篇文章:Understanding JavaScript Function Invocation and 「this」。在這篇文章裏,Yehuda Katz 將 apply 或 call 方式做爲函數調用的基本方式,其餘幾種方式都是在這一基礎上的演變,或稱之爲語法糖。Yehuda Katz 強調了函數調用時 this 綁定的過程,無論函數以何種方式調用,均需完成這一綁定過程,不一樣的是,做爲函數調用時,this 綁定到全局對象;做爲方法調用時,this 綁定到該方法所屬的對象。

結束?

  經過上面的描述,若是你們已經能明確區分各類狀況下 this 的含義,這篇文章的目標就已經完成了。若是你們的好奇心再強一點,想知道爲何 this 在 JavaScript 中的含義如此豐富,那就得繼續閱讀下面的內容了。做者須要提早告知你們,下面的內容會比前面稍顯枯燥,若是隻想明白 this 的含義,閱讀到此已經足夠了。若是你們不嫌枯燥,非要探尋其中究竟,那就一塊兒邁入下一節吧。

函數的執行環境

  JavaScript 中的函數既能夠被看成普通函數執行,也能夠做爲對象的方法執行,這是致使 this 含義如此豐富的主要緣由。一個函數被執行時,會建立一個執行環境(ExecutionContext),函數的全部的行爲均發生在此執行環境中,構建該執行環境時,JavaScript 首先會建立 arguments變量,其中包含調用函數時傳入的參數。接下來建立做用域鏈。而後初始化變量,首先初始化函數的形參表,值爲 arguments變量中對應的值,若是 arguments變量中沒有對應值,則該形參初始化爲 undefined。若是該函數中含有內部函數,則初始化這些內部函數。若是沒有,繼續初始化該函數內定義的局部變量,須要注意的是此時這些變量初始化爲 undefined,其賦值操做在執行環境(ExecutionContext)建立成功後,函數執行時纔會執行,這點對於咱們理解 JavaScript 中的變量做用域很是重要,鑑於篇幅,咱們先不在這裏討論這個話題。最後爲 this變量賦值,如前所述,會根據函數調用方式的不一樣,賦給 this全局對象,當前對象等。至此函數的執行環境(ExecutionContext)建立成功,函數開始逐行執行,所需變量均從以前構建好的執行環境(ExecutionContext)中讀取。

Function.bind

  有了前面對於函數執行環境的描述,咱們來看看 this 在 JavaScript 中常常被誤用的一種狀況:回調函數。JavaScript 支持函數式編程,函數屬於一級對象,能夠做爲參數被傳遞。請看下面的例子 myObject.handler 做爲回調函數,會在 onclick 事件被觸發時調用,但此時,該函數已經在另一個執行環境(ExecutionContext)中執行了,this 天然也不會綁定到 myObject 對象上。

清單 8. callback.js
button.onclick = myObject.handler;

這是 JavaScript 新手們常常犯的一個錯誤,爲了不這種錯誤,許多 JavaScript 框架都提供了手動綁定 this 的方法。好比 Dojo 就提供了 lang.hitch,該方法接受一個對象和函數做爲參數,返回一個新函數,執行時 this 綁定到傳入的對象上。使用 Dojo,能夠將上面的例子改成:

清單 9. Callback2.js

button.onclick = lang.hitch(myObject, myObject.handler);

在新版的 JavaScript 中,已經提供了內置的 bind 方法供你們使用。

  eval 方法

  JavaScript 中的 eval 方法能夠將字符串轉換爲 JavaScript 代碼,使用 eval 方法時,this 指向哪裏呢?答案很簡單,看誰在調用 eval 方法,調用者的執行環境(ExecutionContext)中的 this 就被 eval 方法繼承下來了。

  結束語

  本文介紹了 JavaScript 中的 this 關鍵字在各類狀況下的含義,雖然這只是 JavaScript 中一個很小的概念,但藉此咱們能夠深刻了解 JavaScript 中函數的執行環境,而這是理解閉包等其餘概念的基礎。掌握了這些概念,才能充分發揮 JavaScript 的特色,纔會發現 JavaScript 語言特性的強大。

相關文章
相關標籤/搜索