6
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

(解答例) JavaScript 暗黒問題集

Last updated at Posted at 2020-07-12

これは解答例です。

問題はこちら ⇒ JavaScript 暗黒問題集 - Qiita

ここ見に来る人はたぶん仕様書くらい読めるだろうとの想定の元、一々当該箇所を参照しません(手抜き)。

解答と解説

問1. 私は私ではない

以下の関数 q1true を返すような値 x を与えよ。

function q1(x) {
  return x !== x;
}

解答例

q1(NaN);

解説

厳密等価/不等価演算子(===, !==)において、NaN は等価であるとみなされない。

単に知識を問う問題。あるいは、問 2 以降も解けそうだなと思ってもらうためのトラップ。

問2. 直観に反する

以下の関数 q2true を返すような値の組 a, b を与えよ。

function q2(a, b) {
  return a == b && a != b;
}

解答例

q2(0, {
  value: 0,
  valueOf() {
    return this.value++;
  }
});

解説

厳密でない等価/不等価演算子(==, !=) を用いているので、型変換が行われる。

オブジェクト型がプリミティブ型に変換される際には、valueOf メソッドが呼び出されるので、そこで副作用を起こしてしまえば良い。

問3. さよなら推移律

以下の関数 q3true を返すような値の組 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 == c0 == "+0" も同様に 0 == 0 となる。

a != c"-0" != "+0" は、どちらも文字列であるため、型変換が行われず文字列として比較される。

問4. 同じトコロに違うモノ

以下の関数 q4true を返すような値 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 + x0 == 0 + 00 == 0
  • a[x]a["1"]
  • a[x + x]a[0 + 0]a[0]

解答例の後者では Boolean の加算が数値に変換されることを利用して、false を与えている。

  • x == x + xfalse == false + false0 == 0 + 00 == 0
  • a[x]a["false"] (プロパティが存在しないので undefined となる)
  • a[x + x]a[0 + 0]a[0]

問5. 実例の実例

以下の関数 q5true を返すような値 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 が存在するか」を判定するものである。

instanceofの挙動
// 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. 入ってます

以下の関数 q6true を返すような値 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. いちたりない

以下の関数 q7true を返すような値 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 始め、mapfilter 等のメソッドは、疎な配列の場合空要素を無視するという特性があるので 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;
});

解説

まず前提知識として、awaitPromise の結果を待つもの......というわけではなく、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 (あるいは PromisePromise) を作る方針ではこの問題はうまく行かないことがわかる。

1 回目の await で最終結果まで待機してしまうので、2 回目の await では同じ結果になってしまう。等価演算子 === ではなく Object.is を用いているので、最終結果として NaN を返しても true にはならない。

解答例では、まず Thenable でないオブジェクトを返した上で、2 回目の await までにプロパティを追加して Thenable に変えることで異なる値になるようにしている。

実行順序としては、以下のようになる。

  • q10 の実行が開始する
    • f が実行される
      • obj1 を生成する
      • Promise.resolve().then(...) に渡したクロージャ (A) がジョブキューに積まれる
      • fobj1 を返す
    • obj1 を値として 1 回目の await
      • obj1 は Thenable でないので、await の結果は obj1 となる
      • q10 の残りの部分 (B) がジョブキューに積まれる
    • 終了
  • ジョブキューから (A) が取り出され実行される
    • obj1then メソッドを追加する (Thenable となる)
    • 終了
  • ジョブキューから (B) が取り出され実行される
    • await の結果 obj1x に代入される
    • x (=obj1) を値として 2 回目の await
      • この時点では、obj1 は Thenable なので obj1.then が呼び出される
        • コールバックにより obj2 が次の await の値となる
        • obj2 は Thenable ではないので、await の結果は obj2 となる
      • q10 の残りの部分 (C) がジョブキューに積まれる
    • 終了
  • ジョブキューから (C) が取り出され実行される
    • await の結果 obj2y に代入される
    • xy は異なるオブジェクトなので !Object.is(x, y)true となる
    • true を返却し、q10 の実行が終了する

所謂マルチスレッドと異なり、async/await は FIFO でスケジューリングされることが保証されているので、順序が入れ替わることは無い。

また、実行単位が仕様中で Job と表現されているのでスケジュールに用いる構造を便宜上「ジョブキュー」と表現したが、仕様書中では明確な名称は与えられていない。(単に enqueue するという扱い)。

おわりに

暗黒ってなんだろうな、と考えた結果、「べからず」集かなあと言うことでこんな感じになった。

問 2-4 は「普段は厳密等価演算子使おう」「想定困難な副作用を起こすな」と言う話であるし、
問 5-7 は「演算子や関数の意味を類推してなんとなくで理解するのやめよう」という話である。(問7は他の要素も入っているけれど)

問 8 は、べからずと言うよりは、無謀な実装しないで無難なワークアラウンド探そう、という話。(そして仕様と実装の狭間で起こる嘆きについて)

問 9-10 は、まあ、あまり遭遇しないと思うので、どちらかと言えば単に腕試しだろうか。解答例自体が「べからず」である、と言ってもいいかもしれない。


各種言語において色々な「べからず」はあると思うのだけれど、個人的に「べからず」に頼ったプログラミングというのが、あまりよくないなあと思っている。

「○○は危険なメソッドなのでこれからは△△を使いましょう!」と言っていた人が、結局どちらも理解しておらずバグを作りこむさまを過去に何度か見ている(し、自分にも心当たりがある。)

「避ける」よりも「知る」ほうが安全なのではないか、と言うのが今のところの持論であるのだけれど、実際に「やってはいけない」ことの対象ではなく機序を敢えて深く「知る」機会はあまりない。時間があったら「良い」ものを知ろうとするのが普通ですし。

じゃあ遊びに持っていけたらいいのかな、と言うことでこんなものを作ってみた次第。

何かが達成できているとは思っていないけれど、貴重なお暇を潰せたなら恐悦至極。

6
1
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
6
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?