concatだらけになるtemplate literal

Babelで変換されたあとのコードを見ると、一見無駄にも思えるコードが生成されていたのですが、実はそれが必要だった、というお話です。


Template Literalとは

ES5までのJavaScriptでは、文字列を合成するには「1つ1つ+演算子で連結していく」「Array.prototype.joinで連結する」などの手法があったのですが、どちらの方法にしてもどのような構造の文字列ができるか、分かりづらいものでした。

ES6では、他言語のように文字列途中で式展開ができる、Template Literalが登場しています1。引用符として`...`を使い、式展開はその途中に${...}と書くという、他言語と比べてもそこまで違和感のない記法となっています。


Babelでの変換結果

で、例によってIEは非対応なので(MDN)、IEを切り捨てられない環境ではBabelでの変換が必要となります。この変換を行う@babel/plugin-transform-template-literalsでは、通常モードとlooseモードの2通りの変換が選べます。

// 変換前

const a = `${foo}-${bar}-${baz}`;

// 通常モード
var a = "".concat(foo, "-").concat(bar, "-").concat(baz);

// looseモード
var a = foo + "-" + bar + "-" + baz;

多くの人は、looseモードの方の結果を想像していたことでしょう。


ここまで長くなる理由


.concatによる文字列変換

JavaScriptで一般に思いつく「文字列への変換」といえば、以下の2つかと思います。



  • String(値)による文字列変換


  • '' + 値として、暗黙の変換を活用する

これらは、以下のような動作をします。



  • String(値)…シンボルの場合はSymbolDescriptiveStringSymbol.prototype.toString())を返して、それ以外のときはToString抽象操作を行う(ES2019 §21.1.1.1

  • 文字列連結の演算…ToPrimitive抽象操作を行ってから、ToString抽象操作を行う(ES2019 §12.8.3.1

というように、挙動が異なっています。

一方で、Template Literalに埋め込んだ式は、ToString抽象操作により文字列に変換されるため(ES2019 §12.2.9.6)、上のどちらとも異なっています。そこで登場するのがString.prototype.concatで、これは引数に対してToStringのみを行う仕様となっていますので(ES2019 §21.1.3.4)、仕様どおりの動作を実現させるために文字列変換と連結に.concatを使用するようになっています。

なお、文字列連結とconcatで挙動が違うオブジェクトは以下のようなものです(プリミティブはToPrimitiveで変化しないので、差異はありません)。



  • [Symbol.toPrimitive].toString()と違う結果を返すオブジェクト


  • [Symbol.toPrimitive]がなく、.valueOf().toString()と異なる文字列表記になるプリミティブを返すオブジェクト

なお、Date.valueOf().toString()の結果が異なりますが、別途で[Symbol.toPrimitive]を用意してあるため、文字列連結しても.valueOf()の値が出てくることはありません。


複数の.concat

ここまでで.concatを使う意味については説明してきましたが、「それなら''.concat(foo, "-", bar, "-", baz)でもいいんじゃないか?」と思ってしまうかもしれません。

ということで、Template Literalの処理を見返してみると、与えられた式展開の式1つ1つに対して、式の評価、ToStringを行っては文字列連結、というように進んでいきます。一方で、メソッドを呼ぶ場合、メソッドの引数はすべてメソッドの実行前に評価されてしまいます。つまり、ここで評価順が違ってきます。

.concatを複数にすることで、1つ目の式の評価、ToStringが終わってから2つ目の式の評価に移るというように、評価順序まで正確に同じものとするようになっています。

もっとも、この両者で違ってくるシナリオとしては、



  • ToStringすると例外が飛ぶような式が前の方に入っていたときに、後の式の評価で生じる副作用が起きるか起きないかの違い

  • 前の方の式についてToStringが副作用を持ち、それが後の式の評価やToStringの結果に影響を及ぼす場合

が考えられますが、ToStringが副作用を持つオブジェクトを作っても極めて使いにくいでしょうし、ToStringすると例外が飛ぶような式を文字列に埋め込んで、なおかつその例外が飛ぶパスを、他の式展開で副作用の起きるかどうかまで想定したコードを書く、という場面も考えづらいので、実用的に差異が出る危険性は薄そうです(そもそも、式展開の中に副作用の伴う式を書くこと自体、そう多くないと思いますし)。





  1. 雛形になる文字列と、式展開された値をまとめて関数に投げられるような、タグ付きTemplate Literalもありますが、とりあえず本稿では触れません。