根治JavaScript中的this-ECMAScript規範解讀

前言

this是JavaScript中的著名月經題,每隔一段時間就有人翻出了拿各類奇怪的問題出來討論,每次都會引起一堆口水之爭。從搜索引擎搜了一下如今的比較熱門的關於this的用法,如:Javascript的this用法深刻理解JavaScript中的this關鍵字你不知道的this 等文章幾乎都是從現象出發,總結this在不一樣場景下的指向結果,如同江湖郎中通常,都沒有從根本上解釋現象出現的緣由,這就致使每次有了關於this的題層出不窮,由於經驗總結只是教會了你現有的場景,而沒有教會你如何解釋新的場景。 javascript

老司機都知道,發展到今天,有規範在,有源碼在,早已經不是IE6時代,還須要總結使用經驗場景也太不科學了。最近又在網上刷到關於this的討論,正巧在規範中追尋過this的祕密,在這裏分享一下我的經驗。
*如下規範均引用ES5java

理論篇

規範中之處ECMAScript有三種可執行代碼:git

  • 全局代碼(Global codegithub

  • eval代碼(Eval code數據結構

  • 函數代碼(Function codeide

其中,對於全局代碼直接指向global object,eval代碼因爲已經不推薦使用暫不作討論,咱們主要關注函數代碼中的 this 如何指定。函數

進入函數代碼

The following steps are performed when control enters the execution context for function code contained in function object F, a caller provided thisArg, and a caller provided argumentsListthis

規範指出,當執行流進入函數代碼時,由函數調用者提供 thisArgargumentsList。因此咱們須要繼續尋找,查看函數調用時候this是如何傳遞進去的。搜索引擎

函數調用

The production CallExpression : MemberExpression Arguments is evaluated as follows:
1.Let ref be the result of evaluating MemberExpression.
...
6.If Type(ref) is Reference, then
 a. If IsPropertyReference(ref) is true, then
  i. Let thisValue be GetBase(ref).
 b. Else, the base of ref is an Environment Record
  i. Let thisValue be the result of calling the ImplicitThisValue concrete method of GetBase(ref).
7.Else, Type(ref) is not Reference.
 a. Let thisValue be undefined.lua

從上述規範中,在函數調用發生時,首先會對函數名部分進行計算並賦值給 ref ,六、7中
幾個if else就決定了一個函數調用發生時,this會指向何方。

圖片描述

在套用if else以前,咱們還須要稍微瞭解一下 Type(ref) is Reference 中的 Reference是個什麼東東。

Reference type

Reference type按字面翻譯就是引用類型,可是它並非咱們常說的JavaScript中的引用類型,它是一個規範類型(實際並不存在),也就是說是爲了解釋規範某些行爲而存在的,好比delete、typeof、賦值語句等。規範類型設計用於解析命名綁定的,它由三部分組成:

  • base value,指向引用的原值

  • referenced name,引用的名稱

  • strict reference flag,標示是否嚴格模式

用大白話講,就是規範中定義了一種類型叫作Reference用來引用其餘變量,它有一個規定的數據結構。因爲是規範類型,因此什麼狀況下會返回Reference規範上也會寫得一清二楚。
至此,咱們就能夠直接開始實戰了 ^ ^

實戰篇

咱們經過上述理論來解釋下面代碼中的this:

  • foo();

  • foo.bar();

  • (f = foo.bar)();

如何解釋foo();

  • 1 執行函數調用規範中的第一步:

Let ref be the result of evaluating MemberExpression.

MemberExpression就是括號左邊的部分,此處很簡單就是 foo。foo咱們知道就是一個標示符,那麼執行foo的時候會發生什麼呢?答案都在規範中
11.1.2 Identifier Reference

An Identifier is evaluated by performing Identifier Resolution as specified in 10.3.1. The result of evaluating anIdentifier is always a value of type Reference.

標示符被執行的時候會進行標示符解析(Identifier Resolution),看10.3.1。(連章節都標好了好伐,只須要點過去就好了。)

10.3.1 Identifier Resolution

1.Let env be the running execution context’s LexicalEnvironment.
...
3.Return the result of calling GetIdentifierReference function passing env, Identifier, and strict as arguments.

看這個返回,返回了 GetIdentifierReference 方法的結果,因此咱們還須要再走一步(要有耐心 - -)

GetIdentifierReference

Return a value of type Reference whose base value is envRec, whose referenced name is name, and whose strict mode flag is strict.

咱們看此處返回了一個Reference , base value 是 envRec 也就是 10.3.1中傳入的 execution context’s LexicalEnvironment
(詞法環境爲環境記錄項Environment Record的組成,它是規範用來管理當前做用域下面變量的類型,此處不用理解,知道它返回了這個東東就好了)
其抽象數據結構大概爲:

foo_reference = {
    "base": LexicalEnvironment,
    "name": "foo",
    "strict": false
}

至此,第一步執行完畢,ref = foo_reference

  • 2 由於是reference, 執行6:

Type(ref) is Reference

  • 3 由於是Environment Record,執行 6.b:

Else, the base of ref is an Environment Record
Let thisValue be the result of calling the ImplicitThisValue concrete method of GetBase(ref).

  • 4 ImplicitThisValue
    最後,thisValue等於執行ImplicitThisValue的結果,仍是查看規範:

10.2.1.1.6 ImplicitThisValue()

Declarative Environment Records always return undefined as their ImplicitThisValue.

到此咱們知道,foo()執行時, thisValue = undefined; , 對應到 JavaScript 代碼中的 this,還差最後一步:

Else if thisArg is null or undefined, set the ThisBinding to the global object.

真相大白:foo()執行時,this = global = window

如何解釋 foo.bar()

  • 1 執行函數調用規範中的第一步:

Let ref be the result of evaluating MemberExpression.

foo.bar咱們都知道是一個屬性訪問,那麼執行屬性訪問的時候會發生什麼呢?答案都在規範中
11.2.1 Property Accessors

The production MemberExpression : MemberExpression [ Expression ] is evaluated as follows:
1.Let baseReference be the result of evaluating MemberExpression.
2.Let baseValue be GetValue(baseReference)
8.Return a value of type Reference whose base value is baseValue and whose referenced name ispropertyNameString, and whose strict mode flag is strict

  1. baseReference 等於執行 MemberExpression 在此處爲 []左邊的語句即 foo 的結果,上一節已經說過了返回一個 reference.

  2. baseValue 等於 GetValue(baseReference) 。

    [8.7.1 GetValue](http://es5.github.io/#x8.7.1)   
    ( GetValue屬於一個規範中比較重要的方法,此處爲節約篇幅,暫時不講,之後能夠單開文章講解。暫時記住他的結果通常爲:若是是reference傳入,會返回一個普通類型出來。好比 foo 爲reference,經過GetValue以後就是一個普通的 object,也就是 foo 對應的 js 類型自己。)
reference_foo_bar = {
    "base": foo,
    "name": "bar",
    "strict": false
}
  • 2 由於是reference, 執行6:

Type(ref) is Reference

  • 3 由於是屬性引用類型,執行 6.a:

If IsPropertyReference(ref) is true, then
i.Let thisValue be GetBase(ref).

GetBase(ref) = reference_foo_bar.base = foo ;

真相大白, foo.bar() 執行時 this 指向 foo

如何解釋 (f = foo.bar)()

這個語句屬於罕見寫法,在各類經驗總結中屬於高級技巧,可是經過規範,同樣分分鐘搞定。

  • 1 執行函數調用規範中的第一步:

Let ref be the result of evaluating MemberExpression.

f = foo.bar 是一個明顯的賦值語句,咱們查看規範,賦值語句會發生什麼:

[11.13.1 Simple Assignment ( = )] (http://es5.github.io/#x11.13.1)

The production AssignmentExpression : LeftHandSideExpression = AssignmentExpression is evaluated as follows:

  1. Let rref be the result of evaluating AssignmentExpression.

  2. Let rval be GetValue(rref)

  3. Return rval.

能夠看出簡單賦值語句返回的是對等於號右邊進行GetValue以後的結果,上一節講了,執行過GetValue就會返回js的類型,此處會返回 foo.bar 也就是一個 [Object Function] 類型。

  • 2 ref不是reference,執行7

7.Else, Type(ref) is not Reference.

  1. Let thisValue be undefined.

  • 3 thisValue = undefined

同理:

Else if thisArg is null or undefined, set the ThisBinding to the global object.

真相大白, (f = foo.bar)() 執行時 this = global = window

老司機的經驗

雖然咱們不是江湖郎中,可是每次查詢規範也有點麻煩,此處仍是有必定規律可循的。
咱們觀察上述過程,其實最關鍵的就是判斷返回值是否是 reference ,若是不是,直接能夠推出等於window,若是是,只須要看是否是屬性 reference。這裏有外國友人統計過一張速查表:

Example Reference? Notes
"foo" No
123 No
/x/ No
({}) No
(function(){}) No
foo Yes Could be unresolved reference if foo is not defined
foo.bar Yes Property reference
(123).toString Yes Property reference
(function(){}).toString Yes Property reference
(1,foo.bar) No Already evaluated, BUT see grouping operator exception
(f = foo.bar) No Already evaluated, BUT see grouping operator exception
(foo) Yes Grouping operator does not evaluate reference
(foo.bar) Yes Ditto with property reference

結語

只想說一句:答案都在規範中答案都在規範中答案都在規範中 。與其讀微博上某些不靠譜大V的總結,不如去擼一遍規範。

相關文章
相關標籤/搜索