再也不用擔心javascript的this---從代碼去理解(第一篇)---深析this原理

介紹

在這篇文章裏,我們將討論跟執行上下文直接相關的更多細節。討論的主題就是this關鍵字。
(如果在本篇有不懂的地方,一定要看第二篇)

定義

this是執行上下文中的一個屬性:

activeExecutionContext = {
  VO: {...},
  this: thisValue
};

這裏VO是我們在執行上下文討論的變量對象。

this與上下文中可執行代碼的類型直接相關。this的值在進入上下文時確定,並且在上下文運行代碼期間不會改變this的值。

this在全局代碼中的值

在全局代碼中,this始終是全局對象本身。這樣就有可能間接的引用到它了。

// explicit property definition of
// the global object
this.a = 10; // global.a = 10
alert(a); // 10

// implicit definition via assigning
// to unqualified identifier
b = 20;
alert(this.b); // 20

// also implicit via variable declaration
// because variable object of the global context
// is the global object itself
var c = 30;
alert(this.c); // 30

this在函數代碼中的值

在這種類型的代碼中,this值的首要(也許是最主要的)特點是它沒有靜態綁定到一個函數。

正如我們上面曾提到的那樣,this的值在進入上下文時確定,在函數代碼中,this的值每一次(進入上下文時)可能完全不同。

不管怎樣,在代碼運行期間,this的值是不變的,也就是說,因爲this不是一個變量,所以不可能爲其分配一個新值。

var foo = {x: 10};

var bar = {
  x: 20,
  test: function () {

    alert(this === bar); // true
    alert(this.x); // 20

    this = foo; // error

    alert(this.x); // if there wasn't an error then 20, not 10

  }

};

// on entering the context this value is
// determined as "bar" object; why so - will
// be discussed below in detail

bar.test(); // true, 20

foo.test = bar.test;

// however here this value will now refer
// to "foo" – even though we're calling the same function

foo.test(); // false, 10

那麼,在函數代碼中,什麼影響了this的值發生變化?

The value of this in a function context is provided by the caller and determined by the current form of a call expression (how the function call is written syntactically).

中文的意思就是:在函數上下文中this的值是由函數的調用者以及這個調用者的語法形式決定的。(即這個調用者是怎麼書寫的)

在有些javascript書籍中它們聲稱:“this的值取決於函數如何定義,如果它是全局函數,this設置爲全局對象,如果函數是一個對象的方法,this將總是指向這個對象。–——這絕對不正確。
如:

function foo() {
  alert(this);
}

foo(); // global

alert(foo === foo.prototype.constructor); // true

// but with another form of the call expression
// of the same function, this value is different

foo.prototype.constructor(); // foo.prototype

有時可能將函數作爲某些對象的一個方法來調用,此時this的值不會設置爲這個對象。

var foo = {
  bar: function () {
    alert(this);
    alert(this === foo);
  }
};

foo.bar(); // foo, true

var exampleFunc = foo.bar;

alert(exampleFunc === foo.bar); // true

// again with another form of the call expression
// of the same function, we have different this value

exampleFunc(); // global, false

那麼,到底調用函數的方式如何影響this的值?爲了充分理解this的值是如何確定的,我們需要詳細分析一個內部類型(internal type)——引用類型(Reference type)。

引用類型

用僞代碼可以把引用類型表示爲擁有兩個屬性的對象——base(即擁有屬性的那個對象),和這個屬性名。

var valueOfReferenceType = {
  base: <base object>,
  propertyName: <property name>
};

引用類型的值僅存在於兩種情況中:

  1. 當我們處理一個標示符時;(when we deal with an identifier;)
  2. 或一個屬性訪問器;(or with a property accessor.)

標示符的處理過程將在其它章節中討論,在這裏我們只需要知道,使用這種處理方式的返回值總是一個引用類型的值。

標識符是變量名,函數名,函數參數名和全局對象中未識別的屬性名。例如,下面標識符的值:

