18
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Systemi(システムアイ)Advent Calendar 2022

Day 17

JavaScript の不思議な挙動を追ったらセミコロンの仕様について知れたよ

Last updated at Posted at 2022-12-16

TL;DR

  • 「JS(ECMAScript)では、改行でセミコロンを代用できる」と勘違いしていた

  • 実際は、以下のキーワードを解釈する際に文法上許されないトークンが出てきたら、その間にセミコロンを挿入している

    • let, const
    • import, export
    • Expression statement
    • debugger
    • continue, break, throw
    • return
  • 具体的には}line terminatorごとに分解してセミコロンを挿入する

  • 上記の構文を使う時、改行で記述を分けているつもりでも、セミコロンがなければ予期しない解釈をされることがある

;を忘れただけなのに 😢

皆さん、以下のコードはどんな出力になると思いますか?

constArray.js
const x = {
  y:1
}

["a","b"].forEach((v)=>console.log(v))

普通に以下のような出力を想像したのではないでしょうか?

a
b

残念、これは不正解です。
実際には以下のようなエラーになります。

Error: Cannot read properties of undefined (reading 'forEach')

いやいや、Array ちゃんと宣言してるし、なんで undefined 扱いになってるの??

実験

理解できない挙動になったので、色々と実験してコードがどう解釈されてしまっているのか確かめます。

1. forEach の削除

まずは、forEach 以降を無くして、どう解釈されているか見てみましょう。

removeForEach.js
const x = {
  y:1
}

["a","b"]

console.log(x) // 出力結果: undefined

ん??
これってもしかして、Array の部分までくっついてxとして解釈されてない?
つまり、以下のような形で、xのプロパティの参照として解釈されているのでは?

hypothesis.js
const x = {
  y:1
}["a","b"];

2. xのプロパティの変更

じゃあ、以下のようにしてxabのプロパティを入れてやれば参照できる?

try.js
const x = {
  a:1,
  b:2
}["a","b"];

console.log(x) // 出力結果: 2

おお〜ちょっと気になる挙動だけどそれは置いといて、ちゃんとxのプロパティの参照として解釈されてるっぽい。

3. 行末へのセミコロンの追加

ここまでわかったことをまとめると、お題のコードは以下のような流れで実行されたと考えられる。

  1. x["a","b"]がくっついて解釈
    1. によりxの存在しないプロパティへの参照を実行
  2. undefinedを出力

これに対する解決策は単純で、1.が起きないように、行末にセミコロンを追加すればいい。

semicolon.js
const x = {
  y:1
};

["a","b"].forEach((v)=>console.log(v));

console.log(x);

/*

実行結果
    a
    b
    { y: 1 }

*/

おお、やっぱりセミコロンを入れたらちゃんと動いたね! forEach も動いたし、xの中身も問題ない。

めでたしめでたし。

何が起こったのか

いやいや、ちょっとまて。
JS ではセミコロンなくても動くんじゃなかったっけ??
連続した空行とかで勝手にステートメントの区切りを判断してるんじゃないの?

どうも実験の結果をみると、それとは違う処理を行っていることが今回エラーの原因と考えられる。

そこで、「js semicolon」などのワードでで色々調べてみると、 これが見つかった。以下はその抜粋。

Some JavaScript statements' syntax definitions require semicolons (;) at the end. They include:

・ var, let, const
・ Expression statements
・ do...while
・ continue, break, return, throw
・ debugger
・ Class field declarations (public or private)
・ import, export

訳:JavaScript文の構文定義には、最後にセミコロン(;)が必要なものがあります。それらは以下の通りです。

However, to make the language more approachable and convenient, JavaScript is able to automatically insert semicolons when consuming the token stream

訳:しかし、言語をより親しみやすく便利にするために、JavaScriptはトークン・ストリームを消費する際に自動的にセミコロンを挿入することができます

おお〜!jsはステートメントを連続した空行で区切ってるわけではなく、必要なトークンを定義するときに自動的にセミコロンを挿入しているのか。

さらに読みすすめると、、

When a token not allowed by the grammar is encountered, and it's separated from the previous token by at least one line terminator (including a block comment that includes at least one line terminator), or the token is "}", then a semicolon is inserted before the token.

訳:文法上許されないトークンがあり、それが前のトークンと少なくとも1つの行末(少なくとも1つの行末を含むブロックコメントを含む)で区切られているか、トークンが"}"である場合、その前にセミコロンが挿入されます。

これが今回の原因っぽい!

つまり、いつもならセミコロン無しでxのようなオブジェクトのconst宣言を行う時でも、}の後の記述は文法許されないトークンとなるため、セミコロンが改行の前に挿入される。

auto-insertion-semicolon.js
// 「const x = { y:1 }console.log(x); 」は文法上許されない
const x = {
  y:1
}// よって、「文法上許されないトークンと前のトークン(x)の間」のこの位置にセミコロンが挿入される

console.log(x);

しかし、お題のコードは、}の後の記述が、たまたま文法上許されるものになってしまった。
そのため、xの存在しないプロパティへの参照という動作になった。

error.js
const x = {
  y:1
}// 「const x = {y:1}["a","b"]」 は文法上許される

["a","b"].forEach((v)=>console.log(v))
// なので👆ここまで1つのトークンとして解釈されてしまう
// それにより、実行結果は undefined

まとめ

この現象は、以下ののステートメントで起こりうる。

  • let, const
  • import, export
  • Expression statement
  • debugger
  • continue, break, throw
  • return

特にletconstを使う場合はセミコロンを挿入することを癖付けよう。

参考文献

18
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?