JavaScriptの関数(1) – 関数オブジェクト、関数オブジェクトの作成

JavaScriptで有名な開発者、ダグラス・クロックフォードの言葉を借りると、Javascriptは、地球上で最も誤解されているプログラミング言語だといえるでしょう。もちろん、現在ではその誤解がたくさん解かれていますが、それでもJavaScriptは進化し続けており、その変化を詳しく知っている人は少ないでしょう。

JavaScriptで特に誤解されている部分は、まさに関数でしょう。関数はECMAScriptが発表されてからECMAScript 5になるまで、ほとんど変化がありませんでしたが、ECMAScript 6以降、JavaScriptの関数は、既存の開発者のニーズや不満を受け入れて大きく発展しています。

関数

関数はJavaScriptだけでなく、ほとんどのプログラミング言語で核心となる機能です。そしてJavaScriptの関数はさらに強力なものになっています。
まずES5を基準に関数を考えてみよう。関数は、オブジェクトのように、関数のように、オブジェクト指向のコンストラクタのように動作し、関数に付随するプロトタイプというオブジェクトを介して共通の動作を共有します。関数を作り出すさまざまな文法-関数コンストラクタ、関数式、関数宣言式などが存在し、関数自体にスコープを定義します。関数の本体でthis参照は動的に決定され、arguments参照もthisのように動的で予約語のように使用されます。既存の暗黙的なバグやエラーを誘発していた問題を解決するため“use strict”;があり、関数はstrict code、またはnon-strict codeを区分します。言語的特性と相まってClosureや高階関数を活用したさまざま手法が存在します。

要約すると、JavaScriptの関数はそのままオブジェクト指向も可能な一級関数であると言えます。

良く捉えると、関数はさまざまな機能やテクニックを活用できるように適切に設計されたオブジェクトと言えますが、悪く捉えると、関数は非常に複雑で機能も多いと言えます。そして機能が多いだけに、意図しない動作をしたりもします。こうしてJavaScriptの関数は、JavaScriptを学ぶ上で最初のハードルになりました。

ECMAScript 6+

ECMAScript 6以降も関数は強力になり続けています。デフォルトパラメータ(Default parameter)、クラス(class)、矢印関数(Arrow function)、残りのパラメータ(Rest parameters)、nameプロパティ、new.target、ジェネレータ関数(Generator function)、async関数、末尾再帰(Tail call)、Block-Level関数などがあり、それぞれの特徴や名前を覚えるのも大変です。

このように追加されるさまざまな特徴によって、これまでかろうじて理解して活用していた関数の動作と技術は、もはや旧式のものになってしまいました。Function.prototype.bindの代わりに矢印関数を使い、functionで宣言していたクラスを今ではclassとして宣言し、ES6のPromise.prototype.thenチェーンの代わりにasync/awaitを使い、GeneratorオブジェクトとRunner関数を活用してキャンセル可能な非同期動作が実装でき、非同期動作/機能を同期的にテスト(例: Redux-Sagaのテスト)することもできます。

だから何..?

実際にES6 +の関数を使用してみましょう。いろいろ試しながら開発してみると、動作はしますが、なぜ帰るのか、回らないのか、速いのか、便利なのか、きちんと説明できないかもしれません。
このように進化した関数がどのように動作するのか、もう一度調べてみよう。

関数オブジェクト

ECMAScriptでは、関数オブジェクトとサブルーチン(Subroutine)で実行できるオブジェクトを指します。動作を表す実行コードと状態が含まれており、オブジェクト指向のコンストラクタの役割もできます。基本的にはJavaScriptの一般的なオブジェクト(Ordinary object)と同じ動作ができます(正確にはOrdinary objectのInternal slotとInteral methodをすべて持っています)。

つまり、ECMAScript関数は、通常のオブジェクトを拡張したもので、関数で動作するための追加的な機能を持っています。
関数オブジェクトは、次のようなデータを内部に追加保存します。

  1. Closureで結ばれるレキシカル環境(Lexical Environment) – [[Environment]]
  2. 関数のコード – [[ECMAScriptCode]]
  3. 関数の種類- [[FunctionKind]]: “normal”, “classConstructor”, “generator”, “async”
  4. コンストラクタの種類- [[ConstructorKind]]:”base”, “derived”
  5. this 参照タイプ – [[ThisMode]]
  6. strict modeの実行可否 – [[Strict]]
  7. super参照 – [[HomeObject]]
  8. その他

そして、実際に関数を実行する[[Call]][[Construct]]内部メソッドがあります。単純に関数を呼び出すと、関数オブジェクトの内部[[Call]]が呼び出され、newまたはsuper演算子と一緒に呼び出すと、[[Construct]]が呼び出されます。

[[Call]]が実装されたオブジェクトをcallableと呼び、[[Construct]]が実装されたオブジェクトをconstructorと呼びますが、JavaScriptの関数は、callableでありながらconstructorの場合もあります。代表的なものとして、矢印関数はcallableでありながらnon-constructorでもあります。

