JavaScript のもう一つの「関数名」 —— name プロパティ

  • 5
    いいね
  • 0
    コメント

JavaScript のもう一つの「関数名」 —— name プロパティ

「関数名」と name プロパティ

JavaScript において「関数名」あるいは「メソッド名」というと何を指すでしょうか。
関数オブジェクトを参照している変数名やプロパティ名を指すことが多いと思います。
しかし、もう一つ「関数の名前」と言える物として、関数オブジェクトの name プロパティがあります。
これは関数オブジェクトの生成時に決定される、文字列のプロパティです。

function foo() {
    // do something
}

let bar = foo
let baz = foo
let qux = [foo, foo]

console.log( foo.name    )   // 出力 -> "foo"
console.log( bar.name    )   // 出力 -> "foo"
console.log( baz.name    )   // 出力 -> "foo"
console.log( qux[0].name )   // 出力 -> "foo"

JavaScript では関数はオブジェクトです。
例えば上のコードのように、同じ関数が値としてさまざまな変数から参照されたり、配列の要素になったりします。
また、関数を参照していた変数に別の物が代入されることもあります。
変数名と関数オブジェクトの関係は儚いのです。
ですからデバッグの際などに、関数を区別するための「関数の名前」として name プロパティが使われることがあります。

name プロパティはいかに決定されるか

function 文・function 式での指定

JavaScript に古くからある function 文と function 式では「関数名」を指定できます。
指定した「関数名」は生成される関数オブジェクトの name プロパティに使用されます。
function 文の場合はさらに関数オブジェクトを参照する変数名にも使われます)

// function 文
function foo() {
    // do something
}

console.log( foo.name )   // 出力 -> "foo"


// function 式
let f = function foo() {
    // do something
}

console.log( f.name )   // 出力 -> "foo"

function の後に指定する「関数名」は識別子です。
コードを書いた時点で決定されています。
任意の式を置いて実行時に計算させる事はできません。
下のように eval を使えば別ですが。

let funcName = "foo"

let f = eval(`(function ${funcName}() {
    // do something
})`)

console.log( f.name )   // 出力 -> "foo"

以降、eval のことは考えないことにします。

Function コンストラクタ

Function コンストラクタで作成される関数オブジェクトの name プロパティは "anonymous" となります。
これを変更する引数などはありません。

let f = new Function("// do something")

console.log( f.name )   // 出力 -> "anonymous"

アロー関数式・関数名を省略した function

ES6(ES2015) で登場したアロー関数式には、関数名を指定する方法がありません。
また function 式も、関数名が省略可能です。
では、これらの構文で作成された関数オブジェクトの name プロパティは空なのでしょうか?
実はそうとは限りません。

let f1 = ()=>{ /* do something */ }
let f2 = function(){ /* do something */ }

console.log( f1.name )   // 出力 -> "f1"
console.log( f2.name )   // 出力 -> "f2"

上では変数宣言の初期値としてこれらを使用しました。
すると、生成される関数オブジェクトの name プロパティには変数名と同じ文字列が自動的に設定されます。

この設定は、関数オブジェクトが生成される際に一度だけ行なわれます。
例えば下のようにしても f1 関数の name プロパティが変更されることはありません。

let f1 = function(){ /* do something */ }
let f2 = f1
f1 = null

console.log( f2.name )   // 出力 -> "f1"

name プロパティは不変です。
もはや、この関数オブジェクトは f1 という変数から参照されていませんが、name プロパティは "f1" のままです。

MDN では、このように自動的に設定される name プロパティを "Inferred function names" と呼んで紹介しています。
「推定関数名」といった所でしょうか。
これをもう少し詳しく見てみましょう。

「関数名」の推定

変数宣言

let   f1 = function(){ /* do something */ }
const f2 = function(){ /* do something */ }
var   f3 = function(){ /* do something */ }

console.log( f1.name, f2.name, f3.name )   // 出力 -> "f1 f2 f3"

既に紹介したように、変数の宣言時に初期化する場合、変数名から「関数名」が推定されます。
変数名ですから、これもコードを書いた時点で決定されています。

変数への代入

let f
f = function(){ /* do something */ }

console.log( f.name )   // 出力 -> "f"

