Javascript this
的綁定是一個老大難問題。這裏順着標準捋一下 this
的問題。算法
this
綁定的對象解決 this
綁定的問題,首先要看一下,當程序裏出現 this
的時候,究竟是如何獲取它綁定的對象的呢?閉包
標準裏,經過一個叫作 ResolveThisBinding 的內置方法獲取 this
的綁定,這個方法自己很簡單:app
- Let envRec be GetThisEnvironment().
- Return ? envRec.GetThisBinding().
首先經過 GetThisEnvironment 拿到保存了 this
的環境,而後經過這個環境的 GetThisBinding 內置方法獲得 this
。ecmascript
GetThisEnvironment 就是從當前的環境開始,一級一級向外找,直到找到一個由 this
的環境爲止:ide
- Let lex be the running execution context's LexicalEnvironment.
Repeat,函數
- Let envRec be lex's EnvironmentRecord.
- Let exists be envRec.HasThisBinding().
- If exists is true, return envRec.
- Let outer be the value of lex's outer environment reference.
- Assert: outer is not null.
- Set lex to outer.
什麼樣的環境有 this
呢?其實,只有 Function 跟 Global 環境纔有 this
記錄,其餘環境,如塊(Block),是沒有的。this
這時,一件神奇的事情發生了。在全部的函數環境裏,僅有箭頭函數,它的環境裏是沒有 this
記錄的。因爲 GetThisEnvironment 算法會一直向外找,直到找到有 this
記錄的環境爲止,於是就有了有關 this
的第一條規則:箭頭函數會使用包含它的函數(或全局環境)的 this
。lua
接下來,來看 GetThisBinding。對不一樣的環境,它的定義並不相同。prototype
全局環境比較簡單,它直接返回了一個 [[GlobalThisValue]] 的槽(能夠認爲是內置屬性):rest
- Let envRec be the global Environment Record for which the method was invoked.
- Return envRec.[[GlobalThisValue]].
這個 [[GlobalThisValue]] 又是啥呢?其實這個是由實現決定的。在不少實現裏,它就是全局對象。
在 Module 的全局環境 就更簡單了:
- Return undefined.
注意即便 this
綁定是 undefined
,綁定自己也是存在的。檢測綁定是否存在,基本經過環境的類型就已經肯定了。
函數環境就略微複雜一些:
- Let envRec be the function Environment Record for which the method was invoked.
- Assert: envRec.[[ThisBindingStatus]] is not "lexical".
- If envRec.[[ThisBindingStatus]] is "uninitialized", throw a ReferenceError exception.
- Return envRec.[[ThisValue]].
其中第二步,[[ThisBindingStatus]] 不爲 "lexical" 實際是說這不能是一個箭頭函數(箭頭函數沒有 this
綁定。
第三部檢測若是 this
綁定沒有被初始化過,那麼拋出異常。啥時候初始化的,之後再說。
全部檢測都經過了,那麼能夠返回環境裏記錄 this
綁定了。
因而除了箭頭函數以外,this
直接使用了環境裏記錄的 this
綁定。因而函數裏的 this
是啥,其實就看運行時環境裏的 this
綁定到了哪裏。
除了箭頭函數以外,其餘函數裏的 this
是啥,就看環境裏的 this
綁定到了哪裏。
函數環境的 this
是經過 BindThisValue 來綁定的。
通觀標準,只用兩個地方引用了這個方法,一個是 OrdinaryCallBindThis ,另外一個是 super
。super
用於構造的,咱們一會再看。這裏先看一下 OrdinaryCallBindThis(F, calleeContext, thisArgument):
- Let thisMode be F.[[ThisMode]].
- If thisMode is lexical, return NormalCompletion(
undefined
).- Let calleeRealm be F.[[Realm]].
- Let localEnv be the LexicalEnvironment of calleeContext.
- If thisMode is strict, let thisValue be thisArgument.
Else,
If thisArgument is
undefined
ornull
, then
- Let globalEnv be calleeRealm.[[GlobalEnv]].
- Let globalEnvRec be globalEnv's EnvironmentRecord.
- Assert: globalEnvRec is a global Environment Record.
- Let thisValue be globalEnvRec.[[GlobalThisValue]].
Else,
- Let thisValue be ! ToObject(thisArgument).
- NOTE: ToObject produces wrapper objects using calleeRealm.
- Let envRec be localEnv's EnvironmentRecord.
- Assert: envRec is a function Environment Record.
- Assert: The next step never returns an abrupt completion because envRec.[ [ThisBindingStatus]] is not "initialized".
- Return envRec.BindThisValue(thisValue).
這裏 F 是被調用的函數,thisArgument 是待綁定的 this
值。
這裏有幾件事情須要注意:
this
綁定。可是,若是函數定義在非嚴格模式下,undefined
與 null
會被替換爲全局環境的 this
,通常就是全局對象;其餘(基本類型)值將被轉換爲對象。上面第二點,就是沒有調用對象的時候,this
指向全局對象的來源。
使用 OrdinaryBindThis 的,是普通函數對象的 [[Call]] 方法和 [[Construct]] 方法。[[Construct]] 方法用於構造,一會再看。[[[Call]](thisArgument, argumentsList)](https://www.ecma-internationa... 則是無條件的將傳入的 thisArgument 轉給了 OrdinaryBindThis 。
調用對象的 [[Call]] 方法的,是內置方法 Call(F, V, argumentList) 。它直接使用了 F.[[Call]](V, argumentList) 。
在函數調用的過程當中,使用 EvaluateCall 方法,其中調用了 Call。
If Type(ref) is Reference, then
If IsPropertyReference(ref) is true, then
- Let thisValue be GetThisValue(ref).
Else the base of ref is an Environment Record,
- Let refEnv be GetBase(ref).
- Let thisValue be refEnv.WithBaseObject().
Else Type(ref) is not Reference,
- Let thisValue be
undefined
.- Let argList be ArgumentListEvaluation of arguments.
- ReturnIfAbrupt(argList).
- If Type(func) is not Object, throw a TypeError exception.
- If IsCallable(func) is false, throw a TypeError exception.
- If tailPosition is true, perform PrepareForTailCall().
- Let result be Call(func, thisValue, argList).
- 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.
- Assert: If result is not an abrupt completion, then Type(result) is an ECMAScript language type.
- Return result.
這裏,終於出現肯定 thisValue 的邏輯,可是它與 ref 是不是 Reference 有關。ref 是啥呢,咱們看一下使用 EvaluateCall
的地方(有幾處,都差很少,這裏選了一個簡單的):
CallExpression : CallExpression Arguments
- Let ref be the result of evaluating CallExpression.
- Let func be ? GetValue(ref).
- Let thisCall be this CallExpression.
- Let tailCall be IsInTailPosition(thisCall).
- Return ? EvaluateCall(func, ref, Arguments, tailCall).
ref 是函數調用,函數名部分(函數其實能夠是一個表達式的結果)的計算結果。func 是從 ref 中取出的值,也就是被調用的函數。而 ref 不必定是一個值,多是 Reference (這個不是你們常說的引用,而是一種 ECMA-262 內置類型)。GetValue 能夠從 Reference 中取出記錄的值。
Reference 是一種標準內置類型。它用來表示標識符解析的結果,也就是說,在什麼地方找到了某一個標識符。
它通常記錄瞭如下幾個信息:
base value: 這個標識符是在哪裏找到的。它能夠一個 Object, 基本類型的值,或者是一個環境(Environment Record),或者是 undefined
A.B
, A["B"]
,以及 super.Property
)的結果都會是一個Reference,其中 base value 將是其中至關於對象的部分的值。(注意不會檢查對象中是否真的存在這個屬性)undefined
。(只有單獨的 Identifier 沒有找到時會有此結果)由 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 :
- Assert: IsPropertyReference(V) is true.
If IsSuperReference(V) is true, then
- Return the value of the thisValue component of the reference V.
- Return GetBase(V).
EvaluateCall 中的 ref 是對函數調用裏函數部分的求值結果。
從 Reference 的介紹,以及 EvaluateCall 的算法,能夠獲得初始 thisValue 的設置規則:
若是函數名是由一個表達式計算出來的,那麼 thisValue 是 undefined
。
(x?func1:func2)()
若是它是由 Property Access (A.B
, A[B]
, super.B
)生成的 (IsPropertyReference(ref) is true),那麼:
A.B
使用 base value,也就是 A
super.B
使用 thisValue ,也就是 super.B
所在函數的 this
若是函數名是一個單獨的標識符,Reference 的 base value 是一個環境,那麼返回這個環境的 WithBaseObject()
with
塊的對象的屬性時,爲該 with
塊的對象。其他均爲 undefined
。對函數調用來講,決定 this
經歷瞭如下幾個過程:
初始值:(EvaluteCall)
A.func()
,爲 A
super.func()
,爲 super.func()
語句所在函數的 this
with (obj) { func() ... }
,若是 func
解析爲 obj
的屬性,爲 obj
undefined
非嚴格模式函數替換 undefined
:(OrdinaryCallBindTHis)
undefined
會被替換爲全局環境的 this
讀取 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]] ,可是兩
與 [[Call]] 不一樣,[[[Construct]](argumentList, newTarget)](https://www.ecma-internationa... 並無一個參數指明 thisValue 。
newTarget
是 new Func()
表達式中被調用的構造函數 ,也就是 Func
。須要注意的是,即便 Func
是一個類(使用 class
定義的),而且有基類(在定義時有 extends Base
),那麼在 Func
中會使用 super(...)
來構造基類,此時會執行基類的構造(Base),但在執行基類的構造時, NewTarget 依然爲 Func
。也就是說,NewTarget 永遠指向 new 表達式中的那個構造函數。
[[[Construct]](argumentList, newTarget)](https://www.ecma-internationa... 的執行過程以下(F 時構造函數對象):
- Assert: F is an ECMAScript function object.
- Assert: Type(newTarget) is Object.
- Let callerContext be the running execution context.
- Let kind be F.[[ConstructorKind]].
If kind is "base", then
- Let thisArgument be ? OrdinaryCreateFromConstructor(newTarget, "%ObjectPrototype%").
- Let calleeContext be PrepareForOrdinaryCall(F, newTarget).
- Assert: calleeContext is now the running execution context.
- If kind is "base", perform OrdinaryCallBindThis(F, calleeContext, thisArgument).
- Let constructorEnv be the LexicalEnvironment of calleeContext.
- Let envRec be constructorEnv's EnvironmentRecord.
- Let result be OrdinaryCallEvaluateBody(F, argumentsList).
- Remove calleeContext from the execution context stack and restore callerContext as the running execution context.
If result.[[Type]] is return, then
- If Type(result.[[Value]]) is Object, return NormalCompletion(result.[[Value]]).
- If kind is "base", return NormalCompletion(thisArgument).
- If result.[[Value]] is not
undefined
, throw aTypeError
exception.- Else, ReturnIfAbrupt(result).
- Return ? envRec.GetThisBinding().
注意 F 是正在被調用的構造函數,NewTarget 是 new
表達式中的構造函數。在經過 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
又是哪裏來的呢?是經過 superCall (super(...)
)調用基類的構造函數生成的:
- Let newTarget be GetNewTarget().
- Assert: Type(newTarget) is Object.
- Let func be ? GetSuperConstructor().
- Let argList be ArgumentListEvaluation of Arguments.
- ReturnIfAbrupt(argList).
- Let result be ? Construct(func, argList, newTarget).
- Let thisER be GetThisEnvironment().
- Return ? thisER.BindThisValue(result).
superCall 調用了基類的構造函數,並將構造函數的返回(新建立的對象)綁定到了當前構造函數的運行時環境中。
派生類構造函數,若是不以 return 結束,那麼它的返回就是經過 superCall 新生成,並綁定到 this
的新對象。
於是,在派生類構造函數中,在 superCall 以前使用 this
,或者返回(不經過 return),都會時運行時錯誤,由於 this
並無被初始化。
在以構造方式調用函數是,基類構造函數的 this
,就是新建立的對象。派生類構造函數開始執行時不能引用 this
,必須經過 super(...)
調用基類構造函數,生成一個新對象,此後這個新對象將成爲 this
的值。