セミコロンをつけ忘れただけなのに...【JavaScript】


はじめに

某小説っぽいタイトルにしちゃいましたが、
JavaScriptでコードを書いていた後輩が二つのコードの挙動の差に困っていたので、
原因究明をしようとしたら、最初は全然わからなかったのですが、いろいろ試してみたところ、どうやらいろんなJavaScriptの仕様が絡み合ってできた罠だったぽいので、
せっかくなので、今回でわかったことを記していこうと思います。

原因のコードのサンプル

エラーになるコード
function foo() {
  let point = {}
  try {
    const lat = 35
    const lng = 132
    [point.lat, point.lng] = [lat, lng]
  } catch (e) {
    console.error(e)
  }
}
foo()

みた感じ、tryブロックの中で、latとlngにセットされた値をpointオブジェクトに追加しているように見えると思います。
一見おかしなところがないように思えるのですが、このコードを実行すると以下のエラーになります。

VM50:8 ReferenceError: Cannot access 'lng' before initialization
    at foo (<anonymous>:6:36)
    at <anonymous>:11:1

しかし、以下のようにconstで初期化する処理をtryブロックの外に移動させるようにコードを変更すると実行することが可能になります。

宣言場所を変えただけのエラーにならないコード
function foo() {
  let point = {}
  const lat = 35
  const lng = 132
  try {
    [point.lat, point.lng] = [lat, lng]
  } catch (e) {
    console.error(e)
  }
}
foo()

なぜでしょう? というのが課題提起です。

解決策

上記エラーになるコードの方でconst lng = 132の後ろにセミコロンをつけるとエラーなく動きます。

なんで?

まずはエラーの内容を読み解く

このコードを読む前に表示されたエラーについて確認しましょう。

VM50:8 ReferenceError: Cannot access 'lng' before initialization
    at foo (<anonymous>:6:36)
    at <anonymous>:11:1

このエラーの内容はかなり明確で、constで定義しているlngに対して、初期化する前にアクセスしちゃっているからやめてねということを伝えようとしています。
でもソースコードをみてみると、明らかにlngを定義すると同時に初期化してからlngへのアクセスを行なっていますよね。

しかも、動く方のコードとの差分はtryブロックの外で定義するか中で定義するかの違いでしかありません。

const,letだからスコープの違い?

結論から言うとスコープの違いによることが理由ではありません。

動かないコードと動くコードの比較をしてみても、定義場所の違い以外に差があるようには思えないので、
一見ブロックスコープが影響しているように思う人もいるかもしれません。
たしかにconstとletはブロックでスコープを作りますが、それはブロック内で宣言されたconst,letの値に対してブロックの外では参照することができないというもので、ブロックの外で宣言されたものはブロックの中で参照することが可能です。

// ブロック内で定義されたものへの外からアクセス
if (true) { const hoge = 'fuga'}
console.log(hoge);
//VM196:1 Uncaught ReferenceError: hoge is not defined
//    at <anonymous>:1:13

// ブロック外で定義されたものへの内からアクセス
const hoge2 = 'fugafuga';
if (true) { console.log(hoge2) }
// fugafuga

つまり今回のエラーはスコープのエラーによるものではないですね。

考えること1: JavaScriptはなぜセミコロンがなくても動くのか?

私たちがJavaScriptでコードを記載している際に、セミコロンをつけずとも正しく構文を解釈してスクリプトの実行をすることができることはみなさんご存知のことだと思います。

let a = 1
console.log(a)
// => 1

これは裏側ではどのようなことが行われているのでしょうか?

ECMAScriptの仕様を見るとそのことについて記載されています。
11.9 Automatic Semicolon Insertion

Most ECMAScript statements and declarations must be terminated with a semicolon. Such semicolons may always appear explicitly in the source text. For convenience, however, such semicolons may be omitted from the source text in certain situations. These situations are described by saying that semicolons are automatically inserted into the source code token stream in those situations.

