javascript this 綁定

this

Javascript this 的綁定是一個老大難問題。這裏順着標準捋一下 this 的問題。算法

獲取 this 綁定的對象

解決 this 綁定的問題,首先要看一下,當程序裏出現 this 的時候,究竟是如何獲取它綁定的對象的呢?閉包

標準裏,經過一個叫作 ResolveThisBinding 的內置方法獲取 this 的綁定,這個方法自己很簡單:app

  1. Let envRec be GetThisEnvironment().
  2. Return ? envRec.GetThisBinding().

首先經過 GetThisEnvironment 拿到保存了 this 的環境,而後經過這個環境的 GetThisBinding 內置方法獲得 thisecmascript

GetThisEnvironment

GetThisEnvironment 就是從當前的環境開始,一級一級向外找,直到找到一個由 this 的環境爲止:ide

  1. Let lex be the running execution context's LexicalEnvironment.
  2. Repeat,函數

    1. Let envRec be lex's EnvironmentRecord.
    2. Let exists be envRec.HasThisBinding().
    3. If exists is true, return envRec.
    4. Let outer be the value of lex's outer environment reference.
    5. Assert: outer is not null.
    6. Set lex to outer.

什麼樣的環境有 this 呢?其實,只有 Function 跟 Global 環境纔有 this 記錄,其餘環境,如塊(Block),是沒有的。this

這時,一件神奇的事情發生了。在全部的函數環境裏,僅有箭頭函數,它的環境裏是沒有 this 記錄的。因爲 GetThisEnvironment 算法會一直向外找,直到找到有 this 記錄的環境爲止,於是就有了有關 this 的第一條規則:箭頭函數會使用包含它的函數(或全局環境)的 thislua

GetThisBinding

接下來,來看 GetThisBinding。對不一樣的環境,它的定義並不相同。prototype

全局

全局環境比較簡單,它直接返回了一個 [[GlobalThisValue]] 的槽(能夠認爲是內置屬性):rest

  1. Let envRec be the global Environment Record for which the method was invoked.
  2. Return envRec.[[GlobalThisValue]].

這個 [[GlobalThisValue]] 又是啥呢?其實這個是由實現決定的。在不少實現裏,它就是全局對象。

Module 全局

Module 的全局環境 就更簡單了:

  1. Return undefined.

注意即便 this 綁定是 undefined ,綁定自己也是存在的。檢測綁定是否存在,基本經過環境的類型就已經肯定了。

函數

函數環境就略微複雜一些:

  1. Let envRec be the function Environment Record for which the method was invoked.
  2. Assert: envRec.[[ThisBindingStatus]] is not "lexical".
  3. If envRec.[[ThisBindingStatus]] is "uninitialized", throw a ReferenceError exception.
  4. Return envRec.[[ThisValue]].

