理解 JavaScript 中的 this 關鍵字

原文:Understanding the "this" keyword in JavaScriptjavascript

許多人被JavaScript中的this關鍵字給困擾住了,我想混亂的根源來自人們理所固然地認爲JavaScript中的this應該像Java中的this或Python中的self同樣工做。儘管JavaScript的this有時有相似的效果,但它跟Java或其餘語言中的this是徹底不一樣的。儘管有點難理解,但它的原理並不神祕。事實上,this遵循一些簡單的小規則,這篇文章就對這些規則做出瞭解釋。java

但首先,我想給出一些專業術語和一些JavaScript運行時環境的相關信息,但願能幫助你構建一個心智模型來更好的理解this數組

執行上下文

每一行JavaScript代碼都運行在一個「執行上下文」中。JavaScript運行時環境用一個棧維持着這些上下文,棧頂的一個就是當前正在運行的執行上下文。瀏覽器

有三類可執行代碼:全局代碼(global code)函數代碼(function code)eval代碼(eval code)。大概的說,全局代碼是應用程序最頂層的代碼,不被包含在任何方法中,函數代碼是在函數(function)體中的代碼,eval代碼是被eval解釋的全局代碼。app

this對象每次控制進入一個新的執行上下文時會被從新肯定指向,直到控制切換到一個不一樣的上下文。this的值取決於兩件事:被執行的代碼的類型(global,function,eval)和調用代碼的對象。ide

全局對象

全部的JavaScript運行時都有一個惟一的全局對象。他的屬性包括內置的對象如MathString,以及其餘由主環境變量定義的屬性。函數

在瀏覽器中,全局對象是window對象。在Node.js中,它就叫做「global object」。(我假定你將在瀏覽器中運行代碼,然而,我所說的一切也一樣適用於Node.js。)this

肯定this的值

第一條規則很簡單:this指向全局對象在全部全局代碼中。因爲全部的程序都是由執行全局代碼開始的,而且this在給定的執行上下文中會被修正,因此,默認的this指全局對象。lua

當控制進入一個新的執行上下文時發生了什麼呢?只有三種this的值發生改變的狀況:方法調用,new一個函數對象,函數被callapply調用。我將逐一解釋。excel

方法調用

當咱們經過點(例obj.foo())或者方括號(例obj["foo"])把一個方法做爲一個對象的屬性來調用時,this將指向方法體的父對象:

var counter = {
  val: 0,
  increment: function () {
    this.val += 1;
  }
};
counter.increment();
console.log(counter.val); // 1
counter['increment']();
console.log(counter.val); // 2

這是第二條規則:函數被看成父對象的屬性來調用時,在函數代碼中的this指向函數的父對象。

注意,若是咱們直接調用相同的方法,即,不做爲父對象的屬性,this將不會指向counter對象:

var inc = counter.increment;
inc();
console.log(counter.val); // 2
console.log(val); // NaN

inc被調用時,這裏的this不會改變,因此它仍是指向全局對象。

inc執行

this.val += 1;

它等效於執行:

window.val += 1;

window.val是undefined,且undefined1獲得NaN

new 運算符

任何JavaScript函數均可以經過new運算符當成構造函數使用。new運算符建立一個新對象而且設置函數中的this指向調用函數的新對象。舉個栗子:

function F (v) {
  this.val = v;
}
var f = new F("Woohoo!");
console.log(f.val); // Woohoo!
console.log(val); // ReferenceError

這就是咱們的第三條規則:在用new運算符調用的函數代碼中的this指向新建立的對象。

注意F沒有任何特殊。若是不經過new調用,this將指向全局對象:

var f = F("Oops!");
console.log(f.val); // undefined
console.log(val); // Oops!

在這個例子中,F("Oops!")是一個一般調用,this沒有指向新的對象,由於沒有用new建立新的對象,this繼續指向全局對象。

Call & Apply

全部JavaScript函數都有兩個方法,callapply,讓你可以調用函數而且清楚的設置this指向的對象。apply函數有兩個參數:一個是this指向的對象,一個(可選)傳進函數的參數的數組:

var add = function (x, y) {
      this.val = x + y;
    },
    obj = {
      val: 0
    };
add.apply(obj, [2, 8]);
console.log(obj.val); // 10

call方法和apply方法是徹底同樣的,只不過要逐個的傳遞參數,而不是用一個數組:

add.call(obj, 2, 8);
console.log(obj.val); // 10

這是第四條規則:當使用callapply調用函數時,函數代碼中的this被設置爲callapply中的第一個參數。

總結

  • 默認的,this指向全局對象
  • 當一個函數被做爲一個父對象的屬性調用時,函數中的this指向父對象
  • 當一個函數被new運算符調用時,函數中的this指向新建立的對象
  • 當使用callapply調用函數時,函數代碼中的this被設置爲callapply中的第一個參數。若是第一個參數是null或不是個對象,this指向全局對象。

若是你理解並接受了上面的4條規則,你就能明白this指的是什麼了。

補充:eval打破上面全部規則

Remember when I said that code evaluated inside eval is its own type of executable code? Well, that’s true, and it means that the rules for determining what this refers to inside of eval code are a little more complex.

As a first pass, you might think that this directly inside eval refers to the same object as it does in eval’s caller’s context. For example:

var obj = {
  val: 0,
  func: function() { 
    eval("console.log(this.val)");
  }
};
obj.func(); // 0

That works likely as you expect it to. However, there are many cases with eval where this will probably not work as you expect:

eval.call({val: 0}, "console.log(this.val)"); // depends on browser

The output of the above code depends on your browser. If your JavaScript runtime implements ECMAScript 5, this will refer to the global object and the above should print undefined, because it’s an 「indirect」 call of eval. That’s what the latest versions of Chrome and Firefox do. Safari 5.1 actually throws an error (「The ‘this’ value passed to eval must be the global object from which eval originated」), which is kosher according to ECMAScript 3.

If you want know how this works with eval, you should read Juriy Zaytsev’s excellent, 「Global eval. What are the options?」 You’ll learn more about eval, execution contexts, and direct vs. indirect calls than you probably ever wanted to know.

In general, you should just avoid using eval, in which case the simple rules about this given previously will always hold true.

相關文章
相關標籤/搜索