Edited at

騙されるな。JavaScript の function は関数じゃない。コンストラクタだ。

JavaScript を使っている、あるいは少しでもかじったことのある人なら、

function foo() {

/* ... */
}

const foo = function() {

/* ... */
};

といった書き方を目にしたことがあると思います。また、普段から「関数」として使っている人も多いと思います。

しかし、結論から言うと、JavaScript の function は普通の「関数」ではなく「コンストラクタ」です。JavaScriptの構文・キーワードに騙されてはいけません。(私自身、少し勘違いをしていて、以前書いた記事(JavaScript初心者にはfunctionよりも、まずアロー関数を教えるべき)では function は「メソッド」だと主張していました。)

newができるからコンストラクタって言いたいだけなんでしょう」と思った方、とりあえずブラウザバックしようとする手を止めて最後までお読みください。

function を「関数」として使用すると、状況によってはパフォーマンスに影響する可能性があります(若干誇張表現です。最適化の度合いによります)。

以下、function 文・function 式で定義した関数を便宜上「function 関数」と呼びます。また、ECMAScript® 2018 Language Specification を参照・引用します。英語の仕様書に抵抗のある方は適当に読み飛ばしてください。最後の方には日本語も書いています。


function 関数の作られ方

function 文で関数オブジェクトがどのように作られるかは、14.1.20 Runtime Semantics: InstantiateFunctionObject で説明されています。


With parameter scope.

FunctionDeclaration: function BindingIdentifier ( FormalParameters ) { FunctionBody }


  1. If the function code for FunctionDeclaration is strict mode code, let strict be true. Otherwise let strict be false.

  2. Let name be StringValue of BindingIdentifier.

  3. Let F be FunctionCreate(Normal, FormalParameters, FunctionBody, scope, strict).

  4. Perform MakeConstructor(F).

  5. Perform SetFunctionName(F, name).

  6. Return F.

FunctionDeclaration: function ( FormalParameters ) { FunctionBody }


  1. Let F be FunctionCreate(Normal, FormalParameters, FunctionBody, scope, true).

  2. Perform MakeConstructor(F).

  3. Perform SetFunctionName(F, "default").

  4. Return F.


function 式の方は14.1.21 Runtime Semantics: Evaluation にあります。


FunctionExpression: function ( FormalParameters ) { FunctionBody }


  1. If the function code for FunctionExpression is strict mode code, let strict be true. Otherwise let strict be false.

  2. Let scope be the LexicalEnvironment of the running execution context.

  3. Let closure be FunctionCreate(Normal, FormalParameters, FunctionBody, scope, strict).

  4. Perform MakeConstructor(closure).

  5. Return closure.

FunctionExpression: function BindingIdentifier ( FormalParameters ) { FunctionBody }


  1. If the function code for FunctionExpression is strict mode code, let strict be true. Otherwise let strict be false.

  2. Let scope be the running execution context's LexicalEnvironment.

  3. Let funcEnv be NewDeclarativeEnvironment(scope).

  4. Let envRec be funcEnv's EnvironmentRecord.

  5. Let name be StringValue of BindingIdentifier.

  6. Perform envRec.CreateImmutableBinding(name, false).

  7. Let closure be FunctionCreate(Normal, FormalParameters, FunctionBody, funcEnv, strict).

  8. Perform MakeConstructor(closure).

  9. Perform SetFunctionName(closure, name).

  10. Perform envRec.InitializeBinding(name, closure).

  11. Return closure.


関数オブジェクトの構築に関わり、上記の全てのアルゴリズムで実行されているのは


  • Let F be FunctionCreate(Normal, FormalParameters, FunctionBody, scope, strict).

  • Perform MakeConstructor(F).

の2つです。


FunctionCreate


9.2.5 FunctionCreate ( kind, ParameterList, Body, Scope, Strict [ , prototype ] )

(中略)


  1. If prototype is not present, then


    1. Set prototype to the intrinsic object %FunctionPrototype%.



  2. If kind is not Normal, let allocKind be "non-constructor".

  3. Else, let allocKind be "normal".

  4. Let F be FunctionAllocate(prototype, Strict, allocKind).

  5. Return FunctionInitialize(F, kind, ParameterList, Body, Scope).


ここでは、FunctionCreate(Normal, FormalParameters, FunctionBody, scope, strict)と呼び出されているので、


  • Let F be FunctionAllocate(%FunctionPrototype%, strict, "normal").

  • Return FunctionInitialize(F, Normal, FormalParameters, FunctionBody, scope).

が実行されることになります。ここで注目するのはFunctionAllocateの処理です。


FunctionAllocate


9.2.3 FunctionAllocate ( functionPrototype, strict, functionKind )

