JavaScript の this を理解する — ECMAScript の仕様から見る
はじめに
本記事では JavaScript の this の正体を ECMAScript 仕様書に基づいて紐解いていきます。
随所に、ECMAScript の原文とその該当箇所へのリンクを併記しています。
(※原文を参照・引用する箇所についてはすべて引用ブロック > を用いて明示します)
原文に添えている日本語の文章は日本語訳ではなく、説明に用いやすいように意訳した
ものです。
【各章について】
1章で「同じ関数なのに呼び方で this が変わる」謎を提示し、2〜3章で this を
保持する場所と、this を解決するために必要な Reference Record の正体を解説
します。4章では別例で関数呼び出しの内部処理 OrdinaryCallBindThis を追い、
5〜7章でその応用としての Arrow Function / call・apply・bind / new・
class を順に見ます。8章で1章の謎を完全解明します。
【本記事で扱わないこと】
-
with文、eval内のthis(非推奨機能。境界引き) - モジュールトップレベルの
this(undefinedになる理由は触れるが詳細は別記事) - HTML 仕様で定義される
globalThisの中身(言語仕様外)
目次
- 1.
thisとは — 同じ関数で答えが変わる謎 - 2. Execution Context と
[[ThisValue]] - 3. Reference Record —
obj.fは何を返す式か - 4. 関数呼び出しの流れ —
OrdinaryCallBindThisを別例で追う - 5. Arrow Function —
[[ThisMode]]= lexical - 6.
call/apply/bind—thisArgumentの明示とBoundFunctionExoticObject - 7.
newとclass—[[Construct]]と未初期化this - 8. 1章の謎の解明
- 9. まとめ
1. this とは — 同じ関数で答えが変わる謎
this は JavaScript で最もよく語られるテーマですが、「呼ばれ方で変わる」「レシーバ
になる」「Arrow Function だけ違う」といった説明の断片を組み合わせて理解している
方が多いのではないでしょうか。実際、入門書を読んだ直後でも、次のコードがなぜ
こうなるのかをスラスラ説明するのは案外むずかしいです。
"use strict";
const obj = {
name: "obj",
who() { return this?.name ?? "(no this)"; },
};
const f1 = () => obj.who();
const f2 = () => { const g = obj.who; return g(); };
const f3 = () => obj.who.call({ name: "X" });
f1(); // "obj"
f2(); // "(no this)" ← this は undefined
f3(); // "X"
obj.who という 同じ関数オブジェクト を呼んでいるにもかかわらず、this の値は
3 通りに分かれます。さらに、Arrow Function を絡めるとこうなります。
"use strict";
const outer = {
name: "outer",
classic: function () { return this?.name ?? "(no this)"; },
arrow: () => this?.name ?? "(no this)",
};
outer.classic(); // "outer"
outer.arrow(); // "(no this)" ← ドットで呼んでも "outer" にならない
ドット呼び出しなのに outer.arrow() は "outer" を返しません。「ドット呼び出しで
レシーバが this になる」というよく聞くルールでは、この結果は説明できません。
これらの挙動を「そういうものだ」と覚えるのではなく、ECMAScript が this を
どこに保存し、いつ・どんな値を書き込んでいるか を読めば、すべて1つの仕組みの
帰結として説明できます。
本記事ではこの仕組みを 2〜7章で部品ごとに解説し、8章で再びこの章のコードに戻り、
仕様レベルで完全に解明します。
本記事のコード前提
本記事のコードはすべて strict mode を前提とします。sloppy mode のthisの
ボックス化(プリミティブが Object に包まれる、null/undefinedが globalThis に
置き換わる)は本記事では扱いません。仕様上はOrdinaryCallBindThisの
[[ThisMode]] = global分岐に該当しますが、現代の JS では"use strict"または
モジュール内に書く前提で問題ありません。
2. Execution Context と [[ThisValue]]
this の話を始める前に、まず this は物理的にどこに保存されているのか を
仕様で確認します。結論を先に言うと、this は Function Environment Record の
[[ThisValue]] という内部スロットに保存されています。
Execution Context と Environment Record
ECMAScript はコードの実行を Execution Context(以下 EC)という単位で管理します。
グローバルコードに対して Global EC、関数呼び出しごとに新しい関数 EC が作られ、
EC スタックに積まれます。
EC は複数のコンポーネントを持ちますが、this の話で重要なのは
LexicalEnvironment です。これは Environment Record(以下 ER)を指します。
原文:
An execution context is a specification device that is used to track the runtime
evaluation of code by an ECMAScript implementation. ... Execution contexts have
the additional state components listed in Table: LexicalEnvironment, VariableEnvironment, PrivateEnvironment.
— §9.4 Execution Contexts
ER にはいくつか種類があり、関数呼び出しごとに新しく作られるのが Function
Environment Record です。
Function Environment Record の内部スロット
Function ER は、変数バインディング(普通の ER の役割)に加えて、this のための
スロットを持ちます。
原文:
Function Environment Records have the additional state fields listed in Table 16.
[[ThisValue]] — This is the this value used for this invocation of the function.
[[ThisBindingStatus]] — lexical, initialized, or uninitialized. If the value is lexical,
this is an ArrowFunction and does not have a local this value.
[[FunctionObject]] — The function object whose invocation caused this Environment Record to be created.
[[NewTarget]] — If this Environment Record was created by the [[Construct]] internal method, ...
— §9.1.1.3 Function Environment Records
ここで覚えてほしいのは2つだけです。
-
[[ThisValue]]: その関数呼び出しのthisの値 -
[[ThisBindingStatus]]:lexical/initialized/uninitializedの3状態
lexical は5章(Arrow Function)、uninitialized は7章(new)で重要になります。
this 式の評価
ソースコード中の this という式(ThisExpression)は、評価されると次の処理を
走らせます。
原文:
PrimaryExpression : this
- Return ? ResolveThisBinding().
— §13.2.1.1 Runtime Semantics: Evaluation
ResolveThisBinding の中身はシンプルで、「this を持っている一番内側の ER を
見つけて、その [[ThisValue]] を返す」 だけです。「this を持っている ER を
見つける」のは GetThisEnvironment が担当します。
原文:
- Let env be the running execution context's LexicalEnvironment.
- Repeat,
a. Let exists be env.HasThisBinding().
b. If exists is true, return env.
c. Let outer be env.[[OuterEnv]].
d. Assert: outer is not null.
e. Set env to outer.
— §9.4.3 GetThisEnvironment
つまり、現在の EC の LexicalEnvironment から始めて、[[OuterEnv]] を順番に
辿り、HasThisBinding() が true になる ER を見つけます。Function ER と Global ER
は HasThisBinding() が true、ブロック ER などは false です。
この章のまとめ
つまり、this 式の値は、GetThisEnvironment が走査して見つけた最初の Function ER
(または Global ER)の [[ThisValue]] スロットに格納されている値である、と言えます。
残る疑問は2つです:
-
[[ThisValue]]には いつ・どんな値が書き込まれる のか? -
obj.who()とg()で書き込まれる値が違うのはなぜか?
これらを3章(Reference Record)と4章(OrdinaryCallBindThis)で見ます。
3. Reference Record — obj.f は何を返す式か
obj.who() を1つの操作だと思っていると、this の謎は永遠に解けません。
ECMAScript はこれを 2段階 で評価します。
-
obj.whoを評価する → 結果は「値」ではなく Reference Record -
その結果に
()を適用して呼び出す → このとき Reference Record の情報から
thisを決める
この章では1段階目の Reference Record と、2段階目で this が決まる箇所を見ます。
Reference Record とは
Reference Record は、obj.who のような 「まだ値を取り出していない、参照の途中
状態」 を表す仕様用の型です。ランタイムには現れず、仕様内部だけで使われます。
原文:
The Reference Record type is used to explain the behaviour of such operators as
delete, typeof, the assignment operators, the super keyword and other language
features. ... A Reference Record is a resolved name or (possibly not-yet-resolved)
property binding; its fields are defined by Table 8.
[[Base]] — The value or Environment Record which holds the binding. ...
[[ReferencedName]] — The name of the binding.
[[Strict]] — true if the Reference Record originated in strict mode code, false otherwise.
[[ThisValue]] — If not empty, the Reference Record represents a property binding
that was expressed using the super keyword; it is called a Super Reference Record ...
— §6.2.5 The Reference Record Specification Type
obj.who を評価すると、[[Base]] = obj / [[ReferencedName]] = "who" を持つ
Reference Record ができます(プロパティアクセスの評価規則 §13.3.2.1)。
ここで重要なのは、Reference Record はまだ「who の関数オブジェクト」そのもの
ではない という点です。[[Base]](呼び出し元のオブジェクト)情報が一緒に
くっついています。この [[Base]] が、後で this になります。
GetValue で Reference は「値」に潰れる
const g = obj.who のような代入では、右辺の Reference Record はすぐに
GetValue を通って ただの関数オブジェクト に変換されます。[[Base]] = obj
の情報はここで失われます。
原文:
- If referenceRecord is not a Reference Record, return referenceRecord.
- If IsUnresolvableReference(referenceRecord) is true, throw a ReferenceError exception.
- If IsPropertyReference(referenceRecord) is true, then
a. Let baseObj be ? ToObject(referenceRecord.[[Base]]).
b. ...
c. Return ? baseObj.[[Get]](referenceRecord.[[ReferencedName]], GetThisValue(referenceRecord)).- Else,
a. Let base be referenceRecord.[[Base]].
b. Assert: base is an Environment Record.
c. Return ? base.GetBindingValue(referenceRecord.[[ReferencedName]], referenceRecord.[[Strict]]).
— §6.2.5.5 GetValue
GetValue の戻り値は ECMAScript の通常の値(ここでは関数オブジェクト)であり、
Reference Record ではありません。だから g には関数オブジェクトしか入って
いません。obj との結びつきはこの時点で消えています。
関数呼び出しでは Reference を直接受け取る
ところが、obj.who() のような関数呼び出しでは、( の左側の評価結果を
GetValue で潰す前に、Reference Record のまま EvaluateCall に渡します。
ここがポイントです。
原文:
CallExpression : CallExpression Arguments
- Let ref be ? Evaluation of CallExpression.
- Let func be ? GetValue(ref).
- Let thisCall be this CallExpression.
- Let tailCall be IsInTailPosition(thisCall).
- Return ? EvaluateCall(func, ref, Arguments, tailCall).
— §13.3.6.1 Function Calls: Runtime Semantics: Evaluation
func は GetValue した関数オブジェクトですが、ref(Reference Record そのもの)
も同時に EvaluateCall に渡されている ことに注目してください。
そして EvaluateCall が thisValue を決定します。
原文:
- If ref is a Reference Record, then
a. If IsPropertyReference(ref) is true, then
i. Let thisValue be GetThisValue(ref).
b. Else,
i. Let refEnv be ref.[[Base]].
ii. Assert: refEnv is an Environment Record.
iii. Let thisValue be refEnv.WithBaseObject().- Else,
a. Let thisValue be undefined.- Let argList be ? ArgumentListEvaluation of arguments.
- If func is not an Object, throw a TypeError exception.
- If IsCallable(func) is false, throw a TypeError exception.
- If tailPosition is true, perform PrepareForTailCall().
- Return ? Call(func, thisValue, argList).
— §13.3.6.2 EvaluateCall
これがすべてです。整理すると:
| 呼び出し式 |
( の左の評価結果 |
EvaluateCall の thisValue
|
|---|---|---|
obj.who() |
Reference Record(プロパティ参照、[[Base]] = obj) |
GetThisValue(ref) = obj
|
g() |
Reference Record(識別子参照、[[Base]] は ER) |
WithBaseObject() ≈ undefined(普通の ER は常に undefined を返す) |
(0, obj.who)() |
カンマ演算子で GetValue 済み、Reference ではなく値 |
step 2 へ → undefined
|
(0, obj.who)() のトリックは知っている方も多いと思いますが、カンマ演算子が
GetValue を呼んで Reference を値に潰すから、その後の呼び出しで this が
undefined になる、というのが仕様レベルでの説明です。
この章のまとめ
関数呼び出しの this は、呼び出し式の「( の左側」を評価した結果が
Reference Record か単なる値かで決まります。Reference なら GetThisValue で
取り出した [[Base]]、ただの値なら undefined(strict 時)が thisValue に
なります。
そしてこの thisValue が、次章で見る [[Call]] → OrdinaryCallBindThis の
thisArgument 引数として渡されていきます。
4. 関数呼び出しの流れ — OrdinaryCallBindThis を別例で追う
ここまでで「this 式は Function ER の [[ThisValue]] を引く」(2章)と
「呼び出し式は Reference の [[Base]] から thisValue を作って Call に渡す」
(3章)が分かりました。残る穴は、Call に渡された thisValue がどうやって
Function ER の [[ThisValue]] に書き込まれるか です。
それを担当するのが OrdinaryCallBindThis です。
この章では、リング構造のリーフでよくやるように、1章の例とは別の最小例 を
使って [[Call]] から本体評価までの流れを追います。1章の例で説明すると
「謎の例だから特殊な処理がある」と誤解されがちなので、まずは普通の
obj.method() で機構を見ます。
"use strict";
const counter = {
n: 0,
inc() { this.n++; return this.n; },
};
counter.inc(); // 1
全体の流れ
counter.inc() が走るとき、ECMAScript は次の順で動きます。
-
counter.incを評価 → Reference Record([[Base]] = counter) -
EvaluateCall→thisValue = counterを決定(3章で見た) -
Call(func, thisValue, args)→func.[[Call]](thisValue, args) -
[[Call]]→PrepareForOrdinaryCall→OrdinaryCallBindThis→OrdinaryCallEvaluateBody - 本体内の
this.n++のthisは ResolveThisBinding で Function ER の[[ThisValue]]を引く
ステップ4を細かく見ます。
[[Call]] の中身
原文:
- Let callerContext be the running execution context.
- Let calleeContext be PrepareForOrdinaryCall(func, undefined).
- Assert: calleeContext is now the running execution context.
- If func.[[IsClassConstructor]] is true, then
a. Let error be a newly created TypeError object.
b. ... Throw error.- Perform OrdinaryCallBindThis(func, calleeContext, thisArgument).
- Let result be Completion(OrdinaryCallEvaluateBody(func, argumentsList)).
- Remove calleeContext from the execution context stack and restore callerContext as the running execution context.
- If result is a return completion, return result.[[Value]].
- Assert: result is a throw completion.
- Return ? result.
— §10.2.1 [[Call]]
[[Call]] は次の3つを順に呼ぶだけです:
-
PrepareForOrdinaryCall: 新しい EC と Function ER を作る -
OrdinaryCallBindThis: Function ER の[[ThisValue]]を書き込む(この章の主役) -
OrdinaryCallEvaluateBody: 本体を評価する
PrepareForOrdinaryCall: EC と Function ER を作る
原文:
- Let callerContext be the running execution context.
- Let calleeContext be a new ECMAScript code execution context.
- Set the Function of calleeContext to func.
- Let calleeRealm be func.[[Realm]].
- Set the Realm of calleeContext to calleeRealm.
- Set the ScriptOrModule of calleeContext to func.[[ScriptOrModule]].
- Let localEnv be NewFunctionEnvironment(func, newTarget).
- Set the LexicalEnvironment of calleeContext to localEnv.
- Set the VariableEnvironment of calleeContext to localEnv.
- Set the PrivateEnvironment of calleeContext to func.[[PrivateEnvironment]].
- If callerContext is not already suspended, suspend callerContext.
- Push calleeContext onto the execution context stack; calleeContext is now the running execution context.
- Return calleeContext.
— §10.2.1.1 PrepareForOrdinaryCall
ここで NewFunctionEnvironment が 新しい Function ER を作り、新 EC の
LexicalEnvironment がそれを指します。この時点では Function ER の
[[ThisBindingStatus]] は uninitialized です([[ThisValue]] はまだ未定)。
OrdinaryCallBindThis: [[ThisValue]] を書き込む
ここが this の正体です。
原文:
- Let thisMode be func.[[ThisMode]].
- If thisMode is lexical, return unused.
- Let calleeRealm be func.[[Realm]].
- Let localEnv be the LexicalEnvironment of calleeContext.
- If thisMode is strict, then
a. Let thisValue be thisArgument.- Else,
a. If thisArgument is either undefined or null, then
i. Let globalEnv be calleeRealm.[[GlobalEnv]].
ii. Assert: globalEnv is a Global Environment Record.
iii. Let thisValue be globalEnv.[[GlobalThisValue]].
b. Else,
i. Let thisValue be ! ToObject(thisArgument).
ii. NOTE: ToObject produces wrapper objects using calleeRealm.- Assert: localEnv is a Function Environment Record.
- Assert: The next step never returns an abrupt completion because localEnv.[[ThisBindingStatus]] is not initialized.
- Perform ! BindThisValue(localEnv, thisValue).
- Return unused.
— §10.2.1.2 OrdinaryCallBindThis
[[ThisMode]] は関数オブジェクトの内部スロットで、3つの値を取ります:
-
lexical: Arrow Function → step 2 で即 return(5章で詳説) -
strict: strict mode の通常関数 →thisArgumentをそのまま使う -
global: sloppy mode の通常関数 →null/undefinedを globalThis に置換、プリミティブを Object 化(境界引き)
そして最後の BindThisValue が、Function ER の [[ThisValue]] スロットに値を
書き込みます。
原文:
- Assert: envRec.[[ThisBindingStatus]] is not lexical.
- If envRec.[[ThisBindingStatus]] is initialized, throw a ReferenceError exception.
- Set envRec.[[ThisValue]] to value.
- Set envRec.[[ThisBindingStatus]] to initialized.
- Return unused.
— §9.1.1.3.1 BindThisValue
これで Function ER の [[ThisValue]] = counter、[[ThisBindingStatus]] = initialized
になります。本体内の this.n++ の this が、この [[ThisValue]] を引きます
(2章の GetThisEnvironment)。
ステップ図シリーズで counter.inc() を追う
ここまでの流れを6枚のスナップショットで追います。
step 0: 呼び出し直前。EC スタックには Global EC のみ。counter オブジェクトと
inc の関数オブジェクトが存在し、関数オブジェクトの [[ThisMode]] は strict。
step 1: counter.inc を評価。Reference Record が生成され、[[Base]] = counter、
[[ReferencedName]] = "inc"。GetValue で inc の関数オブジェクトを取得しつつ、
Reference Record も保持。
step 2: EvaluateCall が走り、Reference Record の [[Base]] から
thisValue = counter を決定。Call(func, counter, []) を呼ぶ。
step 3: PrepareForOrdinaryCall が走り、新しい EC が EC スタックに push される。
Function ER も作られ、[[ThisBindingStatus]] = uninitialized。
step 4: OrdinaryCallBindThis が走る。[[ThisMode]] = strict なので
thisValue = counter。BindThisValue(counter) で Function ER の [[ThisValue]]
が counter、[[ThisBindingStatus]] が initialized になる。
step 5: 本体 this.n++ が評価される。this 式は ResolveThisBinding →
GetThisEnvironment で running EC の LexicalEnvironment(= 上で作った Function ER)
に到達し、[[ThisValue]] = counter を返す。
この章のまとめ
関数呼び出しは内部的に [[Call]] → PrepareForOrdinaryCall →
OrdinaryCallBindThis → OrdinaryCallEvaluateBody の順で進み、this は
OrdinaryCallBindThis の中で BindThisValue を経由して Function ER の
[[ThisValue]] に書き込まれる。
そして OrdinaryCallBindThis の step 1 が「[[ThisMode]] が lexical なら
即 return」となっていることに注目してください。これが次章の Arrow Function の
正体です。
5. Arrow Function — [[ThisMode]] = lexical
「Arrow Function は自分の this を持たない」「外側の this を引き継ぐ」という
説明はよく見ますが、ECMAScript の仕様では 「this を持たない」のではなく
「this を書き込む処理をスキップする」 と表現されます。
関数オブジェクトの [[ThisMode]] スロット
ECMAScript 関数オブジェクトは内部スロットの一つとして [[ThisMode]] を持ち、
これは関数の 作成時 に決まります。
原文:
[[ThisMode]] — Defines how this references are interpreted within the formal
parameters and code body of the function. lexical means that this refers to the
this value of a lexically enclosing function. strict means that the this value is
used exactly as provided by an invocation of the function. global means that a
this value of undefined or null is interpreted as a reference to the global object,
and any other this value is first passed to ToObject.
— §10.2 ECMAScript Function Objects, Table: Internal Slots
[[ThisMode]] |
どの関数か |
|---|---|
lexical |
Arrow Function |
strict |
strict mode の通常関数・メソッド |
global |
sloppy mode の通常関数(本記事では境界引きで深入りしない) |
Arrow Function は InstantiateArrowFunctionExpression で [[ThisMode]] = lexical
として生成されます。
原文:
ArrowFunction : ArrowParameters => ConciseBody
- If name is not present, set name to the empty String.
- Let env be the LexicalEnvironment of the running execution context.
- Let privateEnv be the running execution context's PrivateEnvironment.
- Let sourceText be the source text matched by ArrowFunction.
- Let closure be OrdinaryFunctionCreate(%Function.prototype%, sourceText,
ArrowParameters, ConciseBody, lexical-this, env, privateEnv).- Perform SetFunctionName(closure, name).
- Return closure.
— §15.3.4 Runtime Semantics: InstantiateArrowFunctionExpression
OrdinaryFunctionCreate が lexical-this 引数を受け取ると [[ThisMode]] を
lexical にセットします。Arrow Function かどうかは、パースの段階で決まる
関数オブジェクト側の性質 であって、呼び方の問題ではありません。
OrdinaryCallBindThis の早期 return
4章で見た OrdinaryCallBindThis の step 1〜2 を再掲します。
原文:
- Let thisMode be func.[[ThisMode]].
- If thisMode is lexical, return unused.
— §10.2.1.2 OrdinaryCallBindThis
たった1行です。Arrow Function は OrdinaryCallBindThis の step 2 で
即 return し、Function ER の BindThisValue を呼びません。
その結果:
- Function ER の
[[ThisValue]]は 書き込まれない -
[[ThisBindingStatus]]はuninitializedのまま
this 式の評価で外側に走査が伸びる
本体内に this が出てきたとき、2章で見た GetThisEnvironment が走ります。
原文:
- Let env be the running execution context's LexicalEnvironment.
- Repeat,
a. Let exists be env.HasThisBinding().
b. If exists is true, return env.
c. Let outer be env.[[OuterEnv]].
d. Assert: outer is not null.
e. Set env to outer.
— §9.4.3 GetThisEnvironment
ここで効くのが、Function ER の HasThisBinding の定義です。
原文:
- If envRec.[[ThisBindingStatus]] is lexical, return false.
- Return true.
— §9.1.1.3.2 Function Environment Records: HasThisBinding
Arrow Function は OrdinaryFunctionCreate で Function ER 作成時に
[[ThisBindingStatus]] = lexical がセットされており、HasThisBinding は
false を返します。つまり GetThisEnvironment は その Function ER を
スキップし、[[OuterEnv]] を辿って外側に走査を伸ばします。
外側にあるのは Arrow Function を定義した時点の ER(外側の関数の Function ER か、
Global ER)。そこに到達したら、その ER の [[ThisValue]] を読みます。これが
「外側の this を引き継ぐ」の正体です。
1章の Arrow Function 例を再考する
1章で見た次の例を思い出してください。
"use strict";
const outer = {
name: "outer",
arrow: () => this?.name ?? "(no this)",
};
outer.arrow(); // "(no this)"
outer.arrow() は3章の Reference Record の理屈で thisValue = outer を作り、
Call(arrow, outer, []) を呼びます。しかし arrow の [[ThisMode]] = lexical な
ため、OrdinaryCallBindThis は step 2 で即 return し、outer という情報は
完全に捨てられます。
this 式の評価では、外側のスコープ(モジュールトップレベルなら this は
undefined)を読みに行くため、(no this) が返ります。
つまり、ドットで呼んでも、call で this を渡しても、Arrow Function には
何も届きません。
この章のまとめ
Arrow Function は呼び出し時に OrdinaryCallBindThis を早期 return することで
Function ER の [[ThisValue]] を未初期化のままにし、結果として this 式は
GetThisEnvironment が外側の ER まで走査を伸ばす。
これが「自分の this を持たない」「外側の this を引き継ぐ」の仕組みです。
this の 値 をどうこうしているのではなく、this の 保存処理そのものを
スキップ しているのです。
6. call / apply / bind — thisArgument の明示と BoundFunctionExoticObject
3章で見た通り、this の元となる thisValue は通常、Reference Record の
[[Base]] から作られます。Function.prototype の call / apply / bind
の3メソッドは、この経路をバイパスして thisArgument を明示的に渡す ための
仕組みです。
call — Call を直接呼ぶラッパ
原文:
- Let func be the this value.
- If IsCallable(func) is false, throw a TypeError exception.
- Perform PrepareForTailCall().
- Return ? Call(func, thisArg, args).
— §20.2.3.3 Function.prototype.call
f.call(thisArg, ...) は、内部的にはほぼ Call(f, thisArg, args) です。
3章で EvaluateCall が決めていた thisValue を、ユーザーが直接指定する形に
なっただけです。
apply — call の args を配列で渡す版
原文:
- Let func be the this value.
- If IsCallable(func) is false, throw a TypeError exception.
- If argArray is either undefined or null, then
a. Perform PrepareForTailCall().
b. Return ? Call(func, thisArg).- Let argList be ? CreateListFromArrayLike(argArray).
- Perform PrepareForTailCall().
- Return ? Call(func, thisArg, argList).
— §20.2.3.1 Function.prototype.apply
call との違いは引数リストの受け渡し形式だけで、this の渡し方は同じです。
bind — BoundFunctionExoticObject を作る
bind は その場では呼ばず、this と先頭の引数を固定した新しい関数オブジェクトを
返します。この返り値は普通の関数オブジェクトではなく
BoundFunctionExoticObject という特別な種類です。
原文:
- Let target be the this value.
- If IsCallable(target) is false, throw a TypeError exception.
- Let boundFunc be ? BoundFunctionCreate(target, thisArg, args).
- ... (length と name の設定が続く)
- Return boundFunc.
— §20.2.3.2 Function.prototype.bind
BoundFunctionExoticObject は通常の関数オブジェクトと違う内部スロットを持ちます。
原文:
[[BoundTargetFunction]] — The wrapped function object.
[[BoundThis]] — The value that is always passed as the this value when calling the wrapped function.
[[BoundArguments]] — A list of values whose elements are used as the first arguments
to any call to the wrapped function.
— §10.4.1 Bound Function Exotic Objects
そして bound 関数の [[Call]] は、内部のターゲット関数を [[BoundThis]] 付きで
呼ぶだけです。
原文:
- Let target be func.[[BoundTargetFunction]].
- Let boundThis be func.[[BoundThis]].
- Let boundArgs be func.[[BoundArguments]].
- Let args be the list-concatenation of boundArgs and argumentsList.
- Return ? Call(target, boundThis, args).
— §10.4.1.1 [[Call]] of Bound Function Exotic Objects
ここで重要なのは step 5 の Call(target, boundThis, args) です。呼び出し側から
渡された thisArgument は無視され、[[BoundThis]] が代わりに使われます。
"use strict";
const f = function () { return this?.name ?? "(no this)"; };
const bound = f.bind({ name: "bound" });
bound.call({ name: "ignored" }); // "bound" ← call の thisArg は無視
これは bound 関数の [[Call]] が、外から渡された thisArgument を 使わない ように
書かれているからです。
3つを横並びで見る
| 経路 |
thisArgument の出どころ |
|---|---|
obj.f() |
Reference Record [[Base]] = obj(3章) |
f.call(x, ...) |
第1引数 x
|
f.apply(x, [...]) |
第1引数 x
|
bound = f.bind(x); bound() |
bound.[[BoundThis]] = x |
Arrow Function に bind しても無視される理由
bind で返るのは BoundFunctionExoticObject で、その [[Call]] は内部のターゲット
関数の [[Call]] を [[BoundThis]] 付きで呼ぶだけです。でもターゲットが Arrow
Function なら、その [[Call]] が呼ぶ OrdinaryCallBindThis は step 2 で即 return し、
[[BoundThis]] は捨てられます。
"use strict";
const arrow = () => this;
const bound = arrow.bind({ name: "X" });
bound(); // undefined(モジュールトップレベル)
これは Arrow Function の [[ThisMode]] = lexical が bind より優先される
(というより、bind 後段の OrdinaryCallBindThis で必ず効く)ためです。
この章のまとめ
call / apply / bind の3つは、Reference Record の [[Base]] を経由する
代わりに、[[Call]] の thisArgument 引数を直接指定するための異なる経路に
すぎない。最終的に OrdinaryCallBindThis を経由して Function ER の
[[ThisValue]] に書き込まれる、というメインの流れは変わりません。
7. new と class — [[Construct]] と未初期化 this
通常の関数呼び出しは関数オブジェクトの [[Call]] を呼びますが、new f(...) は
[[Call]] ではなく [[Construct]] を呼びます。this の作られ方も微妙に
違い、特に class extends した派生クラスでは「super() 前は this が読めない」
という独特の振る舞いがあります。これも [[ThisBindingStatus]] で説明できます。
new は [[Construct]] を呼ぶ
原文:
- Let callerContext be the running execution context.
- Let kind be func.[[ConstructorKind]].
- If kind is base, then
a. Let thisArgument be ? OrdinaryCreateFromConstructor(newTarget, "%Object.prototype%").- Let calleeContext be PrepareForOrdinaryCall(func, newTarget).
- Assert: calleeContext is now the running execution context.
- If kind is base, then
a. Perform OrdinaryCallBindThis(func, calleeContext, thisArgument).
b. Let initializeResult be Completion(InitializeInstanceElements(thisArgument, func)).
c. If initializeResult is an abrupt completion, then ...- Let constructorEnv be the LexicalEnvironment of calleeContext.
- Let result be Completion(OrdinaryCallEvaluateBody(func, argumentsList)).
- Remove calleeContext from the execution context stack and restore callerContext as the running execution context.
- If result is a throw completion, then return ? result.
- Assert: result is a return completion.
- If result.[[Value]] is an Object, return result.[[Value]].
- If kind is base, return thisArgument.
- If result.[[Value]] is not undefined, throw a TypeError exception.
- Let thisBinding be ? constructorEnv.GetThisBinding().
- Assert: thisBinding is an Object.
- Return thisBinding.
— §10.2.2 [[Construct]]
ここで重要な分岐が2つあります:
-
base class(
extendsなし、または通常関数のnew f()): step 3 で 新しい
オブジェクトを作ってthisArgumentに渡し、step 6 でOrdinaryCallBindThisを
呼ぶ。Function ER の[[ThisValue]]は 本体評価開始時に既に書き込まれている -
derived class(
extendsあり): step 3 と step 6 を 両方スキップ。Function ER
の[[ThisBindingStatus]]はuninitializedのまま本体評価が始まる
base class の new
class Counter {
constructor() {
this.n = 0; // OK: [[ThisValue]] は既に新しいオブジェクト
}
}
new Counter();
[[ConstructorKind]] = base なので、[[Construct]] の step 3 で
OrdinaryCreateFromConstructor が 新しい空オブジェクト を作り、それが
thisArgument になります。step 6 の OrdinaryCallBindThis で Function ER の
[[ThisValue]] にセットされ、本体に入った時点で this は使えます。
derived class の new
class Sub extends Counter {
constructor() {
// this.x = 1; // ← ReferenceError: 'this' is not defined
super();
this.x = 1; // OK: super() で初期化された
}
}
new Sub();
[[ConstructorKind]] = derived の場合、[[Construct]] の step 3 と step 6 は
スキップされます。よって 本体評価開始時、Function ER の [[ThisBindingStatus]]
は uninitialized のまま です。
ここで本体内の this 式が走ると ResolveThisBinding → Function ER の
GetThisBinding が呼ばれます。
原文:
- Assert: envRec.[[ThisBindingStatus]] is not lexical.
- If envRec.[[ThisBindingStatus]] is uninitialized, throw a ReferenceError exception.
- Return envRec.[[ThisValue]].
— §9.1.1.3.3 Function Environment Records: GetThisBinding
step 2 で uninitialized なら ReferenceError。これが super() 前に this を
読むと ReferenceError になる正体です。「this が undefined になる」のでも
「TDZ に入っている」のでもなく、仕様レベルでは [[ThisBindingStatus]]
スロットが uninitialized 状態である ことが理由です。
super() が BindThisValue を呼ぶ
super()(SuperCall)は、親クラスのコンストラクタを呼んで戻ってきたオブジェクトを
[[ThisValue]] にセットします。
原文:
SuperCall : super Arguments
- Let newTarget be GetNewTarget().
- Assert: newTarget is a constructor.
- Let superConstructor be GetSuperConstructor().
- Let argList be ? ArgumentListEvaluation of Arguments.
- If IsConstructor(superConstructor) is false, throw a TypeError exception.
- Let result be ? Construct(superConstructor, argList, newTarget).
- Let thisER be GetThisEnvironment().
- Assert: thisER is a Function Environment Record.
- Perform ? BindThisValue(thisER, result).
- Let funcObj be thisER.[[FunctionObject]].
- Assert: funcObj is an ECMAScript function object.
- Perform ? InitializeInstanceElements(result, funcObj).
- Return result.
— §13.3.7.1 SuperCall: Runtime Semantics: Evaluation
step 9 の BindThisValue が、derived class の Function ER に
親コンストラクタが返した新オブジェクトを書き込みます。これで
[[ThisBindingStatus]] = initialized になり、以降 this が使えるようになります。
ちなみに BindThisValue の step 2 を再掲します。
原文:
- Assert: envRec.[[ThisBindingStatus]] is not lexical.
- If envRec.[[ThisBindingStatus]] is initialized, throw a ReferenceError exception.
...
— §9.1.1.3.1 BindThisValue
initialized 状態の Function ER にもう一度 BindThisValue を呼ぶと
ReferenceError。これが super() を2回呼ぶと ReferenceError になる理由です。
[[ThisBindingStatus]] 3状態のまとめ
| 状態 | いつ |
this 式の挙動 |
|---|---|---|
lexical |
Arrow Function の Function ER(5章) |
HasThisBinding が false → 外側の ER に走査が伸びる |
uninitialized |
derived constructor 本体開始時、super() 前 |
GetThisBinding が ReferenceError |
initialized |
通常関数の本体内、base コンストラクタ本体内、derived コンストラクタの super() 後 |
[[ThisValue]] を返す |
この章のまとめ
new は [[Call]] とは別の経路 [[Construct]] を通り、this の初期化
タイミングが base / derived で異なる。base では本体開始時に既に
[[ThisValue]] が書き込まれているのに対し、derived では super() までは
[[ThisBindingStatus] = uninitialized のまま。だから super() 前に this を
読むと ReferenceError になる。
ここまでで this を決定する仕組みのすべての経路を見ました。次章で1章の謎に
戻ります。
8. 1章の謎の解明
ここまでの章で部品はすべて揃いました。1章のコードに戻ります。
"use strict";
const obj = {
name: "obj",
who() { return this?.name ?? "(no this)"; },
};
const f1 = () => obj.who();
const f2 = () => { const g = obj.who; return g(); };
const f3 = () => obj.who.call({ name: "X" });
f1(); // "obj"
f2(); // "(no this)"
f3(); // "X"
3つのケースに共通するのは、同じ関数オブジェクト obj.who を呼んでいることと、
その関数オブジェクトの [[ThisMode]] = strict であることです。
違うのは、[[Call]] の thisArgument 引数に何が入るか だけです。
f1: obj.who() の場合
-
obj.whoを評価 → Reference Record([[Base]] = obj、[[ReferencedName]] = "who") -
EvaluateCallがこの Reference Record を受け取り、IsPropertyReferenceが true なので
thisValue = GetThisValue(ref) = obj -
Call(who, obj, [])→who.[[Call]](obj, []) -
PrepareForOrdinaryCallで新 EC・新 Function ER を作る -
OrdinaryCallBindThis:[[ThisMode]] = strictなので step 5 に進み、
thisValue = obj。BindThisValue(obj)で Function ER の[[ThisValue]] = obj - 本体内
this→ResolveThisBinding→obj→obj.name = "obj"を返す
再掲(原文):
- If ref is a Reference Record, then
a. If IsPropertyReference(ref) is true, then
i. Let thisValue be GetThisValue(ref).
— §13.3.6.2 EvaluateCall
f2: const g = obj.who; g() の場合
-
const g = obj.who: 右辺の Reference Record はGetValueで値化 され、
gには関数オブジェクトwhoだけが入る。[[Base]] = objの情報はここで消える -
g()の評価:gという識別子の Reference Record ができる
([[Base]]は識別子が宣言された ER) -
EvaluateCallがこの Reference を受け取る。IsPropertyReferenceは false
(プロパティ参照ではなく識別子参照)なので、step 1.b に進む:
thisValue = refEnv.WithBaseObject() - 普通の Environment Record(Declarative ER / Function ER / Module ER)の
WithBaseObjectは常にundefinedを返す(with文専用の Object ER のみ
オブジェクトを返す) -
Call(who, undefined, [])→OrdinaryCallBindThisで[[ThisMode]] = strictなので
thisValue = undefinedがそのまま使われる - 本体内
this→undefined→undefined?.name ?? "(no this)"で"(no this)"
原文(参考):
- Return undefined.
— §9.1.1.1.11 Declarative Environment Records: WithBaseObject
ポイントは、「g に代入した時点で obj との結びつきが消える」 こと。これが
3章で見た「Reference は GetValue で値に潰れる」の具体例です。
f3: obj.who.call({ name: "X" }) の場合
-
obj.who.callを評価 → Reference Record([[Base]] = obj.who、[[ReferencedName]] = "call") -
EvaluateCallでthisValue = obj.who(Function.prototype.call のthis)、
args = [{name:"X"}] -
Function.prototype.callの本体(§20.2.3.3):- step 1:
func = this value = obj.who - step 4:
Call(func, thisArg, args)=Call(obj.who, {name:"X"}, [])
- step 1:
-
who.[[Call]]({name:"X"}, [])→OrdinaryCallBindThisでthisValue = {name:"X"} - 本体内
this→{name:"X"}→"X"を返す
3章の Reference の経路を 1段スキップして、Function.prototype.call 内の
Call(func, thisArg, ...) で thisArgument を明示している、というのが
仕様レベルでの説明です。
3つを一望する
| 呼び出し |
thisArgument の出どころ |
Function ER [[ThisValue]]
|
this?.name |
|---|---|---|---|
f1() |
Reference の [[Base]] = obj
|
obj |
"obj" |
f2() |
WithBaseObject() = undefined |
undefined |
→ (no this)
|
f3() |
Function.prototype.call の第1引数 |
{name:"X"} |
"X" |
「同じ関数なのに this が変わる」のではなく、「[[Call]] の thisArgument 引数に
何が渡るかが、呼び出し式の形によって違う」 だけです。this 自体の決め方は
常に同じ: OrdinaryCallBindThis が thisArgument を加工せず(strict なら)
Function ER の [[ThisValue]] に書き込み、this 式がそれを引く。
Arrow Function 例も再確認
1章の outer.arrow() も同じ枠組みで説明できます:
-
outer.arrow→ Reference Record([[Base]] = outer) -
EvaluateCallでthisValue = outer -
Call(arrow, outer, [])→arrow.[[Call]](outer, []) -
OrdinaryCallBindThis:[[ThisMode]] = lexicalなので step 2 で即 return - Function ER の
[[ThisValue]]は 書き込まれない([[ThisBindingStatus]] = lexical) - 本体内
this→ResolveThisBinding→GetThisEnvironmentがこの Function ER を
スキップして外側 ER(モジュールトップ)へ →undefined→(no this)
outer という情報は OrdinaryCallBindThis の step 2 で完全に捨てられる。
ドットで呼ぼうが call で渡そうが、Arrow Function には届きません。
この章のまとめ
「this は呼び方で変わる」のではなく、「呼び出し式が何を [[Call]] の
thisArgument として渡すか」が呼び方で違うだけ。そして OrdinaryCallBindThis
で thisArgument を Function ER の [[ThisValue]] に書き込み、this 式は
それを引く、という骨組みは常に同じ。Arrow Function はこの書き込み処理自体を
スキップし、derived constructor は super() まで書き込みを遅延する。
バリエーションは存在するが、すべて同じ仕組みの上に乗っている。
9. まとめ
本記事の内容を4項目にまとめます。
-
this式の値は、ResolveThisBindingが見つけた一番内側の Function ER(または
Global ER)の[[ThisValue]]スロットに格納されている値。thisは変数では
なくスロット参照。 -
[[ThisValue]]の中身はOrdinaryCallBindThisがthisArgumentから書き込む。
そのthisArgumentの出どころは:-
ドット呼び出し
obj.f()→ Reference Record の[[Base]] = obj -
裸の呼び出し
g()→ 識別子参照のWithBaseObject() = undefined(strict) -
call/apply→ ユーザー指定の第1引数 -
bind→BoundFunctionExoticObject.[[BoundThis]] -
new→ base なら新規オブジェクト、derived ならsuper()経由で親から戻ったオブジェクト
-
ドット呼び出し
-
Arrow Function は
[[ThisMode]] = lexicalでOrdinaryCallBindThisを早期 return する。
結果、Function ER の[[ThisValue]]は書き込まれず、HasThisBindingが
falseを返すため、this式は外側の ER まで走査が伸びる。「自分のthisを
持たない」とはこの早期 return のこと。 -
derived class(
extendsあり)の constructor はsuper()前に
[[ThisBindingStatus]] = uninitialized。GetThisBindingがこの状態を見ると
ReferenceError。super()がBindThisValueで初期化することでthisが
解禁される。
this を「呼び方によって値が変わる魔法の変数」と捉えていた方は、ぜひ
「Function ER の [[ThisValue]] スロットに OrdinaryCallBindThis が書き込む値」
と読み替えてみてください。これだけで仕様書のあちこちの記述が一直線に繋がります。



















