JavaScriptに限った話ではありませんが、関数というのは名前を持っていたり持っていなかったりします。関数名は普通はプログラムの読みやすさくらいにしか影響しませんが、JavaScriptでは必ずしもそうではありません。
例えばReactで関数コンポーネントを使う場合は関数名がコンポーネント名となり、React用開発者ツールなどで見ることができデバッグに役立ちます。また、Gulp v4もエクスポートした関数名がタスク名となります。
関数名は、関数オブジェクトのname
プロパティで取得できます。
function foo() {
console.log('foo!');
}
console.log(foo.name); // "foo"
この例では関数宣言時にfoo
という名前を付けています。function
式の場合も同様に名前を付けた関数を作ることができます。
const myFunc = function bar() {
console.log('I am bar');
};
console.log(myFunc.name); // "bar"
その一方で、function
式では名前を付けずに関数を作ることもできるのでした。この場合、関数は無名関数となり……
const noNameFunc = function() {
console.log("I don't have a name");
};
……あれ?
ゴシゴシ(目をこする音)
🤔**??????????**
というわけで、これがこの記事のテーマです。上の例では、名前なしでfunction
式で関数を作ったはずなのに親切にもいつのまにか"noNameFunc"
という名前が関数についています。この記事では、関数の名前はいつどのように決められるのかについて隅から隅まで解説します。なお、関数のname
プロパティは文字列です。
名前付きで宣言された関数の場合
さっきの例のように、名前付きで宣言された関数はそれがそのまま名前となります。これは明らかですね。
// 名前は"foo"
function foo() {
console.log('foo!');
}
// 名前は"bar"
const myFunc = function bar() {
console.log('I am bar');
};
ですので、ここからは名前無しで宣言された関数を考えることにします。
そのまま使う場合は無名
最後の例は、作った無名関数を変数に代入したのがとても怪しいですね。一応確認ですが、無名で作った関数をそのまま(変数に代入したりせずに)使用する場合は名前はありません。name
プロパティは""
となります。
console.log((function(){}).name); // ""
変数に代入する場合
結論から言えば、無名関数を変数に代入する場合はその変数の名前が関数名となります。
const noNameFunc = function() {
console.log("I don't have a name");
};
この例の場合は、無名関数をnoNameFunc
という変数に入れたので名前がnoNameFunc
となったのでした。
面白いことに、この挙動は形が重要です。変数 = function(...) { ... }
という形の場合にのみ、この挙動が発生します。この形から少しでも外れると、関数に名前は付きません1。
const noNameFunc = [function() {}][0];
console.log(noNameFunc.name); // ""
この例では、無名関数を配列に入れてから取り出すという無意味なことをしていますが、これは上で説明した形とは違うので関数に名前は付きません。
一つだけあの形から外れても大丈夫な場合があります。次のように、function
式を括弧で囲むだけならセーフです。
const myFunc = (function(){ console.log("I'm myFunc"); });
console.log(myFunc.name); // "myFunc"
注意すべき点としては、オブジェクトのプロパティへの代入では関数に名前がつきません。変数への代入のみです。
const obj = {};
obj.foo = function() { console.log("I'm not foo"); };
console.log(obj.foo.name); // ""
オブジェクトリテラルの場合
オブジェクトのプロパティに関数を入れる場合は、オブジェクトリテラルを使う方法もありました。下の例のように、オブジェクトリテラルによりオブジェクトを作るときにプロパティに関数を入れることができます。実は、この場合は関数に自動的に名前が付加されます。
const obj = {
foo: function() {
console.log("I'm foo");
}
};
console.log(obj.foo.name); // "foo"
このように、その関数が入ったプロパティ名が関数の名前となります。
この場合も先ほどと同様に形が重要です。プロパティ名: function(...) { ... }
という形のみが特別扱いされます。ちょっとでも余計なことをすると名前は付きません。
メソッド宣言の場合
ES2015からは、メソッド宣言という新しい記法で関数を宣言できます。これはオブジェクトリテラル(やクラス内)で使える以下の例のような構文です。
const obj = {
foo() {
console.log("I'm foo");
}
};
console.log(obj.foo.name); // "foo"
これはobj.foo
に入る関数を宣言しています。当然ながら、この場合もこの関数には"foo"
という名前が与えられます。
分割代入のデフォルト値の場合
ES2015では、分割代入というとても便利な構文があります。オブジェクトの中身を取り出してまとめて変数に代入できる機能です。
// objのfooプロパティとbarプロパティをそれぞれ変数fooと変数barに代入
const {foo, bar} = obj;
そして、このときfoo
やbar
がobj
に存在しなかった場合2のためにデフォルト値を設定可能です。下の例ではbar
のデフォルト値を設定しています。
// objにbarが無かった場合はbarには0が入る
const {foo, bar = 0} = obj;
そして、このデフォルト値に例の形が来た場合はやはり変数名がその関数に与えられます。
const { foo = function(){} } = {};
console.log(foo.name); // "foo"
これはオブジェクトの分割代入でしたが、配列(イテレータ)の場合も同様です。
const [bar = function(){}] = [];
console.log(bar.name); // "bar"
これらはなかなか普段発生しないレアケースですね。
export default
の場合
export default
で無名関数をエクスポートする場合は関数名が"default"
となります。defaultという名前が嫌な場合はちゃんと自分で名前を宣言しないといけませんね。
export default function() {
console.log("I'm default!");
}
関数式として認められるもの
ここまで何度も形が重要と述べてきましたが、実は関数式の部分はいくつかバリエーションが可能です。ここまで見てきた基本の形は以下のような関数式です。
const foo = function(){};
console.log(foo.name); // "foo"
これに加えて、ジェネレータ関数式やasync関数式を作る関数式もあります。実は、これらもOKです。
const myGenerator = function*(){};
const myAsyncFunc = async function(){};
console.log(myGenerator.name); // "myGenerator"
console.log(myAsyncFunc.name); // "myAsyncFunc"
さらに、アロー関数でもOKです。
const myArrowFunc = ()=>{};
console.log(myArrowFunc.name);
また、ご存知の方も多いかと思いますが、JavaScriptではクラスは関数の一種です。よって、クラスもname
プロパティを持ちます。
class Foo {}
console.log(Foo.name); // "Foo"
クラスもclass
式によって無名クラスを宣言することができます。これもやはり同様にname
がセットされます。
const MyClass = class {};
console.log(MyClass.name);
Function
コンストラクタの場合
実は、JavaScriptはFunction
を用いて文字列から関数を作る機能があります。これによって作られた関数の名前は"anonymous"
となります。
const func = new Function("return 100;");
console.log(func.name); // "anonymous"
ゲッタやセッタの場合
オブジェクトリテラルにおいては、ゲッタとセッタによりプロパティを宣言することができます。
const obj = {
// プロパティfooに対するゲッタを定義
get foo() {
return 100;
}
};
console.log(obj.foo); // 100
今回はゲッタを宣言するget プロパティ名() { ... }
の構文によってプロパティfoo
のゲッタを宣言しました。obj.foo
が参照されるたびにこの関数が呼び出されます。
ここではfoo
のゲッタとなる関数が作られました。この関数の名前はどうなっているのでしょうか。
これはobj.foo
としても参照することができませんが、foo
のプロパティデスクリプタを取得することでアクセスできます。
const obj = {
// プロパティfooに対するゲッタを定義
get foo() {
return 100;
}
};
// fooのプロパティデスクリプタを取得
const fooDesc = Object.getOwnPropertyDescriptor(obj, "foo");
console.log(fooDesc.get); // fooのゲッタとなっている関数が表示される
console.log(fooDesc.get.name); // "get foo"
というわけで、foo
のゲッタの名前は"get foo"
でした。名前にスペースが入っているのが面白いですね。このように、ゲッタとして作成された関数は"get プロパティ名"
に設定されます。セッタの場合は"set プロパティ名"
です。
bind
により作られる関数の場合
bind
は関数オブジェクトが持つメソッドで、ある関数のthis
の値を固定したり、一部の引数が既に決められたりしている新しい関数を作ってくれます。新しい関数が作られるということで、このとき作られる関数の名前はどうなるのか気に成りますね。早速結果を見ましょう。
const foo = function(){};
const foo2 = foo.bind(null);
console.log(foo2.name); // "bound foo"
const foo3 = foo2.bind(null);
console.log(foo3.name); // "bound bound foo"
このように、bind
の結果は元々の関数名に"bound "
が追加された名前を持ちます。名前のない関数をbind
した場合は"bound "
という関数名になります。最後のスペースがいい味を出していますね。
プロパティ名がシンボルだとどうなるのか
以上で関数の名前が自動で設定される場合を全部列挙しました(ES2018時点の情報です)。いくつかの場合は、関数が代入されるプロパティの名前が関数名となりました。
ここで、鋭い読者の方はひとつの疑問を抱くでしょう。それは、プロパティ名はシンボルかもしれないという点です。それに対して、関数名は文字列のみです。では、シンボルの名前を持つプロパティに関数を入れると何が起こるのでしょうか。
実は、この場合の挙動は2種類あります。プロパティ名のシンボルにdescription(説明)がセットされているかどうかによって分かれます。シンボルのdescriptionというのは、Symbol
によって新しいシンボルを作るときにSymbol
の引数に渡された文字列です。
// 新しいシンボル(descriptionなし)を作成
const prop = Symbol();
const obj = {
[prop]: function() { console.log("What's my name?"); }
};
console.log(obj[prop].name); // ""
このように、description無しのシンボルがプロパティ名の場合は関数名は""
になります。
次にdescriptionありの場合を見ましょう。
// 新しいシンボル(descriptionあり)を作成
const prop = Symbol("prop");
const obj = {
[prop]: function() { console.log("What's my name?"); }
};
console.log(obj[prop].name); // "[prop]"
このように、descriptionを持つシンボルがプロパティ名の場合は関数名は"[description]"
になります。分かりやすさのために、関数名に使われるかもしれないシンボルにはいい感じにdescriptionを設定しておいたほうがよいかもしれません。
この[]
で囲むという慣習には組み込みの関数も従っています。例えば文字列は[Symbol.iterator]
関数を持っていますので、そのname
を見てみましょう。
console.log("123"[Symbol.iterator].name); // "[Symbol.iterator]"
余談:関数のname
を変える方法
関数のname
プロパティはwritable属性がfalseに設定されているため、プロパティに代入しても書き換えられません。
const foo = ()=> console.log("I'm foo");
foo.name = "FOOOOOO";
console.log(foo.name); // "foo"
しかし、configurable属性はtrueに設定されているため、Object.defineProperty
などで無理やり書き換えることは可能です。関数の名前を書き換えたいときは使ってみましょう。
const foo = ()=> console.log("I'm foo");
Object.defineProperty(foo, "name", {
value: "FOOOOOO",
configurable: true,
});
console.log(foo.name); // "FOOOOOO"
余談2: name
プロパティはいつ存在するのか
この記事で紹介したように、いくつかの場合には関数オブジェクトにいい感じにname
プロパティが設定されます。設定されなかった場合はname
プロパティは""
となります。
実は、これはFunction.prototype.name
由来の""
です。つまり、名前のない関数はname
プロパティを持っておらず、Function.prototype.name
が参照されています。Function.prototype.name
は""
が入っています。このことは仕様にも書いてあります。
Anonymous functions objects that do not have a contextual name associated with them by this specification do not have a
name
own property but inherit the name property of %FunctionPrototype%.
……と言いたいところなのですが、実際はそうではなく、ChromeやFirefoxなど複数のブラウザで関数は常に自分のname
プロパティを持っています。
console.log((function(){}).hasOwnProperty("name")); // true
つまり、仕様と実情が乖離しているのです。このような例は重箱の隅をつつけばそう珍しいことでもありませんが。この件については議論が亀の歩みでされており、仕様を現状に合わせて修正する方向でそのうちまとまりそうな感じがしています。
まとめ
この記事では、関数のname
プロパティがどのように決定されるのかをまとめました。関数名はソースコードをminifyしたときに大抵は消えるのでname
のことを真剣に考えなければいけない機会はそう多くはありませんが、もしそんな機会が発生したらこの記事を思い出してみてください。
ところで、記事の最後に「いいねお願いします」というと本当にいいね数が増えるという説を見たので実験したいと思います。この記事の内容がいいと思ったらぜひ「いいね」を押してください。よろしくお願いします。
関連記事
記事を公開したあとに内容が結構重なる記事を発見しました。この記事では網羅性を高めるように頑張ったので許してください。
余談3: 仕様書を読むコーナー
こういう記事を書く時にはガチ勢を目指す人向けに仕様書に言及するようにしています(上の余談2で少しフライングしてしまいましたが)。
関数のname
プロパティを設定する処理は仕様書の9.2.13 SetFunctionNameに記述されています。この記事のコンテンツは、SetFunctionNameが使われている場所を全部列挙して関係ありそうなところをまとめ直してできたものです。
例えば代入式 (LeftHandSideExpression = AssignmentExpression)の評価の定義を見てみましょう(12.15.4 Runtime Semantics: Evaluation)。左辺と右辺を評価した後、1-eで IsAnonymousFunctionDefinition(AssignmentExpression) と IsIdentifierRef of LeftHandSideExpression が両方trueであるかどうかを判定しています。これはようするに、右辺のAssignmentExpressionが上で見たような関数式の形をしているかどうか、そして左辺が単なる変数であるかどうかを判定しています。
次の1-e-iは、右辺の評価結果の関数が既にname
プロパティを持っているかどうか判定しています。これにより、右辺が既にname
を持っている場合(function foo(){}
のように自分で名前をつけている場合など)にname
を上書きするのを避けています。そうでなければ、1-e-iiでSetFunctionNameにより関数の名前を設定しています。
SetFunctionNameが使われている箇所はだいたいこんな感じです。
以上のように、JavaScriptの特定の挙動を調べたいとき(今回の場合は関数のname
がセットされるときの挙動)は、仕様書でそれを行う手順が定義されているのを見つけるのがよいです。今回の場合はSetFunctionNameがそれに相当します。あとはそれを参照しているところを探せばだいたい理解することができます。たまにSetFunctionNameのようにまとまっておらずあちこちに点在している場合があって大変ですが。