(中略)


  1. Assert: Type(functionPrototype) is Object.

  2. Assert: functionKind is either "normal", "non-constructor", "generator", "async", or "async generator".

  3. If functionKind is "normal", let needsConstruct be true.

  4. Else, let needsConstruct be false.

  5. If functionKind is "non-constructor", set functionKind to "normal".

  6. Let F be a newly created ECMAScript function object with the internal slots listed in Table 27. All of those internal slots are initialized to undefined.

  7. Set F's essential internal methods to the default ordinary object definitions specified in 9.1.

  8. Set F.[[Call]] to the definition specified in 9.2.1.

  9. If needsConstruct is true, then


    1. Set F.[[Construct]] to the definition specified in 9.2.2.

    2. Set F.[[ConstructorKind]] to "base".



  10. Set F.[[Strict]] to strict.

  11. Set F.[[FunctionKind]] to functionKind.

  12. Set F.[[Prototype]] to functionPrototype.

  13. Set F.[[Extensible]] to true.

  14. Set F.[[Realm]] to the current Realm Record.

  15. Return F.


ここで、functionKindには"normal"が渡されるので、needsConstructtrueになります(3.)。よって、F.[[Construct]]スロットが設定されます(9.1.)。

[[Construct]]スロットによって、function 関数はコンストラクタとして呼び出すことが可能になっています。


MakeConstructor


9.2.10 MakeConstructor ( F [ , writablePrototype [ , prototype ] ] )

(中略)


  1. Assert: F is an ECMAScript function object.

  2. Assert: IsConstructor(F) is true.

  3. Assert: F is an extensible object that does not have a prototype own property.

  4. If writablePrototype is not present, set writablePrototype to true.

  5. If prototype is not present, then


    1. Set prototype to ObjectCreate(%ObjectPrototype%).

    2. Perform ! DefinePropertyOrThrow(prototype, "constructor", PropertyDescriptor { [[Value]]: F, [[Writable]]: writablePrototype, [[Enumerable]]: false, [[Configurable]]: true }).



  6. Perform ! DefinePropertyOrThrow(F, "prototype", PropertyDescriptor { [[Value]]: prototype, [[Writable]]: writablePrototype, [[Enumerable]]: false, [[Configurable]]: false }).

  7. Return NormalCompletion(undefined).


ここではパラメータprototypeは省略されているので、まず新しいオブジェクトを作ってからそれをprototypeに代入しています(5.)。prototypeは最終的に関数Fprototypeプロパティに格納されます(6.)。


結論

JavaScript の function 関数は「関数」ではなくて「コンストラクタ」です。理由は上で挙げた通り、



  1. [[Construct]]スロットが設定されていて、コンストラクタとして呼び出せる

  2. コンストラクタとして使用するためのprototypeプロパティに新しく作成されたオブジェクトが必ず設定される

1.はそのままです。ただnewを付けて呼び出したりできるというだけです。

2.がどういうことかと言うと、function 関数オブジェクトが作られる際にprototypeプロパティに設定するためだけに新しいオブジェクトが作られるということです。prototypeプロパティはコンストラクタ以外には必要ないにもかかわらず、全ての function 関数に設定されてしまいます。しかも新しいオブジェクトが作成されるので、その分のコストもかかってしまいます。

function 関数を「関数」として使った場合、弊害が出てくる可能性があります。

例えば、以下のようなコードでは

const arr = [1, 2, 3, 4, 5];

for (let i = 1; i <= 5; i++) {
console.log(arr.map(function(n) {
return n * i;
}));
}

普通にコールバック関数を渡しているだけのつもりで、実際はループの度に余分なprototypeプロパティのオブジェクトが作成されてしまいます。Array.prototype.mapのような組み込み関数のコールバックの場合はおそらく最適化できると思いますが、全てのケースで最適化が使えるわけではありません。

アロー関数を使いましょう。

大事なことなのでもう一度言います。

アロー関数を使いましょう。

アロー関数はコンストラクタとして使えないようになっているので、普通の「関数」として問題なく使うことができます。

const arr = [1, 2, 3, 4, 5];

for (let i = 1; i <= 5; i++) {
console.log(arr.map(n => n * i));
}

さらに、アロー関数には可読性が向上するという利点があります。

逆に普通の「関数」にアロー関数を使わない理由はありません。積極的に使っていきましょう。


おまけ: メソッドの記法

オブジェクトリテラルにはメソッドを簡単に記述するための糖衣構文があります。

こう書いていたものを

const obj = {

method: function() { /* ... */ } // (A)
};

こう書けるようになりました。

const obj = {

method() { /* ... */ } // (B)
};

実は上の2つは厳密には等価ではありません。

(A)ではmethodプロパティに function 関数が設定されています。function 関数オブジェクトが作成された時点でその関数のprototypeプロパティにはオブジェクトが設定されます。つまり、obj.method.prototypeプロパティが存在します。また、obj.method[[Construct]]スロットも持つので、new obj.methodができます。

一方、(B)はメソッド定義専用の構文で、メソッドはコンストラクタである必要がないので、prototypeプロパティも[[Construct]]スロットも設定されません(14.3.7 Runtime Semantics: DefineMethod14.3.8Runtime Semantics: PropertyDefinitionEvaluation)。クラス構文もメソッドについても同様です。

こちらも、特別な意図のない限り、新しい(B)の構文を使った方がいいです。

ES5以前は全て function だったのが、


  • 関数 ⇒ アロー関数

  • メソッド ⇒ メソッド定義の構文

  • コンストラクタ ⇒ class 構文の constructor

と、役割によって書き分けられるようになりました。そのうち function キーワードも var 同様互換性のためだけのものになるのかもしれませんね。