この記事は、syumai Advent Calendar 2024 18日目の記事です。
まずはクイズにチャレンジ!
今回のJavaScriptクイズにチャレンジできる特設ページを作ったので、まずはこちらで遊んでみてください。
Shinyai.js 第1回 JavaScriptクイズ - https://shinyaijs.syum.ai/events/1
v0にベースを作ってもらって、@opennextjs/cloudflareを使ってCloudflare Workersにデプロイする形でホストしています。
Shinyai.jsとは?
Shinyaigeekの家で行われた、ただの飲み会です。 何故か全員発表資料があったので、自分の持ってきたクイズに回答したり、それぞれのスライドを映したりしながら酒を飲んでいました。
準備不足だったために、当日に解説をちゃんと用意できていなかった問題もいくつかあったのですが、その場で一緒にECMAScriptの仕様を読んでくれたuhyoさんに非常に助けられました。ありがとうございます!
今回の問題
下記Zennのスクラップを資料としてクイズを行いました(上記特設ページおよび、本ブログと問題の内容は同じです)
以下、解答と解説です。
もし不備があれば、ぜひXやブログのコメントでご連絡ください。
Question 1
問題文
以下のJavaScriptのコードについて、 ?
の部分を任意の英数字に置き換えて、有効となるものを3つ挙げてください。
実行については、ファイル名を script.mjs
とし、Node.jsによって node script.mjs
のコマンドで実行する (Moduleとして実行する) こととします。
? function () { console.log("Hello!"); };
解答↓
解答
- void
- typeof
- delete
- await
の4つ。
- new
- throw
も可で、これら6つのうち、3つ挙げられたら正解とする。
(補足: 当日は飲み会ということもあり、一番多く挙げられた方に点数を付けました。また、当初の公開時は4つで正解としていましたが、難しすぎたようなので3つに減らしました)
解説
単項演算子の問題です。
単項演算子には、
- delete
- void
- typeof
- +
- -
- ~
- !
- (Moduleの場合のみ) await
があります。
問題文より、この中から英数字のみで構成されるものを挙げると、解答に記載の4つになります。
void、typeof、awaitまでは挙げられても、deleteを挙げるのは難しいと思います。(これをプロパティの削除以外で使うという発想がないので)
newについては、元の想定解答には無かったのですが、クイズ開催当日にsadnessOjisanが発見してくれたので解答に加えました。ありがたい…。
(2024/12/19追記) Masaki Haraさんの指摘の通り、 throw
も有効な文法なので解答に加えました。
また、(直接解答には影響しませんが、)問題文の ?
の部分を省略すると、関数式ではなく関数宣言とみなされ、無名の関数を宣言することができないために不正な構文となります。
functionキーワードの前に単項演算子を置くことで、関数宣言ではなく式の一部とみなされ、有効な構文となります。
Question 2
問題文
以下のclass式について、有効なもの(ランタイムエラーが発生しないもの)はいくつあるでしょうか?
new class extends (function () {}) {}; new class extends (() => {}) {}; new class extends (async function () {}) {}; new class extends ({}.toString) {};
解答↓
解答
1 (new class extends (function () {}) {};
のみ)。
解説
これは、ある式の評価結果がコンストラクタになれるかどうかを問う問題です。
class宣言、およびclass式のextendsに伴うことができるのは、評価結果がコンストラクタとなる式のみ(8-g)です*1。
function () {}
は通常の関数で、問題なくコンストラクタになれる*2ため、有効です。
() => {}
のアロー関数、 async function() {}
の非同期関数はコンストラクタになれないため、無効です*3。
{}.toString
はビルトイン関数で、コンストラクタとして明示的に書かれているもの以外は全てコンストラクタになりません。
Built-in function objects that are not identified as constructors do not implement the Construct internal method unless otherwise specified in the description of a particular function.
ビルトイン関数の挙動は問題作成中に発見したのですが、仕様の方は当日にuhyoさんが見付けてくれました。ありがとうございます!
Question 3
問題文
以下のコードの出力結果は以下の選択肢 A, B のうちどちらでしょうか?
let a = 0; let b = 0; let c = 0; a ++ b ++ c console.log(a, b, c);
1 1 0
0 1 1
解答↓
解答
Bの 0 1 1
。
解説
自動セミコロン挿入の問題です。
a ++ b ++ c
に対して、
a ++ ;b ++ ;c
か、
a ;++ b ;++ c
のどちらにセミコロンが挿入されるかを問う問題となっています。
セミコロンの自動挿入は、基本的にはoffending token(どんな生成規則にも一致しないトークン)の直前に改行文字がある場合に、そのoffending tokenの直前に挿入する形で行われます。ただし、これだけでは不十分な場合に、文法生成規則に対して restricted production (制限された生成規則)が追加される場合があります。具体的には、 [no LineTerminator here]
という制限です。
今回の問題の、Update Expressionsの文法生成規則は以下の通りです。
a++
も、 ++b
も有効なため、[no LineTerminator here]
の制限がなければどちらのパターンも適用されうるということになってしまいますが、この制限によって、 a{改行文字}++
が無効になるため、セミコロンが自動挿入される位置が、
a ;++ b ;++ c
に定まります。したがって、解答はBになります。
この問題は、Update Expressionsの [no LineTerminator here]
が、文法生成規則上のどこに示されているかを記憶していれば解ける問題でした。(一回セミコロン自動挿入の仕様を読んだことがあれば何となく覚えているんじゃないかと思います)
Question 4
問題文
以下のコードの出力結果は以下の選択肢 A, B, C, D のうちどれでしょうか?
const a = 010; const b = Number("010"); const c = Number("0010"); console.log(a, b, c);
10 10 10
8 10 10
8 8 10
8 8 8
解答↓
解答
Bの 8 10 10
。
解説
Number constructorの仕様の問題です。
010
の数値リテラルは、先頭に0がついているため、LegacyOctalIntegerLiteralに相当し、8進数として扱われます。そのため、値は 8
となります。
次に、Number constructorに渡されている "010"
と "0010"
がどのように扱われるかについて考えます。
Number constructorに渡された、数字からなる文字列の扱い
Number constructorに文字列が渡されたとき、その値はToNumericを経由してToPrimitiveへと渡っていきます。
ToPrimitiveにおいて、文字列はただのプリミティブな値なので、特に変換は行われず、そのまま返っていきます。
続いて、その結果を受け取ったToNumericは、ToNumber abstract operationに文字列を渡します。
ToNumberは、文字列を受け取るとStringToNumber abstract operationに渡し、ここでStringNumericLiteralとしてパースされます。
最終的に、このリテラルに対応する数値はDesimalDigitsのMV(Mathematical Value)のセマンティクスによって定まります。
The MV of DecimalDigits :: DecimalDigits DecimalDigit is (the MV of DecimalDigits × 10) plus the MV of DecimalDigit.
ここで、010
の値は、
- MV(010) = MV(01) * 10 + 0
- MV(01) = MV(0) * 10 + 1
- MV(0) = 0
を総合した結果であり、
- MV(010) = ((0 * 10) + 1) * 10 + 0 = 10
となります。
0010
についても同様で、単に上位の桁の0を読み飛ばして、10進数として解釈した値になります。
したがって、解答はBになります。
010
を見て、8進数だ!とすぐに思い当たる方は、逆に間違った回答を行ってしまう可能性がある引っ掛け問題となっています。
最後に
今回の問題は、主にECMAScript仕様輪読会の中で発見したものから出題しています。
ECMAScript仕様輪読会は2週間に1回、火曜日の開催で、準備不要のゆるめの枠組みで実施しています。
新規参加大歓迎ですので、ぜひ気軽にご参加ください。YouTubeにて、視聴専用の枠もあります!Zoomで一緒に参加してくれた方が嬉しいのでぜひそちらもお願いします。
次回は年明けの1/7(火)に開催となります。
ECMAScriptの仕様は非常にややこしいですが、不思議な振る舞いについても仕様を読んだらきちんと書かれているので、読む練習を怠らないようにしていきたい、という気持ちに改めてなりました。(プロダクトではこんなコード書かないですが)
ぜひ皆さんも、この機にECMAScriptの仕様を読んでみてください!
*1:仕様上、nullも伴うことができるはずですが、実際に実行するとエラーになります。この振る舞いがどの仕様から来ているのかは見付けられていません
*2:関数式の評価時に、MakeConstructor abstract operationの呼び出しを伴うため https://tc39.es/ecma262/2024/multipage/ecmascript-language-functions-and-classes.html#sec-runtime-semantics-instantiateordinaryfunctionexpression
*3:式の評価時に、MakeConstructor abstract operationの呼び出しを伴わないため https://tc39.es/ecma262/2024/multipage/ordinary-and-exotic-objects-behaviours.html#sec-makeconstructor