var foo = 10;
function bar() {}

在操作的中間結果中,引用類型對應的值如下:

var fooReference = {
  base: global,
  propertyName: 'foo'
};

var barReference = {
  base: global,
  propertyName: 'bar'
};

爲了從引用類型中得到一個對象真正的值,在僞代碼中可以用GetValue方法:

function GetValue(value) {

  if (Type(value) != Reference) {
    return value;
  }

  var base = GetBase(value);

  if (base === null) {
    throw new ReferenceError;
  }

  return base.[[Get]](GetPropertyName(value));

}

內部的[[Get]]方法返回對象屬性真正的值,

GetValue(fooReference); // 10
GetValue(barReference); // function object "bar"

屬性訪問器都應該熟悉。它有兩種變體:點(.)語法(此時屬性名是正確的標示符,且事先知道),或括號語法([])

foo.bar();
foo['bar']();

在計算中間的返回值中,引用類型對應的值如下:

var fooBarReference = {
  base: foo,
  propertyName: 'bar'
};

GetValue(fooBarReference); // function object "bar"

那麼,引用類型的值與函數上下文中的this的值是如何關聯起來的呢?這個關聯的過程是這篇文章的核心。函數上下文中確定this的值的通用規則如下:

在一個函數上下文中,this的值由調用者提供,且由調用者的語法形式決定。如果調用括號()的左邊是引用類型的值,this將設爲這個引用類型值的base對象,在其他情況下(與引用類型不同的任何其它屬性),this的值都爲null。不過,實際不存在this的值爲null的情況,因爲當this的值爲null的時候,其值會被隱式轉換爲全局對象。

下面讓我們看個例子:

function foo() {
  return this;
}

foo(); // global

我們看到在調用括號的左邊是一個引用類型值(因爲foo是一個標示符):

var fooReference = {
  base: global,
  propertyName: 'foo'
};

相應地,this也設置爲引用類型的base對象。即全局對象。

同樣,使用屬性訪問器:

var foo = {
  bar: function () {
    return this;
  }
};

foo.bar(); // foo

同樣,我們擁有一個引用類型的值,其base是foo對象,在函數bar調用時將base設置給this。

var fooBarReference = {
  base: foo,
  propertyName: 'bar'
};

但是,如果用另一種方式調用相同的函數,this的值將不同。

var test = foo.bar;
test(); // global

因爲test作爲標識符,產生了其他引用類型的值,該值的base(全局對象)被設置爲this的值。

var testReference = {
  base: global,
  propertyName: 'test'
};

現在,我們可以很明確的說明,爲什麼用不同的形式激活同一個函數會產生不同的this,答案在於不同的引用類型(type Reference)的中間值。

function foo() {
  alert(this);
}

foo(); // global, because

var fooReference = {
  base: global,
  propertyName: 'foo'
};

alert(foo === foo.prototype.constructor); // true

// another form of the call expression

foo.prototype.constructor(); // foo.prototype, because

var fooPrototypeConstructorReference = {
  base: foo.prototype,
  propertyName: 'constructor'
};

另一個通過調用方式動態確定this的值的經典例子:

function foo() {
  alert(this.bar);
}

var x = {bar: 10};
var y = {bar: 20};

x.test = foo;
y.test = foo;

x.test(); // 10
y.test(); // 20

函數調用和非引用類型

那麼,正如我們已經指出,當調用括號的左邊不是引用類型而是其它類型,this的值自動設置爲null,實際最終this的值被隱式轉換爲全局對象。

讓我們思考下面這種函數表達式:

(function  () {
  alert(this); // null => global
})();

在這個例子中,我們有一個函數對象但不是引用類型的對象(因爲它不是標示符,也不是屬性訪問器),相應地,this的值最終被設爲全局對象。

更多複雜的例子:

var foo = {
  bar: function () {
    alert(this);
  }
};

