🏭

Functionのサブクラス (JavaScript)

2021/09/16に公開

現代のJSで Function コンストラクタを直接触ることはほとんどないと思いますが、隠された機能がちょっと面白いので本記事で説明します。

Function コンストラクタ

JavaScriptの Function 関数は厳密にはクラスではありませんが、クラスのように使うことができます。

// typeof object === "function" とするべきだが、これでもほぼ同様に判定できる
if (object instanceof Function) {
  // ...
}

// ソースコードから関数を生成
const f = new Function("x", "\"use strict\"; return x + 1");
console.log(f(42)); // => 43

// newが無くても同じ
const f = Function("x", "\"use strict\"; return x + 1");

Function コンストラクタは eval の比較的安全な代替としてよく使われます。

ユーザー定義サブクラス

ほとんど使う機会はありませんが、 Function のサブクラスを作ることができます。

class MyFunction extends Function {
  twice() {
    this();
    this();
  }
}
const f = new MyFunction("console.log('Hello!');");
f.twice(); // 2回呼ばれる

組み込みのサブクラス

AsyncFunction, GeneratorFunction, AsyncGeneratorFunction というサブクラス (正確にはクラスではない) が存在します。それぞれ Function の直接のサブクラスです。

図: Function, AsyncFunction, GeneratorFunction, AsyncGeneratoFunction

これらはグローバル変数として露出されていないため、以下のようにして抽出する必要があります。

const AsyncFunction = (async()=>0).constructor;
const GeneratorFunction = (function*(){}).constructor;
const AsyncGeneratorFunction = (async function*(){}).constructor;

以下のような構文が各クラスのインスタンスになっています。

  • Functionの直接のインスタンス
    • function式、function宣言
    • class式、class宣言
    • メソッド
    • アロー関数式
  • AsyncFunctionの直接のインスタンス
    • async function式、async function宣言
    • asyncメソッド
    • asyncアロー関数式
  • GeneratorFunctionの直接のインスタンス
    • function* 式、 function* 宣言
    • ジェネレーターメソッド
  • AsyncGeneratorFunctionの直接のインスタンス
    • async function* 式、 async function* 宣言
    • asyncジェネレーターメソッド

これらのサブクラスのコンストラクタを使うと、awaitやyieldを含んだ処理のevalが可能です。

new AsyncFunction("await 1");
new GeneratorFunction("yield 1");
new AsyncGeneratorFunction("await yield");

こちらもユーザー定義のクラスに継承させることが可能です。

トランスパイラ対応

Babelなどのトランスパイラはasync functionやジェネレーターを通常の関数にトランスパイルすることができます。ターゲットブラウザによっては AsyncFunction などのコンストラクタインスタンスを正しく取得できない可能性があります。もちろん、Webブラウザが実際にこれらの構文に対応していなければ根本的に不可能です。

対応方法のひとつとして、トランスパイルできないようにevalで括ってしまう方法が考えられます。

const AsyncFunction = Function("return (async()=>0).constructor")();
const GeneratorFunction = Function("return (function*(){}).constructor")();
const AsyncGeneratorFunction = Function("return (async function*(){}).constructor")();

もう1つの方法として生成後に値の正当性をチェックするというのが考えられます。

if (AsyncFunction.name !== "AsyncFunction") throw new Error("Not an AsyncFunction");
// あるいは
if (AsyncFunction === Function) throw new Error("Not an AsyncFunction");

Discussion