関数の作成

JavaScriptの関数を作成する際に、基本的に6つの情報を使用します。

  1. 関数の作成方法(種類) – NormalArrowMethod
  2. 関数のパラメータリスト
  3. 関数の本体(関数コード)
  4. スコープ(Lexical Environment)
  5. strict modeの実行可否
  6. 関数オブジェクトのプロトタイプ – FunctionPrototype、Generator、AsyncFunctionPrototypeなどのオブジェクトで使用

次のbar関数を作成する場合を考えてみよう。

function foo() { // bar関数のスコープはfooのLexical Environment

  // barは一般的な関数宣言式 - 関数作成方式はNormal、プロトタイプはFunction.prototype
  function bar(/* パラメータリスト */) {
    "use strict"; // barはstrict function

    console.log('foo'); // 関数の本体
  }
}

ECMAScriptは、このような情報に基づいて関数を作成します。

  1. 関数の作成方法でコンストラクタになれるかどうか判断する
  2. strict 可否、実際の関数オブジェクトの種類(一般的な関数(Function)か、ジェネレータ関数か、async関数か)を区別して保存する
  3. 関数オブジェクトのプロトタイプ(FunctionPrototype、Generator、AsyncFunctionPrototypeなど)を保存する
    • ここでのプロトタイプは、bind、apply、callなどのメソッドを持っている関数自体のプロトタイプである
  4. 最後にEnvironment(スコープ)、パラメータ、関数の本体、this参照方式などの情報を格納する

このとき関数を区別する方式で少し混乱するかもしれません。関数を作成するときに、関数の作成方法と関数自体の種類が2つあります。関数自体の種類は、前述の[[FunctionKind]]と同じで、関数の作成方法は作成時にのみ区別して使用し、別々に保存しません。

関数の作成方法

関数の作成方法は、ES5とES6を基準に考えると簡単です。ES5までの関数表現(functionキーワードを使用)はすべてNormalで、ES6の矢印関数はArrow、オブジェクトリテラルで、メソッドの構文はMethodとなります。

function foo() {} // Normal
const foo1 = () => {}; // Arrow
const person = {
    // ...
    sayName() {} // Method
};

関数の作成を区分する理由は、ArrowMethodの場合、コンストラクタで動作するのを防ぎ( –[[Construct]]メソッドを実装せずに)、関数のthis参照方法を決定するためです( –Arrowの場合this、キーワードはlexical参照)。

既存のES5までは、関数がコンストラクタあるいは通常の関数どちらでも呼び出し可能ですが、ES6からこのような混乱を軽減するため区分され始めました。

つまり、ECMAScript 6以降の関数をどのように作成するかによって、コンストラクタとして使用できるか決定されます。これは関数作成時、割り当て関数(FunctionAllocate)と呼ばれる段階で決定されます。

注意すべき点は、Babelのようなトランスパイラを使用している場合、ECMAScriptスペックとは異なる動作ができます。例えば、矢印関数は[[Construct]]メソッドがないため、コンストラクタで使用する場合にはエラーが発生しますが、トランスパイルが行われると、エラーが発生しないことがあります。

次のコードをChromeの開発ツールで実行してみよう。エラーが発生します。

const Foo = () => {};
var foo = new Foo(); // Uncaught TypeError: Foo is not a constructor

しかし、Babelに変換されたコードを見ると、以下のとおりエラーが発生しません。

"use strict";

var Foo = function Foo() {};
var foo = new Foo();

メソッドの構文も同様に、コンストラクタで使用する場合はエラーが発生しますが、変換されたコードはエラーが発生しません。

var obj = {
    Foo() {}
};
new obj.Foo(); // Uncaught TypeError: obj.Foo is not a constructor

以下は変換されたコードですが、エラーは発生しません。

"use strict";

var obj = {
  Foo: function Foo() {}
};

new obj.Foo();

変換されたコードは、Normal関数で処理するためにエラーが発生しません。実際のところ矢印関数やメソッドの構文でコンストラクタを定義して使用することはほとんどありませんが、このようなコードは作成しないように注意が必要です。

おわりに

JavaScriptで関数を表現する際、内部的に関数オブジェクトをどのように作成するかについて調べました。

  1. 関数は一般的なオブジェクトを特別に拡張したオブジェクトである
  2. 関数を作成する際、コンストラクタとしての使用可否を決定する
  3. よく使用されるトランスパイラはECMAScriptのエラーをすべて表現できないので注意が必要
  4. 関数が作成されるとき複数の内部データを保存するが、ここには関数のスコープや、this参照方式など関数の動作を理解するのに重要なデータがある

ECMAScriptスペックを見ると、思ったより変更点が多く、特に関数作成の部分でたくさんの変更点がありました。これから、関数が実際にどのように動作するのか、thisをどのように参照するのか、コンストラクタ関数で呼び出されるとsuperはどのように参照するのか、などの基本動作を詳しく調べて連載する予定です。

Reference

TOAST Meetup 編集部

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