以下のツイートを見て、確かにもっと積極的に ``
を使ってもいいかな、と思ったので、改めて考えてみました。
#JavaScript
— suin❄️TypeScript入門書執筆中 (@suin) May 5, 2020
変数展開しないときに\`\`を使うというのは考えたこともなかったけど、
- 全部\`\`でも害は無さそう
- 後でタグ付け、変数展開したくなったときに''や""から直さなくてもいい
- '', "", \`\`の使い分け意思決定が不要
という観点で、全部\`\`でもいいんじゃないかと思えてきた。
どうなんだろ https://t.co/aUCmwCH8gD
JavaScript には現在、3 種類の「文字列リテラル」1 があります。
'シングルクオーテーション'
"ダブルクォーテーション"
`テンプレートリテラル`
ECMAScript 2019 仕様書 (https://www.ecma-international.org/ecma-262/10.0/) を参照しながら、これらの使い分けについて考えてみようと思います。
リテラルの文法の違い
まずは、3 種類のリテラルの文法(lexical grammar)がどう違うのかを見てみましょう。
シングルクオーテーション・ダブルクォーテーションの文字列は、11.8.4 String Literals で定義されています。
- StringLiteral ::
- " DoubleStringCharactersopt "
- ' SingleStringCharactersopt '
- DoubleStringCharacters ::
- DoubleStringCharacter DoubleStringCharactersopt
- SingleStringCharacters ::
- SingleStringCharacter SingleStringCharactersopt
- DoubleStringCharacter ::
- SourceCharacter but not one of " or \ or LineTerminator
- <LS>
- <PS>
- \ EscapeSequence
- LineContinuation
- SingleStringCharacter ::
- SourceCharacter but not one of ' or \ or LineTerminator
- <LS>
- <PS>
- \ EscapeSequence
- LineContinuation
この 2 種類のリテラルは、リテラル内(DoubleStringCharacter・SingleStringCharacter)の '
と "
の扱いが異なるだけで、他は全く同じです。
また、共に StringLiteral として定義されているので、構文(syntactic grammar)上で区別されることはないようです。
一方、テンプレートリテラルは、11.8.6 Template Literal Lexical Components に定義があります。
- Template ::
- NoSubstitutionTemplate
- TemplateHead
- NoSubstitutionTemplate ::
- ` TemplateCharactersopt `
- TemplateHead ::
- ` TemplateCharactersopt ${
- TemplateSubstitutionTail ::
- TemplateMiddle
- TemplateTail
- TemplateMiddle ::
- } TemplateCharactersopt ${
- TemplateTail ::
- } TemplateCharactersopt `
- TemplateCharacters ::
- TemplateCharacter TemplateCharactersopt
- TemplateCharacter ::
- $ [lookahead ≠ {]
- \ EscapeSequence
- \ NotEscapeSequence
- LineContinuation
- LineTerminatorSequence
- SourceCharacter but not one of ` or \ or $ or LineTerminator
Head や Middle、 Tail という単語が出てきているのは、テンプレートリテラルの中に式を埋め込むことができるからです 2。これが他の文字列リテラルとの大きな違いです。(もう一つ、テンプレートリテラルは改行を含むことができるという違いもあります。)
`普通の文字列`
// ^^^^^^^^^^^^^
// NoSubstitutionTemplate
`任意の式を埋め込める: ${1 + 2} とか ${func()} とか`
// ^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^ ^^^^^^
// TemplateHead TemplateMiddle TemplateTail
また、テンプレートリテラルは StringLiteral の一種ではないということにも注目すべきです。
構文上の扱いの違い
Primary Expression
12.2 Primary Expression を見てみましょう。
- PrimaryExpression[Yield, Await] :
- this
- IdentifierReference[?Yield, ?Await]
- Literal
- ArrayLiteral[?Yield, ?Await]
- ObjectLiteral[?Yield, ?Await]
- FunctionExpression
- ClassExpression[?Yield, ?Await]
- GeneratorExpression
- AsyncFunctionExpression
- AsyncGeneratorExpression
- RegularExpressionLiteral
- TemplateLiteral[?Yield, ?Await, ~Tagged]
- CoverParenthesizedExpressionAndArrowParameterList[?Yield, ?Await]
PrimaryExpression は、式の基本的な構成要素で、ひとまとまりの式(項)を表します(5
, a
, [1, 2, 3]
, (a + b)
など。a + b
, f(x)
, a.b
などは含まれません)。
TemplateLiteral は、すぐに見つかりますね。StringLiteral はどこにいるのかというと、
- Literal :
- NullLiteral
- BooleanLiteral
- NumericLiteral
- StringLiteral
Literal にまとめられています。
StringLiteral も TemplateLiteral も共に PrimaryExpression として扱われることから、式が必要とされる場所においては、この 2 つに区別はないようです。
少し歯切れの悪い言い方になりましたが、これは、StringLiteral が要求される場所・TemplateLiteral が要求される場所が存在するためです。
Object Initializer
StringLiteral が特別扱いされる 1 つ目は、12.2.6 Object Initializer(オブジェクトリテラル)のプロパティ名です。
- PropertyName[Yield, Await] :
- LiteralPropertyName
- ComputedPropertyName[?Yield, ?Await]
- LiteralPropertyName :
- IdentifierName
- StringLiteral
- NumericLiteral
- ComputedPropertyName[Yield, Await] :
- [ AssignmentExpression[+In, ?Yield, ?Await] ]
LiteralPropertyName に StringLiteral が含まれています。つまり、StringLiteral はプロパティ名の位置に書ける 3 ということです。
const object = {
id: 1,
"string literal": "foo",
0: "bar",
}
一方、TemplateLiteral はここに含まれていないので、プロパティ名として使うには ComputedPropertyName でなければなりません。
const object = {
[`template literal`] : 0,
[`1 + 2 = ${1 + 2}`] : 3,
}
Computed property は、プロパティのキーとして任意の式を書ける構文で、動的にプロパティ名を設定するほか、Symbol をキーにするのにも使われます。
Imports
StringLiteral のみが許可される場所は、他にもあります。15.2.2 Imports のモジュール名 4 です。
- ImportDeclaration :
- import ImportClause FromClause ;
- import ModuleSpecifier ;
- (中略)
- ModuleSpecifier :
- StringLiteral
ModuleSpecifier の位置に書けるのは StringLiteral だけです。
import "path/to/module" // ok
import 'path/to/module' // ok
import `path/to/module` // error
Directive Prologues
聞きなれない言葉かもしれませんが、関数やスクリプトの始めの "use strict"
がこれに当たります。Directive Prologue は、コードの先頭に単独で現れる StringLiteral の並び 5 として定義されています(14.1.1 Directive Prologues and the Use Strict Directive)。
現在の ECMAScript の仕様では "use strict"
以外の Directive は定義されていませんが、処理系によっては他の Directive もサポートされているかもしれません。
当然、Directive のつもりで
`use strict`
と書いても、ただの式文とみなされるので、何も効果がありません。
Tagged Templates
TemplateLiteral が特殊な意味を持つ唯一のケースが 12.3.7 Tagged Templates です。
関数(メソッド)の直後に TemplateLiteral を書く 関数呼び出しの形式で、主な用途は String.raw
(いわゆる raw 文字列の生成)とメタプログラミングです。
const text = String.raw`tab: \t, lf: \n, ${2 * 3}`
text // "tab: \\t, lf: \\n, 6"
詳しく説明しようとすると別の記事が一つ出来上がってしまうので、ここでは紹介だけにとどめておきます。
考察(主観的)
とりあえず、import 文とディレクティブの文字列リテラルはテンプレートリテラルで代用できないので、「全て」テンプレートリテラルにするのは無理だとわかりました。
そこで、「どう使い分けるか」というのが問題になるのですが、すぐに思いつくのは以下の 2 通りの考え方でした。
- 上記の例外を除いてテンプレートリテラルを使う。
- 式を埋め込む時だけテンプレートリテラルを使う。
しかし、テンプレートリテラルを可能な限り使うとすると、どこが動的に変化し、どの文字列は変化しないのかが、わかりにくくなります。
一方、テンプレートリテラルの使用を最低限にすると、式を埋め込みたくなった時に引用符を書き直さなければなりません。
StringLiteral と TemplateLiteral の使い分け
StringLiteral が特別に扱われる場所をまとめてみましょう。
- オブジェクトリテラルのプロパティ名
- import 文
- Directive(
"use strict"
)
これらの共通点は、文字列の値が静的に 6 評価・使用されている点です。つまり、StringLiteral のみを期待しているのは、値が実行時ではなくコードを解析した時点で決定してほしい場所だ、ということです。
確かに、TemplateLiteral は式を埋め込む場合 実行時に値が決まるのに対し、StringLiteral は常に同じ値に評価されます。
この性質に着目すると、
- 値が静的に意味を持つ、すなわち「定数」7 としての文字列には StringLiteral を使う
- それ以外の文字列には TemplateLiteral を使う
という使い分けができると思います。
ダブルクォーテーションとシングルクオーテーションの使い分け
StringLiteral を定数として使うことを提案しましたが、その場合どちらの引用符を使えばよいでしょうか。
現在、シングルクオーテーション '
がデファクトスタンダードになっているようです。しかし、C 系の言語では "
が使われていますし、'
でなければならない理由もないように思えます。
さて、もう一度仕様書に登場してもらいましょう。とは言っても、今回参照するのは概要の一節で、言語仕様の一部ではありません。4.2 ECMAScript Overview の最後の段落です。
ECMAScript syntax intentionally resembles Java syntax. ECMAScript syntax is relaxed to enable it to serve as an easy-to-use scripting language. For example, a variable is not required to have its type declared nor are types associated with properties, and defined functions are not required to have their declarations appear textually before calls to them.
始めの文を見てください。JavaScript は意図的に Java に似せてある、と書かれています(確かに、ES 2015 で導入された class 構文も、Java の影響を受けているようです)。
では、いっそのこと、Java に倣って 8
- ダブルクォーテーションは文字列定数を意図する時に使う
- シングルクオーテーションは文字定数を意図する時に使う
としてみてはどうでしょうか。
JavaScript では、文字列のインデックスアクセス・charAt
メソッド等で「1 文字の文字列」を(文字の代わりに)扱う機会があるので、文字列と「文字」を見た目だけであっても区別できると便利だと思います。
let text = "string" // 文字列
switch (text[0]) {
case 'c': // 文字
break
case 's': // 文字
break
case 'char': // ' の中に 4 文字あるのはおかしい
break
}
text = "A" // 1 文字でも文字列を意図している場合は " を使う
結論
JavaScript では型を明示することができないので、意味や意図をコードで表現しにくいという短所があります。
それを踏まえた上で コードの可読性・メンテナンス性の面で、私は以下の使い分けが良いと考えます。
-
' '
を「文字」定数の意味で使う -
" "
を文字列定数として使う -
` `
はその他全般の文字列を表すのに使う
-
仕様書では String Literal と Template Literal として呼び分けられていますが、ここでは文字列を表すリテラルをまとめて「文字列リテラル」と呼ぶことにします。 ↩
-
式の埋め込み(文字列補間)の文法は、lexical grammar ではなく、syntactic grammar として 12.2.9 Template Literals で定義されています。 ↩
-
JSON でお馴染みの記法ですが。 ↩
-
ModuleSpecifier なので、直訳するならモジュール指定子でしょうか。 ↩
-
厳密には、ASI(自動セミコロン挿入)処理後、StringLiteral にセミコロンが続いた文の繰り返しで、最長となるもの。 ↩
-
Static Semantics によって。 ↩
-
実行時の状態や プログラムの仕様・実装の詳細によって左右されない値。 ↩
-
Java に限らず、文字と文字列を区別する言語では よく見られる記法です。 ↩