JavaScriptのアロー関数でオブジェクトを返す方法

2019.08.01

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

みなさんこんにちは。 突然ですが、下記に記載したJavaScriptのコードを実行した時の出力はどうなるでしょうか。

const res = [1,2,3].map(i => {res: i})
console.log(res)

正解はこうです。

[ undefined, undefined, undefined ]

答えはあってましたか。
最初私は下記のような出力が返ってくると思いました。

[ {res: 1}, {res: 2}, {res: 3} ]

予期した答えと大きく異なりますね。
なので今回は予期した答えを得る方法と、なぜこうなったかを書きたいと思います。

予期した答えを得る方法

真っ先に思いついたのは、returnを省略せずに書く方法でした。
つまり下記のようなコードで動くだろうと予測しました。

const res = [1,2,3].map(i => {
  return {
    res: i
  }
})
console.log(res)

出力も予想通りでした。

[ {res: 1}, {res: 2}, {res: 3} ]

ですが記述が冗長で可読性が一気に悪くなりましたね。
なんとかreturnなしで簡潔に書けないかと思い、ドキュメントを見ました。
ちゃんと書いてありました。

// object リテラル式を返す場合は、本体を丸括弧 () で囲みます:
params => ({foo: bar})
アロー関数 高度な構文

なので、最初のコードを下記のように書き換えると、予期した結果を得られます。

const res = [1,2,3].map(i => ({res: i}))
console.log(res)

順序が逆になってしまいましたが、このような記述もありました。
基本的には、式(expression)を使う場合は中括弧が不要で、文(statements)の場合は必要になるということです。

(param1, param2, …, paramN) => { statements }
(param1, param2, …, paramN) => expression
// 上記の式は、次の式と同等です: => { return expression; }
アロー関数 基本的な構文

それでもう少しドキュメントを読み進めていくと、下記のような記述があります。

短縮構文 params => {object:literal} を使ってオブジェクトリテラルを返そうとしても、期待通りに動作しないことに注意しましょう。
これは、括弧 ({}) 内のコードが文の列として構文解析されてしまっているからです(つまり、foo はオブジェクトリテラル内のキーでなく、ラベルとして扱われています)。
アロー関数 オブジェクトリテラルを返す

何となくわかりましたが、もう少し深入りしたくなりました。
実際に構文解析した結果をみて、どうなっているかを確認しましょう。

構文解析の結果を確認する

Esprimaを使って、構文木を見てみます。 まずはこのコードから

[1,2,3].map(i => {res: i})

構文木の全体はこちらにあります。
必要な部分、map以降の部分のみ抜粋して載せます。

{
  "type": "Program",
  "body": [
    {
      "type": "ExpressionStatement",
      "expression": {
        "type": "CallExpression",
        "arguments": [
          {
            "type": "ArrowFunctionExpression",
            "id": null,
            "params": [
              {
                "type": "Identifier",
                "name": "i"
              }
            ],
            "body": {
              "type": "BlockStatement",
              "body": [
                {
                  "type": "LabeledStatement",
                  "label": {
                    "type": "Identifier",
                    "name": "res"
                  },
                  "body": {
                    "type": "ExpressionStatement",
                    "expression": {
                      "type": "Identifier",
                      "name": "i"
                    }
                  }
                }
              ]
            },
            "generator": false,
            "expression": false,
            "async": false
          }
        ]
      }
    }
  ],
  "sourceType": "script"
}

resLabeledStatementで、iExpressionStatementとして認識されています。

それに対して、下記のコードはどうでしょうか。

[1,2,3].map(i => ({res: i}))

構文木の全体はこちらにあります。 必要な部分、map以降の部分のみ抜粋して載せます。

{
  "type": "Program",
  "body": [
    {
      "type": "ExpressionStatement",
      "expression": {
        "type": "CallExpression",
            "body": {
              "type": "ObjectExpression",
              "properties": [
                {
                  "type": "Property",
                  "key": {
                    "type": "Identifier",
                    "name": "res"
                  },
                  "computed": false,
                  "value": {
                    "type": "Identifier",
                    "name": "i"
                  },
                  "kind": "init",
                  "method": false,
                  "shorthand": false
                }
              ]
            },
            "generator": false,
            "expression": true,
            "async": false
          }
        ]
      }
    }
  ],
  "sourceType": "script"
}

正しく、ObjectExpressionと認識されておりKeyにresが、valueiが指定されています。

予想通りに動かなかった方は、resというラベルが貼られたスコープないで、iという式を評価していることになります。

つまりこのような事に解釈されているはずです。
なので、undefinedがmapに渡されます。

res: i;

さいごに

普段よく使う言語の仕様であってもまだまだ知らないことがたくさんあるんだと実感しました。
後は、やっぱりMDNのドキュメントは丁寧でわかりやすくて良いですよね。