これは解答例です。
問題はこちら ⇒ JavaScript 暗黒問題集 - Qiita
ここ見に来る人はたぶん仕様書くらい読めるだろうとの想定の元、一々当該箇所を参照しません(手抜き)。
解答と解説
問1. 私は私ではない
以下の関数 q1
が true
を返すような値 x
を与えよ。
function q1(x) {
return x !== x;
}
解答例
q1(NaN);
解説
厳密等価/不等価演算子(===
, !==
)において、NaN
は等価であるとみなされない。
単に知識を問う問題。あるいは、問 2 以降も解けそうだなと思ってもらうためのトラップ。
問2. 直観に反する
以下の関数 q2
が true
を返すような値の組 a
, b
を与えよ。
function q2(a, b) {
return a == b && a != b;
}
解答例
q2(0, {
value: 0,
valueOf() {
return this.value++;
}
});
解説
厳密でない等価/不等価演算子(==
, !=
) を用いているので、型変換が行われる。
オブジェクト型がプリミティブ型に変換される際には、valueOf
メソッドが呼び出されるので、そこで副作用を起こしてしまえば良い。
問3. さよなら推移律
以下の関数 q3
が true
を返すような値の組 a
, b
, c
を与えよ。
function q3(a, b, c) {
return a == b && b == c && a != c;
}
解答例
q3("-0", 0, "+0");
解説
問 2 と同様に valueOf
メソッドを用いて副作用を起こしても良いが、型変換の優先順位に着目すれば、より簡単に解くことができる。
解答例では、a == b
→ "-0" == 0
は、文字列から数値への変換が行われ 0 == 0
となる。
b == c
→ 0 == "+0"
も同様に 0 == 0
となる。
a != c
→ "-0" != "+0"
は、どちらも文字列であるため、型変換が行われず文字列として比較される。
問4. 同じトコロに違うモノ
以下の関数 q4
が true
を返すような値 x
を与えよ。
function q4(x) {
const a = [0, 1];
return x == x + x && a[x] != a[x + x];
}
解答例
q4({
valueOf() {
return 0;
},
toString() {
return "1";
}
});
q4(false);
解説
問 2 で述べたように、プリミティブ型が要求される演算でオブジェクト型を使用する場合には valueOf
メソッドが呼び出される。valueOf
メソッドがプリミティブ型を返さなかった場合は、toString
メソッドが呼び出されて文字列に変換される。
一方で、プロパティキーとしてオブジェクト型が使用されたときには、valueOf
メソッドを 呼び出さずに 直接 toString
メソッドが呼び出される。
解答例の前者では、以下のように変換が行われている。
-
x == x + x
→0 == 0 + 0
→0 == 0
-
a[x]
→a["1"]
-
a[x + x]
→a[0 + 0]
→a[0]
解答例の後者では Boolean の加算が数値に変換されることを利用して、false
を与えている。
-
x == x + x
→false == false + false
→0 == 0 + 0
→0 == 0
-
a[x]
→a["false"]
(プロパティが存在しないのでundefined
となる) -
a[x + x]
→a[0 + 0]
→a[0]
問5. 実例の実例
以下の関数 q5
が true
を返すような値 x
を与えよ。
function q5(x) {
return x instanceof x;
}
解答例
q5(Function);
q5(Object);
function v1() {}
v1.prototype = Function.prototype;
q5(v1);
function v2() {}
Object.setPrototypeOf(v2, v2.prototype);
q5(v2);
解説
A instanceof B
は、クラスベースのオブジェクト指向言語においては、A がクラス/インターフェース B のインスタンスであるかを判定するものであることが多い。
一方で、JavaScript は class
記法が導入されたものの実態はプロトタイプベースである。
JavaScript における instanceof
演算子は「A
のプロトタイプチェーン中に B.prototype
が存在するか」を判定するものである。
// A.__proto__.__proto__ ... __proto__ === B.prototype;
let p = A.__proto__;
while (p) {
if (p === B.prototype) {
return true;
}
p = p.__proto__;
}
return false;
問題では上記条件を満たしたうえで A == B となるものを与えれば良い。
(※仕様上、B が関数でなければ実行時エラーとなる点には注意。)
Function
コンストラクタは、この条件を満たしている。
Function.__proto__ === Function.prototype;
また Object
コンストラクタも条件を満たす。
Object.__proto__.__proto__ === Object.prototype;
もちろん、解答例 v1
, v2
のように、自力で目的のプロトタイプチェーンを構築しても良い。
問6. 入ってます
以下の関数 q6
が true
を返すような値 x
を与えよ。
function q6(x) {
return x in x;
}
解答例
q6({
toString() {
return "toString";
}
});
q6(new String("length"));
q6([0]);
解説
in
演算子は、A in B
において、プロパティ名 A が B に存在することを判定する。
A は文字列またはシンボルであることが期待されるが、いずれでもなければ toString
メソッドにより文字列に変換される。
B はプリミティブ型の場合実行時エラーとなるので、オブジェクト型である必要がある。
というわけで、「自身のプロパティ名に合致する文字列に変換可能なオブジェクト」を渡せばよい。
解答例のように、直接オブジェクトを作成しても良いし、文字列のラッパーオブジェクト(String
)を利用する手もある。
変わり種としては配列 [0]
も文字列 "0"
に変換され、[0]["0"]
が存在するので条件を満たすことができる。
問7. いちたりない
以下の関数 q7
が true
を返すような値 x
を与えよ。
function q7(x) {
return Array.isArray(x) &&
!Array.prototype.some.call(x, v => v) &&
x.length === 7 &&
new Set(x).size === 7 &&
Array.prototype.reduce.call(x, i => i + 1, 0) === 6;
}
解答例
q7([, 0, 0n, null, false, NaN, ""]);
解説
複数の条件を満たすオブジェクトを与える問題である。
それぞれの条件を見ていくと、
- 条件 1.
Array.isArray(x)
-
x
は配列である
-
- 条件 2.
!Array.prototype.some.call(x, v => v)
- 要素はいずれも真と評価されない(すべて偽と評価される)
- 条件 3.
x.length === 7
- 長さは 7 である
- 条件 4.
new Set(x).size === 7
- 集合に変換しても大きさが 7 である
- つまり、要素に重複が無い
- 条件 5.
Array.prototype.reduce.call(x, i => i + 1, 0) === 6
-
reduce
を用いてカウントした場合には 6 になる。
-
条件 1 から 4 までで、異なる 7 種の偽と評価される値を配列で渡せ、と言っていることになる。
JavaScript (ES2020) において、(環境依存のものを除けば) 偽と評価されるのは以下の 7 種である。
false
NaN
-
""
(空文字列,''
や``
は同じもの) null
undefined
-
0
-
+0
,-0
を区別することもできるが今回はSet
を用いているので同一
-
-
0n
-
BigInt
の 0 (ES2020 で導入) -
+0n
,-0n
を区別することもできるが以下略
-
これに加えて条件 5 では、reduce
を用いると総数が 6 になる、と言っている。
reduce
始め、map
や filter
等のメソッドは、疎な配列の場合空要素を無視するという特性があるので undefined
を空要素に置き換えてやればよい。
「配列と集合の基礎」+ 「Falsyな値」+「新たに加わった BigInt」+「疎な配列の扱い」をすべて抑えていれば解ける、という問題。
問8. おおきすぎる
以下の関数 q8
が、必ず true
を返すような関数 f
を与えよ。
function q8(f) {
const n = 4294967297;
const i = Math.floor(Math.random() * n);
const x = f(n);
return x[i] === i;
}
解答例
q8(n => {
return new Proxy({}, {
get(target, property, receiver) {
return Number(property);
}
});
});
解説
一見、インデックスと同じ値を入れた配列を作れば良いようにも思えるが、JavaScript の配列は長さが $2^{32} - 1$ まで、という制限があるため作成することができない。4294967297 は $2^{32} + 1$ である。そもそも実際に作成するには、おおきすぎる。
解答例では配列ではなく Proxy
を用いて、アクセスされたプロパティ名を数値に変換して返すオブジェクトを作成している。
別解と補遺
別解として、メモリを無視して仕様のみを考えれば以下のような解答もあり得る。
// 別解1
q8(n => {
const a = new Float64Array(n);
a.forEach((_, i, a) => a[i] = i);
return a;
});
// 別解2
q8(n => {
const o = {};
for (let i = 0; i < n; i++) {
o[i] = i;
}
return o;
});
別解 1 では、型付き配列を使用している。
型付き配列は通常の配列と違い、長さの制限が $2^{53}-1$ (double で正しく表現できる最大の整数)である。
別解 2 では、配列でなく通常のオブジェクトを用いている。
現行の仕様ではこれらは正しいコードであるはずなので、一応別解として載せた。
ただし現実問題としてメモリが潤沢にあったとしてもこれらを実行可能な処理系は、私は見たことが無いし、おそらく現状存在しない。
と言うのは、JavaScript の他の仕様が $2^{32}$ 個以上の要素/プロパティを持つオブジェクトを想定していないためである。例として Object.keys
がある。これは、オブジェクトのプロパティ名の一覧を「配列で」取得するメソッドであり、最大長 $2^{32}-1$ まで、という制限を受ける。
このような状況であるため、ほとんどの処理系においては要素数・プロパティ数が$2^{32}-1$を超えないという前提が置かれており、別解のコードは仕様に無い実行時例外を引き起こすかクラッシュする。型付き配列の上限が $2^{53}-1$ と言うのも絵にかいた餅である。
と、言うことを伝えたいがために、この問題を加えた。
変な別解が存在するので悪問の類だと思う。すまぬ。
問9. 見慣れない構文
以下の関数 q9
が、必ず true
を返すような値 each
を与えよ。
function q9(each) {
const array = new Array(10);
for (let i = 0; i < 10; i++) {
array[i] = Math.floor(Math.random() * 10);
}
const total = array.reduce((x, y) => x + y, 0);
for (each.item of array);
return each.sum === total;
}
解答例
q9({
sum: 0,
set item(v) {
this.sum += v;
}
});
解説
一般に for-of
は、以下のような形式で用いることが多い。
for (let x of elements) {
// ...
}
of
の左辺での変数宣言は必須ではなく、単に変数を使ってもかまわない。
let x;
for (x of elements) {
// ...
}
というか、代入の左辺になれる式なら、変数でなくて良い。
let a = [0];
for (a[0] of elements) {
// ...
}
そして、of
の左辺式はループの回数だけ評価される。
let count = 0;
function f() {
count++;
return { dummy: 0 };
}
for (f().dummy of elements) {
}
console.log(count === elements.length); // true
つまり、以下はだいたい同じだと思えば良い。
// これは
for (式 of elements) {
}
// これとだいたい同じこと
for (let item of elements) {
式 = item;
}
と言うわけで、setter を仕掛けてしまえば問のようなコードが実現できる。
問10. 次の日は別の人
以下の非同期関数 q10
が、最終的に true
を返すような関数 f
を与えよ。
async function q10(f) {
const x = await f();
const y = await x;
return !Object.is(x, y);
}
// q10(answer).then(value => console.log(value)); // true
解答例
q10(() => {
const obj1 = {};
Promise.resolve().then(() => {
obj1.then = resolve => {
const obj2 = {};
resolve(obj2);
};
});
return obj1;
});
解説
まず前提知識として、await
は Promise
の結果を待つもの......というわけではなく、then
と言う名前のメソッドで一回限りのコールバックを設定できるオブジェクト(Thenable) であれば、なんでも待つことができる。
await new Promise(callback => {
setTimeout(() => callback("some result value"), 1000);
});
// 以下でも同様
const promiseLike = {
then(callback) {
setTimeout(() => callback("some result value"), 1000);
}
};
await promiseLike;
また、await
は、与えられた値が Thenable でなければ、値をそのまま返す。(処理自体は一度中断される)
const v = await 10;
console.log(v); // 10
加えて、Thenable で返された値が Thenable であった場合、Thenable でなくなるまでさらに待つ。
const nested = {
then(callback1) {
callback1({
then(callback2) {
callback2(10);
}
});
}
};
const result = await nested;
console.log(result); // 数値の 10, オブジェクトではない
上記特性を踏まえれば、Thenable の Thenable (あるいは Promise
の Promise
) を作る方針ではこの問題はうまく行かないことがわかる。
1 回目の await
で最終結果まで待機してしまうので、2 回目の await
では同じ結果になってしまう。等価演算子 ===
ではなく Object.is
を用いているので、最終結果として NaN
を返しても true
にはならない。
解答例では、まず Thenable でないオブジェクトを返した上で、2 回目の await
までにプロパティを追加して Thenable に変えることで異なる値になるようにしている。
実行順序としては、以下のようになる。
-
q10
の実行が開始する-
f
が実行される-
obj1
を生成する -
Promise.resolve().then(...)
に渡したクロージャ (A) がジョブキューに積まれる -
f
がobj1
を返す
-
-
obj1
を値として 1 回目のawait
-
obj1
は Thenable でないので、await
の結果はobj1
となる -
q10
の残りの部分 (B) がジョブキューに積まれる
-
- 終了
-
- ジョブキューから (A) が取り出され実行される
-
obj1
にthen
メソッドを追加する (Thenable となる) - 終了
-
- ジョブキューから (B) が取り出され実行される
-
await
の結果obj1
がx
に代入される -
x
(=obj1
) を値として 2 回目のawait
- この時点では、
obj1
は Thenable なのでobj1.then
が呼び出される- コールバックにより
obj2
が次のawait
の値となる -
obj2
は Thenable ではないので、await
の結果はobj2
となる
- コールバックにより
-
q10
の残りの部分 (C) がジョブキューに積まれる
- この時点では、
- 終了
-
- ジョブキューから (C) が取り出され実行される
-
await
の結果obj2
がy
に代入される -
x
とy
は異なるオブジェクトなので!Object.is(x, y)
はtrue
となる -
true
を返却し、q10
の実行が終了する
-
所謂マルチスレッドと異なり、async/await は FIFO でスケジューリングされることが保証されているので、順序が入れ替わることは無い。
また、実行単位が仕様中で Job と表現されているのでスケジュールに用いる構造を便宜上「ジョブキュー」と表現したが、仕様書中では明確な名称は与えられていない。(単に enqueue するという扱い)。
おわりに
暗黒ってなんだろうな、と考えた結果、「べからず」集かなあと言うことでこんな感じになった。
問 2-4 は「普段は厳密等価演算子使おう」「想定困難な副作用を起こすな」と言う話であるし、
問 5-7 は「演算子や関数の意味を類推してなんとなくで理解するのやめよう」という話である。(問7は他の要素も入っているけれど)
問 8 は、べからずと言うよりは、無謀な実装しないで無難なワークアラウンド探そう、という話。(そして仕様と実装の狭間で起こる嘆きについて)
問 9-10 は、まあ、あまり遭遇しないと思うので、どちらかと言えば単に腕試しだろうか。解答例自体が「べからず」である、と言ってもいいかもしれない。
各種言語において色々な「べからず」はあると思うのだけれど、個人的に「べからず」に頼ったプログラミングというのが、あまりよくないなあと思っている。
「○○は危険なメソッドなのでこれからは△△を使いましょう!」と言っていた人が、結局どちらも理解しておらずバグを作りこむさまを過去に何度か見ている(し、自分にも心当たりがある。)
「避ける」よりも「知る」ほうが安全なのではないか、と言うのが今のところの持論であるのだけれど、実際に「やってはいけない」ことの対象ではなく機序を敢えて深く「知る」機会はあまりない。時間があったら「良い」ものを知ろうとするのが普通ですし。
じゃあ遊びに持っていけたらいいのかな、と言うことでこんなものを作ってみた次第。
何かが達成できているとは思っていないけれど、貴重なお暇を潰せたなら恐悦至極。