TL;DR
-
「JS(ECMAScript)では、改行でセミコロンを代用できる」と勘違いしていた
-
実際は、以下のキーワードを解釈する際に文法上許されないトークンが出てきたら、その間にセミコロンを挿入している
- let, const
- import, export
- Expression statement
- debugger
- continue, break, throw
- return
-
具体的には
}
やline terminator
ごとに分解してセミコロンを挿入する -
上記の構文を使う時、改行で記述を分けているつもりでも、セミコロンがなければ予期しない解釈をされることがある
;
を忘れただけなのに 😢
皆さん、以下のコードはどんな出力になると思いますか?
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
以降を無くして、どう解釈されているか見てみましょう。
const x = {
y:1
}
["a","b"]
console.log(x) // 出力結果: undefined
ん??
これってもしかして、Array の部分までくっついてx
として解釈されてない?
つまり、以下のような形で、x
のプロパティの参照として解釈されているのでは?
const x = {
y:1
}["a","b"];
2. x
のプロパティの変更
じゃあ、以下のようにしてx
にa
とb
のプロパティを入れてやれば参照できる?
const x = {
a:1,
b:2
}["a","b"];
console.log(x) // 出力結果: 2
おお〜ちょっと気になる挙動だけどそれは置いといて、ちゃんとx
のプロパティの参照として解釈されてるっぽい。
3. 行末へのセミコロンの追加
ここまでわかったことをまとめると、お題のコードは以下のような流れで実行されたと考えられる。
-
x
と["a","b"]
がくっついて解釈 -
- により
x
の存在しないプロパティへの参照を実行
- により
-
undefined
を出力
これに対する解決策は単純で、1.が起きないように、行末にセミコロンを追加すればいい。
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
宣言を行う時でも、}
の後の記述は文法許されないトークンとなるため、セミコロンが改行の前に挿入される。
// 「const x = { y:1 }console.log(x); 」は文法上許されない
const x = {
y:1
}// よって、「文法上許されないトークンと前のトークン(x)の間」のこの位置にセミコロンが挿入される
console.log(x);
しかし、お題のコードは、}
の後の記述が、たまたま文法上許されるものになってしまった。
そのため、x
の存在しないプロパティへの参照という動作になった。
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
特にlet
やconst
を使う場合はセミコロンを挿入することを癖付けよう。
参考文献