変数への代入でも、宣言時の初期化の際と同じように推定されます。

プロパティへの代入

let obj = {}
obj.foo = function(){ /* do something */ }

console.log( obj.foo.name )   // 出力 -> ""

左辺から推定され、"obj.foo" などになるのが正しいと思うのですが、空文字になります。
現在の処理系では推定が行なわれないようです。
(ただ、Firefox は内部的にはこういった推定を行なっているようで、関数オブジェクトのコンソールへの表示には推定された関数名が使われています)

オブジェクト初期化子

let obj = {
    foo:        function(){ /* do something */ },
    "foo bar":  function(){ /* do something */ },
}

console.log( obj.foo.name        )   // 出力 -> "foo"
console.log( obj["foo bar"].name )   // 出力 -> "foo bar"

プロパティ名から推定されます。
プロパティ名は空白なども許される文字列ですから、これは自由度が高いですね。

オブジェクト初期化子 —— 計算によるプロパティ名

オブジェクト初期化子を使えば、関数オブジェクトの name プロパティを自由な文字列にできることが分かりました。
これを使って、実行時に任意の「関数名」を付けてみましょう。
ES6 からは、オブジェクト初期化子のプロパティ名部分に式を使うことができます。
括弧 [] で囲み任意の式を置きます。

// funcName は 実行時に決定される「関数名」を保持するものとする
let funcName = "foo"

let obj = {
    [funcName]: function(){ /* do something */ }
}
let f = obj[funcName]

console.log( f.name )   // 出力 -> "foo"

これで実行時に計算し、関数オブジェクトの name プロパティを決定することができます。

オブジェクト初期化子 —— その他の記法

ES6 で導入されたオブジェクト記述子の記法を用いても、下のように関数名の推定が行なわれます。

let obj = {
    // メソッドの短縮記法
    foo() { /* do something */ },

    // アクセサ
    get bar()  { /* do something */ },
    set bar(v) { /* do something */ },
}
let barDesc = Object.getOwnPropertyDescriptor(obj, "bar")

console.log( obj.foo.name     )   // 出力 -> "foo"
console.log( barDesc.get.name )   // 出力 -> "get bar"
console.log( barDesc.set.name )   // 出力 -> "set bar"

これらの記法を計算によるプロパティ名と共に使用すれば、オーソドックスな初期化子で行なったように、実行時の計算により関数オブジェクトの name プロパティを決定することができます。

let methodName = "foo"
let accessorName = "bar"

let obj = {
    // メソッドの短縮記法
    [methodName]() { /* do something */ },

    // アクセサ
    get [accessorName]()  { /* do something */ },
    set [accessorName](v) { /* do something */ },
}

let f    = obj[methodName]
let desc = Object.getOwnPropertyDescriptor(obj, accessorName)

console.log( f.name        )   // 出力 -> "foo"
console.log( desc.get.name )   // 出力 -> "get bar"
console.log( desc.set.name )   // 出力 -> "set bar"

name プロパティを後から変更する

さて、関数オブジェクトの生成時に name プロパティがいかに決定されるかを見てきましたが、これを後から変更できるでしょうか。

function foo(){
    // do something
}
foo.name = "bar"

console.log( foo.name )   // 出力 -> "foo"

上のように name プロパティに代入しても例外が発生したりはしませんが、値は変更されません。
name プロパティは書込不可の属性を持つからです。
しかし、ES6 での標準化により name プロパティの属性は設定可能になりました。
「設定可能」ならば値(value 属性)を設定することもできます。
つまりは間接的にならば書き込めるということです。

function foo(){
    // do something
}

let desc = Object.getOwnPropertyDescriptor(foo, "name")
desc.value = "bar"
Object.defineProperty(foo, "name", desc)

console.log( foo.name )   // 出力 -> "bar"

あるいは、name プロパティを書き込み可能(writable 属性を true)に設定すれば、代入で変更できるようにもなります。

function foo(){
    // do something
}

let desc = Object.getOwnPropertyDescriptor(foo, "name")
desc.writable = true
Object.defineProperty(foo, "name", desc)

foo.name = "bar"
console.log( foo.name )   // 出力 -> "bar"