ざっくり内容を読んでみると、全てのステートメントと宣言はセミコロンで終わるべきであり、セミコロンがないときには特定条件下では自動で挿入すると記載されています。

上記11.9の下にある11.9.1も合わせて読んで見ると、
セミコロン挿入には3つの基本的なルールがあるという記載がありまして、
それらを読むともう少し具体的にルールがわかると思います。

ざっくりまとめると以下のような場合にセミコロンが自動で追加されます。
(和訳に自信がないので間違ってたら優しく教えてください)
コードを左から右に読み込んだときに
1. 文法の生成で許可されていないトークンが発見されたとき。
2. パーサーが入力トークンストリームをゴール非終端の単一インスタンスとして解析できない場合。
3. 文法の生成によって許可されているトークンが検出されたが、生成が制限された生成であり、トークンが直後のターミナルまたは非ターミナルの最初のトークンの時。

つまり、JavaScriptは上記のコードを一旦一行のコードとして解釈をしていて、文法上おかしかったり、宣言ステートメントが途中にあったりした場合には、ルールに乗っ取った上で自動でセミコロンを自動挿入した上で実行しているわけですね。

例えば以下のようなコードだったとしても、

let a
=
1
console.log(a)

以下のように解釈されて

let a = 1;console.log(a);

として実行されているわけですね。
だから私たちはセミコロンをつけずともJavaScriptを記載することができるのです。

ただ、今回のソースコードに落とし込むと、正しく解釈されず以下のように解釈されてしまったようです。

エラーになるコードにセミコロンが挿入された場合
function foo() {
  let point = {};
  try {
    const lat = 35;
    const lng = 132[point.lat, point.lng] = [lat, lng];
  } catch (e) {
    console.error(e);
  }
}
foo();

なぜでしょう?

考えること2: なぜ上記コードはセミコロンを自動でつけて実行してくれなかったのか?

結論から述べると文法上おかしな場所がなかったために、セミコロンが挿入されなかったことが原因です。
上記考えること1で一定のルールをもとにセミコロンを自動挿入することがわかるのですが、
この部分にはJSの文法上おかしなところがなかったので、jsは同一行が続いてると勘違いしてセミコロンが付与されなかったわけですね。

const lng = 132[point.lat, point.lng] = [lat, lng];

ECMAScriptの仕様ページの11.10.1 Interesting Cases of Automatic Semicolon Insertion in Statement Lists にも、今回のケースの内容が記載されておりました。

An opening square bracket ([). Without a semicolon, the two lines together are treated as property access, rather than an ArrayLiteral or ArrayAssignmentPattern.

tryの外に宣言した方の動くソースコードの方では、try {が宣言と呼び出しの中間にあるおかげで、const lng = 132;のようにセミコロンを挿入したので動いていたということです。

  const lng = 132;
  try {
    [point.lat, point.lng] = [lat, lng]

考えること3: const lng = 132[point.lat, point.lng] = [lat, lng];の挙動について

上記の考えること2の中で、文法上問題がないからセミコロンが入らないという話をしたのですが、

const lng = 132[point.lat, point.lng] = [lat, lng];

に対してPrettierをかけると以下のように変換してくれるようです。

const lng = ((132)[(point.lat, point.lng)] = [lat, lng]);

この時、[(point.lat, point.lng)]のところでは
上記式の場合にはArrayアクセスを行なっているように見えた場所はArrayアクセスではなくBracketアクセスを行なっているようです。
ブラケット内にてカンマ区切りで値をセットすると、末尾の項が返されるという仕様があるらしく、上記コードの場合にはpoint.langが返るようです。

試しに、ディベロッパーツールのconsoleに1,2,3,4,5とだけ記載すると以下のように返ってきます。

たしかに末尾の5が返されますね。

したがって、上記コードは文法上問題ないという形になるわけらしいです。

const lng = 132[point.lng] = [lat, lng];

難しい…

結論

僕は絶対セミコロンを忘れずにつけようとおもいます。

参考