🤳

JavaScriptに密かに存在する“無名関数宣言”

2021/06/13に公開

この記事では JavaScript エンジニアがしてしまいがちなある誤解を紹介し、それがなぜ誤解なのかを解説します。
その誤解とは、「関数宣言には必ず名前が必要である」ということです。これはexport defaultの場合に例外が存在しているため、誤解となります。

JavaScript の関数宣言

JavaScript で関数を作る方法は色々ありますが、その中でもfunctionキーワードを用いる方法は初期から存在しています。functionキーワードを用いて関数を作る場合は関数式関数宣言の 2 つに大別されます。関数式はその名の通り式である一方で、関数宣言は文のように使用され、巻き上げ (hoisting) の挙動を持つことが特徴的です。

// 関数式
const func = function (num) {
  return num * 2;
};

console.log(func(100));
// 関数宣言
console.log(func(100));

function func(num) {
  return num * 2;
}

functionの後に書く関数名は、関数式の場合は省略して無名関数とできる一方で、関数宣言の場合は関数名を省略すると構文エラーとなります。実際、Google Chrome で試すと「Uncaught SyntaxError: Function statements require a function name」という構文エラーが報告されます。

// 構文エラー
function (num) {
  return num * 2;
}

このように、関数宣言としてfunctionの構文を使う際は関数名が必要であるというのが一般的な JavaScript エンジニアが持つ経験則です。確かに、名前がない関数宣言というのは作った関数を参照する手段が無くなってしまうことからもこれは妥当な制約です。しかし、実はこの「関数宣言には名前が必要である」という制約には一つ例外があるのです。

export defaultと関数宣言

次にexport default宣言の話に移ります。これはモジュールから何かを default エクスポートするための構文であり、典型的な構文はexport default 式;です。例 2 のように何かのインスタンスをエクスポートしてシングルトン的に使うような例を見たことがある方も多いでしょう。

// 例1
export default 123;

// 例2
export default new SomeBigClass();

一方で、React のプロジェクトなどでは、次のようにexport defaultで関数をエクスポートする例も見られます。

export default function (props) {
  // ...
}

この構文が今回の本題です。export default 式;の類例から見ればこのfunction (props) { ... }は一見関数式のように見えますが、実はこれは関数式ではなく関数宣言です。そして、このようにexport defaultに続く関数宣言では特別に名前なしの関数宣言——すなわち無名関数宣言——が認められているのです。

仕様書で確かめる

以上のことを仕様書で確かめてみましょう。https://tc39.es/ecma262/ を参照します。

以下に引用するように、構文定義上export default構文には 3 種類あります。

export default HoistableDeclaration[~Yield, ~Await, +Default]
export default ClassDeclaration[~Yield, ~Await, +Default]
export default [lookahead ∉ { function, async [no LineTerminator here] function, class }] AssignmentExpression[+In, ~Yield, ~Await] ;

仕様書のスクリーンショット

すなわち、export defaultの後ろに HoistableDeclaration が来るもの、export defaultの後ろに ClassDeclaration が来るもの、そしてexport defaultの後ろに AssignmentExpression ; が来るものです。HoistableDeclaration はfunctionfunction*async functionasync function*という 4 種類のfunction宣言を総称する非終端記号です。

export default functionの形の宣言は、1 番目の HoistableDeclaration に当てはまります。3 番目(後ろに式が来る形)に当てはまる可能性は、lookahead ∉ { ... }という制限によって除去されています(export defaultの直後にfunctionasync functionclassといったトークン(列)が来るものが除かれています)。

以上のことが、export default functionにおけるfunctionが関数式ではなく関数宣言であることの根拠となります。

次に、関数宣言(FunctionDeclaration 非終端記号)の定義も見てみましょう。

FunctionDeclaration[Yield, Await, Default] :

function BindingIdentifier[?Yield, ?Await] ( FormalParameters[~Yield, ~Await] ) { FunctionBody[~Yield, ~Await] }
[+Default] function ( FormalParameters[~Yield, ~Await] ) { FunctionBody[~Yield, ~Await] }

仕様書のスクリーンショット

このように、FunctionDeclaration の構文には 2 種類あります。前者はfunctionトークンの後ろに BindingIdentifier がありますが、後者にはありません。これらがそれぞれ、関数名がある関数宣言と関数名が無い関数宣言に対応します。ただし、後者には最初に[+Default]と書いてあるのが見て取れます。実は FunctionDeclaration 非終端記号は Yield, Await, Default という 3 つのパラメータでパラメトライズされており、そのうち Default というフラグが立っている場合にのみ 2 つ目の定義が有効(使用可能)という定義になっています。

export defaultの構文定義に立ち返ってよく見てみると、 export default HoistableDeclaration[~Yield, ~Await, +Default] とあり、HoistableDeclaration に +Default としてパラメータを渡しています。これが Default パラメータをオンにするという意味です(+は有効、~は無効を表します。他に?というのがあり、これは自身の同名パラメータを引き継ぐという意味です)。HoistableDeclaration に渡された Default パラメータはそのまま FunctionDeclaration に引き継がれます。

このことにより、export defaultの後ろの関数宣言においては無名バージョンの関数宣言が使用可能となるのです。他に Default フラグが立つところはありませんから(これは仕様書を全文検索してみてば分かります)、無名関数宣言が可能なのはexport defaultの場合のみです。

関数式と関数宣言の違いが現れる例

以上で解説した通り、export default functionの形におけるfunctionは関数式ではなく関数宣言です。このことから帰結する少し面白い例を紹介したのが以下のツイートです。

https://twitter.com/uhyo_/status/1403661230175256589

ツイートの画像にあるコードを示します。

const expr = function() {} 123;
//                         ^^^
//          ここで構文エラーが発生

export default function () {} 123;

constによる変数宣言とexport defaultの場合で、どちらもfunction() {} 123;という同じコード列であるにもかかわらず、constの場合は123で構文エラーが発生する一方export defaultの場合は構文エラーが発生しません。

この違いは、constの方ではfunctionが関数式であるのに対してexport defaultの方ではfunctionが関数宣言であることから発生します。

変数宣言はconst 変数 = 式;という構文ですから[1]function() {} 123の部分が式とならなければなりません。関数式の場合はfunction() {}でひとつの式となりその後ろに123が続くような構文は存在しないため、123は構文エラーとなります。

一方で、export defaultの場合もfunction () {}までで関数宣言が終わるのは同じですが、上記のexport宣言の定義をよく見ると分かるように、export defaultのあとが関数宣言である場合はセミコロンが必要ありません。よって、}の時点でexport宣言が終了したものと見なされます。そのため、123;export宣言とは別の単独の文と見なされ、(意味はないものの)構文的に正しいプログラムとなります。

まとめると、式か宣言かによってセミコロンの要不要が異なり、それが123が構文エラーになるかどうかを左右しているのです。

まとめ

「JavaScript の関数宣言には必ず関数名が必要」と言ってしまうと誤りなので、気をつけましょう。

余談ですが、class宣言の場合も同様に、export defaultの場合のみ無名クラス宣言が可能です。

ちなみに、export defaultの無名関数宣言で作られた関数は、自動的にdefaultという関数名が与えられます。詳しくはJavaScript の関数名の全てをご覧ください。

脚注
  1. 分割代入などもありますが今回は関係ないので省略しています。 ↩︎

GitHubで編集を提案

Discussion