foo.bar(); // Reference, OK => foo
(foo.bar)(); // Reference, OK => foo

(foo.bar = foo.bar)(); // global
(false || foo.bar)(); // global
(foo.bar, foo.bar)(); // global

那麼,爲什麼我們有一個屬性訪問器,它的中間值應該爲引用類型的值,但是在某些調用中我們得到this的值不是base對象,而是global對象?

問題出現在後面的三個調用,在執行一定的操作運算之後,在調用括號的左邊的值不再是引用類型。

第一個例子很明顯———明顯的引用類型,結果是,this爲base對象,即foo。

在第二個例子中,分組操作符(譯者注:這裏的分組操作符就是指foo.bar外面的括號”()”)沒有實際意義,想想上面提到的,從引用類型中獲得一個對象真正的值的方法,如GetValue (參考11.1.6)。相應的,在分組操作的返回值中———我們得到的仍是一個引用類型。這就是this的值爲什麼再次被設爲base對象,即 foo。

第三個例子中,與分組操作符不同,賦值操作符調用了GetValue方法(參考11.13.1的第三步)。返回的結果已經是函數對象(不是引用類型),這意味着this的值被設爲null,實際最終結果是被設置爲global對象。

第四個和第五個也是一樣——逗號操作符和邏輯操作符(OR)調用了GetValue 方法,相應地,我們失去了引用類型的值而得到了函數類型的值,所以this的值再次被設爲global對象。

引用類型和this爲null

有一種情況,如果調用方式確定了引用類型的值,不管怎樣,只要this的值被設置爲null,其最終就會被隱式轉換成global。當引用類型值的base對象是激活對象時(即A O / VO),就會導致這種情況。

下面的實例中,內部函數被父函數調用,此時我們就能夠看到上面說的那種特殊情況。正如我們在 第二章 學到的一樣,局部變量、內部函數、形式參數都儲存在給定函數的激活對象中。

function foo() {
  function bar() {
    alert(this); // global
  }
  bar(); // the same as AO.bar()
}

激活對象總是作爲this的值返回——null(即僞代碼AO.bar()相當於null.bar()),this的值最終還是被設置爲全局對象。

this在作爲構造器調用的函數中的值

還有一個在函數的上下文中與this的值相關的情況是:函數作爲構造器調用時。

function A() {
  alert(this); // newly created object, below - "a" object
  this.x = 10;
}

var a = new A();
alert(a.x); // 10

在這個例子中,new操作符調用“A”函數內部的[[Construct]]方法,接着,在對象創建後,調用其內部的[[Call]]方法,所有相同的函數“A”都將this的值設置爲新創建的對象.

手動設置一個函數調用的this

在Function.prototype中定義了兩個方法允許手動設置函數調用時this的值,它們是.apply和.call方法(所有的函數都可以訪問它們)。它們用接受的第一個參數作爲this的值,this在調用的作用域中使用。這兩個方法的區別不大,對於.apply,第二個參數必須是數組(或者是類似數組的對象,如arguments,相反,.call能接受任何參數。兩個方法必須的參數都是第一個——this。

例如:

var b = 10;

function a(c) {
  alert(this.b);
  alert(c);
}

a(20); // this === global, this.b == 10, c == 20

a.call({b: 20}, 30); // this === {b: 20}, this.b == 20, c == 30
a.apply({b: 30}, [40]) // this === {b: 30}, this.b == 30, c == 40

結論

在這篇文章中,我們討論了JavaScript中this關鍵字的特徵,希望對你有所幫助,如果在此篇不懂的內容,在下一篇裏會詳細講述。


下一篇:再也不用擔心javascript的this—從代碼去理解(第二篇)–引用類型

本文參考自:

中文版 :[javascript]ECMA-262-3深入解析 第三章 this

英文版:ECMA-262-3 in detail. Chapter 3. This.

發佈了32 篇原創文章 · 獲贊 23 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章