JavaScriptの関数(2) – 関数呼び出し

前回の記事で、関数がどのように生成されるかについて説明しました。

  • 関数は一般的なオブジェクトの拡張である
  • 関数生成時に、その関数の役割がある程度決定される(callableとconstructor)
  • 関数生成時に保存されるデータによってスコープやthis参照方式を決定する

ECMAScriptが実際に関数オブジェクトを関数として呼び出すとき、どのように動作するのか確認してみよう。この記事では、ジェネレーター関数やasync関数など、特別な関数ではなく、一般的な関数と一般的な呼び出しについて調べてみます。

関数呼び出し – Call(F, V, [, argumentsList])

ECMAScript 2017は、関数の呼び出しをCall(F, V, [, argumentsList])で表現します。Callは関数オブジェクトの内部[[Call]]メソッドを実行する動作(Abstract operation)で、F, V, argumentsListを引数として受け取ります。Fは関数オブジェクト、V[[Call]]this値、argumentsListは関数呼び出し時に渡した引数で、デフォルト値は空のリストです。

  1. argumentsListが配信されていない場合は空のリストで指定する
  2. FCallableでなければ、エラーを発生させる
  3. F.[[Call]](V, argumentsList)の実行結果を返す

3から分かるように、実質的な関数の呼び出しと動作は、F.[[Call]]に示されます。F.[[Call]]を調べてみよう。
なお、ECMAScriptで関数を呼び出す動作を上記のように表現しただけで、実際のJavaScriptエンジンがECMAScript 2017のCallという演算を上記のとおり定義したものではありません。

関数オブジェクトの[[Call]] – F.[[Call]](thisArgument, argumentsList)

[[Call]]は関数オブジェクトの内部メソッド(Internal method)です。引数としてthis値とargumentsListを受け取ります。

  1. F.[[FunctionKind]]が「classConstructor」であれば、エラーを発生させる
  2. callerContextは現在実行中の実行コンテキスト(running execution context)
  3. callerContextに新しい実行コンテキストを作成して指定する(PrepareForOrdinaryCall)
  4. ( – assertion:新規作成されたcallerContextが、現在のrunning execution contextである)
  5. thisをバインドする(OrdinaryCallBindThis)
  6. 関数のコードを実行し、resultにその結果を保存する(OrdinaryCallEvaluateBody)
  7. callerContextをexecution context stackで除去し、callerContextを再びrunning execution contextで指定する
  8. resultを返却する

上記の動作では、execution contextexecution context stackrunning exeuction contextの内容が少し難解ですね。
関数呼び出しをもう少し簡単にまとめると次のようになります。

  1. 関数を呼び出すと、それに合わせて関数を実行できる環境を作り初期化する
  2. thisをバインドする(thisがどのオブジェクトを参照すべきか決める)
  3. 実際の関数のコードを実行し、その結果をresultに保存する
  4. この関数を呼び出したところ(環境)に戻り、
  5. resultを返却する

Execution Context

Execution Context(以下EC)は、スコープ(識別子の名前と値のマッチング)と基本オブジェクト(intrinsic objects – Array、Objectなどの基本的なコンストラクタとそのプロトタイプなど)を有するRealmなどのコード実行環境に複数の情報を持っている装置だと思えばよいでしょう。結局、ECはECMAScriptでコードを実行する(evaluation)メカニズムを表現するもので、実際のスクリプトエンジンは、この仕様と完全に一致しません。

Javascriptがシングルスレッド環境でコードをコンパイルして実行するとき、call stackを作るようにEC Stackを作ると考えてみよう。最も根底にはGlobalコード環境のECがあり、その上に関数が呼び出されるたび、それに合わせてECが1つずつ追加/除去を繰り返します。そして、各時点でstackの最上位にあるECがrunning execution contextです。

次のようなコードからEC stackがどのように変化するか調べてみよう。

// global

function foo() {

  function bar() {     
    return 'bar';
  }

  return bar();
}

foo();
                      |--------|
                      | bar    |
           |--------| |--------| |--------| 
           | foo    | | foo    | | foo    | 
|--------| |--------| |--------| |--------| |--------|
| global | | global | | global | | global | | global |
|--------| |--------| |--------| |--------| |--------|

ECMAScriptコードを実行するECにはLexicalEnvironmentとVariableEnvrironmentというコンポーネントが存在します。簡単に変数の参照を記録する環境だと考えればよいでしょう。LexicalEnvironmentとVariableEnvironmentが互いに分かれていますが、実際には初期化時に同じオブジェクトを見ています。withのような特別な文章に出会うと、そのblock内部では新しく作成されたLexicalEnvironmentを参照します。
(よくみると、デフォルト値parameterもLexical / VariableEnvironmentと関連があります。今後の記事でEnvironment、Environment Record、thisについて詳しく紹介する予定です。)

OrdinaryCall

[[Call]]の動作でOrdinaryCallという言葉が3回登場しました。

  1. PrepareForOrdinayCall
  2. OrdinaryCallBindThis
  3. OrdinaryCallEvaluateBody

上3つはECMAScriptで定義している内部動作なので1つずつ調べる必要があるようです。

PrepareForOrdinayCall

PrepareForOrdinayCallは、ECを作成し初期化させる内容の動作を抽象的に表現したもので、次の3つのステップを有します。(Realmは、この記事では大きな関連がないため説明は省きます。)

  1. 新しいECを生成する(calleeContext
  2. calleeContextに入るLexical Environmentを生成する
  3. calleeContextをEC Stackに追加(push)する。したがってcalleeContextがrunning execution contextになる

OrdinayCallBindThis

OrdinayCallBindThisは関数オブジェクトの[[ThisMode]]によるthis値の参照を決定します。矢印関数(Arrow Function)、Strict mode、Environmentとつながります。

  1. [[ThisMode]]がlexicalの場合は、他の処理をしない(arrow function)
  2. [[ThisMode]]がstrictの場合は、引数として渡されたthisArgumentをEnvironment recordに設定する
  3. [[ThisMode]]がlexicalでもなくstrictでもない場合は、globalの[[thisValue]]をEnvironment recordに設定する

OrdinaryCallEvaluateBody

OrdinaryCallEvaluateBodyは次の2つの動作に分けられます。

  1. 変数宣言の初期化(FunctionDeclarationInstantiation)
  2. コード実行及び結果返却

変数宣言の初期化は、最終的にEnvironment Recordを満たす動作で、かなり複雑な動作が作成されています。特にargumentsオブジェクトの必要可否、デフォルトのパラメータの有無によって分岐し、最終的には、ECのLexicalEnvironmentとVariableEnvironmentにつながります。

まとめ

関数の呼び出しを理解するには、ECとEC内部のLexicalEnvironment、VariableEnvironmentを理解する必要があります。そしてECMAScriptの関数を理解してコードを作成すると、自由度の高いJavaScriptとなり、予期せぬバグを防ぐのに大いに役立つことでしょう。
次回は、EnvironmentとRecord、変数宣言の初期化などについて詳しく紹介します。少し予告すると、下にあるJSコードがどのように実行され、なぜエラーが発生しているか、その理由が分かるようになります。

function add(a, b) {
  return a + b;
}

function foo(a, b = add(a, 1)) {
  return `foo ${a + b}`;
}

function bar(a = add(b, 1), b) {
  return `bar ${a + b}`;
}

console.log(foo(1)); // foo 3
console.log(bar(undefined, 1)); // Error

Reference

TOAST Meetup 編集部

TOASTの技術ナレッジやお得なイベント情報を発信していきます
pagetop