其中第二步,[[ThisBindingStatus]] 不爲 "lexical" 實際是說這不能是一個箭頭函數(箭頭函數沒有 this 綁定。

第三部檢測若是 this 綁定沒有被初始化過,那麼拋出異常。啥時候初始化的,之後再說。

全部檢測都經過了,那麼能夠返回環境裏記錄 this 綁定了。

因而除了箭頭函數以外,this 直接使用了環境裏記錄的 this 綁定。因而函數裏的 this 是啥,其實就看運行時環境裏的 this 綁定到了哪裏。

普通函數調用

除了箭頭函數以外,其餘函數裏的 this 是啥,就看環境裏的 this 綁定到了哪裏。
函數環境的 this 是經過 BindThisValue 來綁定的。

OrdinaryCallBindThis(F, CalleeContext, thisArgument)

通觀標準,只用兩個地方引用了這個方法,一個是 OrdinaryCallBindThis ,另外一個是 supersuper 用於構造的,咱們一會再看。這裏先看一下 OrdinaryCallBindThis(F, calleeContext, thisArgument):

  1. Let thisMode be F.[[ThisMode]].
  2. If thisMode is lexical, return NormalCompletion(undefined).
  3. Let calleeRealm be F.[[Realm]].
  4. Let localEnv be the LexicalEnvironment of calleeContext.
  5. If thisMode is strict, let thisValue be thisArgument.
  6. Else,

    1. If thisArgument is undefined or null, then

      1. Let globalEnv be calleeRealm.[[GlobalEnv]].
      2. Let globalEnvRec be globalEnv's EnvironmentRecord.
      3. Assert: globalEnvRec is a global Environment Record.
      4. Let thisValue be globalEnvRec.[[GlobalThisValue]].
    2. Else,

      1. Let thisValue be ! ToObject(thisArgument).
      2. NOTE: ToObject produces wrapper objects using calleeRealm.
  7. Let envRec be localEnv's EnvironmentRecord.
  8. Assert: envRec is a function Environment Record.
  9. Assert: The next step never returns an abrupt completion because envRec.[ [ThisBindingStatus]] is not "initialized".
  10. Return envRec.BindThisValue(thisValue).

這裏 F 是被調用的函數,thisArgument 是待綁定的 this 值。

這裏有幾件事情須要注意:

  1. 第 2 步檢測了 thisMode ,若是爲 lexical,不作綁定,直接返回。這實際是在檢測箭頭函數。當前只有箭頭函數的 thisModelexical
  2. 若是函數定義在嚴格模式下,thisArgument 將直接做爲 this 綁定。可是,若是函數定義在非嚴格模式下,undefinednull 會被替換爲全局環境的 this ,通常就是全局對象;其餘(基本類型)值將被轉換爲對象。

上面第二點,就是沒有調用對象的時候,this 指向全局對象的來源。

[[Call]](thisArgument, arumentsList)

使用 OrdinaryBindThis 的,是普通函數對象的 [[Call]] 方法和 [[Construct]] 方法。[[Construct]] 方法用於構造,一會再看。[[[Call]](thisArgument, argumentsList)](https://www.ecma-internationa... 則是無條件的將傳入的 thisArgument 轉給了 OrdinaryBindThis 。

Call(F, V, argumentsList)

調用對象的 [[Call]] 方法的,是內置方法 Call(F, V, argumentList) 。它直接使用了 F.[[Call]](V, argumentList) 。

EvaluateCall(func, ref, arguments, tailPosition)

在函數調用的過程當中,使用 EvaluateCall 方法,其中調用了 Call。

  1. If Type(ref) is Reference, then

    1. If IsPropertyReference(ref) is true, then

      1. Let thisValue be GetThisValue(ref).
    2. Else the base of ref is an Environment Record,

      1. Let refEnv be GetBase(ref).
      2. Let thisValue be refEnv.WithBaseObject().
  2. Else Type(ref) is not Reference,

    1. Let thisValue be undefined.
  3. Let argList be ArgumentListEvaluation of arguments.
  4. ReturnIfAbrupt(argList).
  5. If Type(func) is not Object, throw a TypeError exception.
  6. If IsCallable(func) is false, throw a TypeError exception.
  7. If tailPosition is true, perform PrepareForTailCall().
  8. Let result be Call(func, thisValue, argList).
  9. Assert: If tailPosition is true, the above call will not return here, but instead evaluation will continue as if the following return has already occurred.
  10. Assert: If result is not an abrupt completion, then Type(result) is an ECMAScript language type.
  11. Return result.

這裏,終於出現肯定 thisValue 的邏輯,可是它與 ref 是不是 Reference 有關。ref 是啥呢,咱們看一下使用 EvaluateCall 的地方(有幾處,都差很少,這裏選了一個簡單的):

CallExpression : CallExpression Arguments

  1. Let ref be the result of evaluating CallExpression.
  2. Let func be ? GetValue(ref).
  3. Let thisCall be this CallExpression.
  4. Let tailCall be IsInTailPosition(thisCall).
  5. Return ? EvaluateCall(func, ref, Arguments, tailCall).

ref 是函數調用,函數名部分(函數其實能夠是一個表達式的結果)的計算結果。func 是從 ref 中取出的值,也就是被調用的函數。而 ref 不必定是一個值,多是 Reference (這個不是你們常說的引用,而是一種 ECMA-262 內置類型)。GetValue 能夠從 Reference 中取出記錄的值。

Reference

Reference 是一種標準內置類型。它用來表示標識符解析的結果,也就是說,在什麼地方找到了某一個標識符。

它通常記錄瞭如下幾個信息:

  1. base value: 這個標識符是在哪裏找到的。它能夠一個 Object, 基本類型的值,或者是一個環境(Environment Record),或者是 undefined

    1. 對於對象屬性,base value 將是包含這個標識符的對象。對象屬性訪問的形式(Property Access,如 A.BA["B"],以及 super.Property)的結果都會是一個Reference,其中 base value 將是其中至關於對象的部分的值。(注意不會檢查對象中是否真的存在這個屬性)
    2. 對於變量/常量,如局部變量,全局變量,函數參數等,或者說一個單獨的 Identifier,求值的結果是一個 Reference ,其中的 base value 將是包含這個變量的環境。查找會從標識符出現的環境開始,一層層向上找,直到全局環境。
    3. 變量沒有找到的時候,base value 爲 undefined。(只有單獨的 Identifier 沒有找到時會有此結果)
  2. referenced name: 這是一個字符串。表示標識符的名字。
  3. strict: 引用標識符的地點是否處於嚴格模式

super.Property 獲得 Reference 比較特殊,它通常只用在類成員中,Reference 的 base value 是其父類構造函數的 prototype 。同時,它還記錄了一個 thisValue,這是其它 Reference 所沒有的。這個 thisValue ,記錄了 super.Property 語句所在環境的 this

Property Access、super.Property 和 Identifier 的求值結果是 Reference 。(Expr) 的求值結果與 Expr 一致。其它全部表達式的求值結果都不是 Reference 。

EvalueteCall 中用的 GetThisValue ,會返回 Reference 中記載的 thisValue (若是存在),或者 base value :

  1. Assert: IsPropertyReference(V) is true.
  2. If IsSuperReference(V) is true, then

    1. Return the value of the thisValue component of the reference V.
  3. Return GetBase(V).

EvaluateCall 中肯定 thisValue 的規則

EvaluateCall 中的 ref 是對函數調用裏函數部分的求值結果。
從 Reference 的介紹,以及 EvaluateCall 的算法,能夠獲得初始 thisValue 的設置規則:

  1. 若是函數名是由一個表達式計算出來的,那麼 thisValue 是 undefined

    • 好比 (x?func1:func2)()
    • 此時 ref 不是 Reference
  2. 若是它是由 Property Access (A.B, A[B], super.B)生成的 (IsPropertyReference(ref) is true),那麼:

    1. A.B 使用 base value,也就是 A
    2. super.B 使用 thisValue ,也就是 super.B 所在函數的 this
  3. 若是函數名是一個單獨的標識符,Reference 的 base value 是一個環境,那麼返回這個環境的 WithBaseObject()

    • 此值僅當標識符解析爲一個 with 塊的對象的屬性時,爲該 with 塊的對象。其他均爲 undefined

小結(函數調用)

對函數調用來講,決定 this 經歷瞭如下幾個過程:

  1. 初始值:(EvaluteCall)

    1. A.func(),爲 A
    2. super.func(),爲 super.func() 語句所在函數的 this
    3. with (obj) { func() ... } ,若是 func 解析爲 obj 的屬性,爲 obj
    4. 其他均爲 undefined
  2. 非嚴格模式函數替換 undefined :(OrdinaryCallBindTHis)

    • 非嚴格模式函數,undefined 會被替換爲全局環境的 this
    • 此處僅檢查函數定義是否在嚴格模式。與調用處是否嚴格模式無關
  3. 非箭頭函數,將以上求得的值寫入函數運行時環境

讀取 this 的值時,除了箭頭函數,直接從被調用函數的環境中讀取。對於箭頭函數,從包含箭頭函數定義的環境中讀取。(注意,不是箭頭函數的調用者)(也能夠認爲,箭頭函數在定義時,對外層的 this 造成了一個閉包)

其它函數調用/系統回調函數

除了直接調用以外,Javascript 函數還能夠經過 call, apply 來調用。這兩中調用方式相似,均可以認爲是對上面提到的系統內置的 Call 方法的一個封裝。

經過這種方式調用,不會經歷 EvaluateCall ,而是以一個指定的 thisValue 來調用函數。這個 thisValue 會被寫進被調用函數的運行時環境。

固然,因爲箭頭函數運行時環境沒有記錄 thisValue ,這中方式設置 thisValue 對箭頭函數是無效的。

ECMA-262 中規定的不少內置函數會又回調函數,好比 forEach 。這些回調函數一般會經過 Call 指定 thisValue 爲 undefined 調用(一樣,對箭頭函數不生效)。個別函數(好比 forEach)能夠調用時經過參數指定調用回調時的 thisValue。

bind

bind 會生成一個新的函數對象。這個新的函數對象在生成時記錄了調用原函數對象時須要使用的 thisValue 。

bind 返回的函數對象被調用時,會經過 Call 以記錄下的 thisValue 調用被綁定的函數對象。(同將,對箭頭函數時無效的。)

構造

Javascript 中,使用 new Func() 的方式調用構造函數,會使用構造函數的 [[Construct]] 方法來執行。注意一個函數能夠同時有 [[Call]] 與 [[Construct]] ,可是兩

[[Construct]](argumentList, newTarget)

與 [[Call]] 不一樣,[[[Construct]](argumentList, newTarget)](https://www.ecma-internationa... 並無一個參數指明 thisValue 。

newTargetnew Func() 表達式中被調用的構造函數 ,也就是 Func 。須要注意的是,即便 Func 是一個類(使用 class 定義的),而且有基類(在定義時有 extends Base),那麼在 Func 中會使用 super(...) 來構造基類,此時會執行基類的構造(Base),但在執行基類的構造時, NewTarget 依然爲 Func 。也就是說,NewTarget 永遠指向 new 表達式中的那個構造函數。

[[[Construct]](argumentList, newTarget)](https://www.ecma-internationa... 的執行過程以下(F 時構造函數對象):

  1. Assert: F is an ECMAScript function object.
  2. Assert: Type(newTarget) is Object.
  3. Let callerContext be the running execution context.
  4. Let kind be F.[[ConstructorKind]].
  5. If kind is "base", then

    1. Let thisArgument be ? OrdinaryCreateFromConstructor(newTarget, "%ObjectPrototype%").
  6. Let calleeContext be PrepareForOrdinaryCall(F, newTarget).
  7. Assert: calleeContext is now the running execution context.
  8. If kind is "base", perform OrdinaryCallBindThis(F, calleeContext, thisArgument).
  9. Let constructorEnv be the LexicalEnvironment of calleeContext.
  10. Let envRec be constructorEnv's EnvironmentRecord.
  11. Let result be OrdinaryCallEvaluateBody(F, argumentsList).
  12. Remove calleeContext from the execution context stack and restore callerContext as the running execution context.
  13. If result.[[Type]] is return, then

    1. If Type(result.[[Value]]) is Object, return NormalCompletion(result.[[Value]]).
    2. If kind is "base", return NormalCompletion(thisArgument).
    3. If result.[[Value]] is not undefined, throw a TypeError exception.
  14. Else, ReturnIfAbrupt(result).
  15. Return ? envRec.GetThisBinding().

注意 F 是正在被調用的構造函數,NewTargetnew 表達式中的構造函數。在經過 super(...) 指向到基類構造的時候,兩這是不一樣的:F 是基類構造函數,NewTarget 依然是 new 表達式中的派生類構造函數。其他狀況下,二者是相同的。

根據第 5 步與第 8 步,僅僅在 F.[[ConstructorKind]] 爲 "base" 的時候,纔會建立一個新對象(其原型對象是 NewTarget.prototype),並將這個新建立的對象經過 OrdinaryCallBindThis 綁定到被調用函數的運行時環境。

其他狀況下,在函數開始執行的時候,函數環境的 this 綁定並無被初始化。(構造函數必定不是箭頭函數,其運行時環境中必定存在一個 this 綁定)

[[ConstructorKind]] 如今又兩個可能的取值,"base""derived" 。僅當構造函數使用 class 定義,而且有基類 (extends Base) 時(也就是派生類的構造函數),[[ConstructorKind]] 才時 "derived" 。其他全部構造函數的 [[ConstructorKind]] 都是 "base" (包括全部不使用 class 語法定義的構造函數)。爲方便起見,下面把全部 [[ConstructorKind]] 爲 "base" 的構造函數成爲基類構造函數,[[ConstructorKind]] 爲 "derived" 的構造函數成爲派生類構造函數。

因而,全部基類構造函數的 this ,都是構造函數開始執行之間,建立的一個新對象。可是,派生類構造函數的 this ,在構造函數開始執行時,是沒有初始化的。此時引用 this 會拋出異常。

在以構造方式調用基類構造函數時,若是函數不以 return 結束,那麼函數的返回就是這個新建立的對象。

super(...)

那麼派生類構造函數的 this 又是哪裏來的呢?是經過 superCallsuper(...))調用基類的構造函數生成的:

  1. Let newTarget be GetNewTarget().
  2. Assert: Type(newTarget) is Object.
  3. Let func be ? GetSuperConstructor().
  4. Let argList be ArgumentListEvaluation of Arguments.
  5. ReturnIfAbrupt(argList).
  6. Let result be ? Construct(func, argList, newTarget).
  7. Let thisER be GetThisEnvironment().
  8. Return ? thisER.BindThisValue(result).

superCall 調用了基類的構造函數,並將構造函數的返回(新建立的對象)綁定到了當前構造函數的運行時環境中。

派生類構造函數,若是不以 return 結束,那麼它的返回就是經過 superCall 新生成,並綁定到 this 的新對象。

於是,在派生類構造函數中,在 superCall 以前使用 this ,或者返回(不經過 return),都會時運行時錯誤,由於 this 並無被初始化。

小結(構造)

在以構造方式調用函數是,基類構造函數的 this ,就是新建立的對象。派生類構造函數開始執行時不能引用 this ,必須經過 super(...) 調用基類構造函數,生成一個新對象,此後這個新對象將成爲 this 的值。

相關文章
相關標籤/搜索