本編から読みたい方へ
雑な導入
みなさん、プログラミングをやっていてこんな経験はありませんか?
・『なんだこの仕様、、、』
・『うわマジックナンバーじゃん...... 0x5f3759dfってなんだよ......』
・『なんかめっちゃスタイリッシュに書いてあるけどどうやって動いてるかわからね~~~!』
・『独自の型に対して死ぬほど演算子オーバーロードされてる、、、なにこれ?』
・『おいJavaScript君、型がばがばじゃんかなんだこれ (困惑)』
よくありますよね, ね (圧)?
このようなコードを「実務 / 開発中」に見るとぐえぇぇ....となってしまうのですが、
よく考えてみてください。
なんか自分しか知らない不思議な技を扱っているように見えませんか、、、
...
...
そうです
黒魔術ってかっこよくないですか!!!!
かっこいいんですよ!!!
黒魔術って!!
じゃあやりたいですよね?
やりましょう!!!!!!!!!!!
何を書きますか?
もちろんFizzBuzzですね!
書きましょう!!!
FizzBuzzの書き納め
できたもの
Array.prototype[Symbol.toPrimitive] = function(hint) {
this[1] ??= 1;
const num = this[1]++;
this[0] += (this[0].slice(4 - !(num%3) * 4, 4 + !(num%5) * 4) || num) + "\n";
if(num<1e2) return String(this);
return this[0].slice(8);
};
console.log( "" + ["FizzBuzz"] );
??????
解説
どこで何を呼び出しているのか、さっそく見ていきましょう!
出力
まずコードを見てください。
出力が
console.log( "" + ["FizzBuzz"] );
しかありません。
そうですね、そこで呼ばれています。
ということは、
"" + ["FizzBuzz"] == "<FizzBuzzの答え>"
となります。
何を言っているか分からないかもしれませんがそうなっています。
配列を文字列化するとなぜか動きます。
関数呼び出し
じゃあなんで関数が呼び出されているんだろう?
となるので、(強制)
ドキュメントのToStringを見ると
ドキュメント見るのこの章だけなので許してくだせぇ...
9. Assert: argument is an Object.
10. Let primValue be ? ToPrimitive(argument, string).
11. Assert: primValue is not an Object.
12. Return ? ToString(primValue).
って書いてあって何言ってるんだ?っていう感じではあるけど、
要はこう、
9. // 1~8が当てはまらないならargumentオブジェクト (Arrayなのでここに行きつく)
10. primValue := ToPrimitive(argument, string);
11. // primValueはオブジェクトじゃないよ (確認)
12. return ToString(primValue);
もっと言えば、
return argument |> ToPrimitive |> ToString
ということで、ToPrimitiveというやつが呼び出されます。
じゃあ、ToPrimitiveってなんだ?
っとなって下さい(願望)(命令)
で、ドキュメントのToPrimitiveを見ると、
- If input is an Object, then
a. Let exoticToPrim be ? GetMethod(input, %Symbol.toPrimitive%).
b. If exoticToPrim is not undefined, then
i. If preferredType is not present, then
1. Let hint be "default".
ii. Else if preferredType is string, then
1. Let hint be "string".
iii. Else,
1. Assert: preferredType is number.
2. Let hint be "number".
iv. Let result be ? Call(exoticToPrim, input, « hint »).
v. If result is not an Object, return result.
vi. Throw a TypeError exception.
c. If preferredType is not present, let preferredType be number.
d. Return ? OrdinaryToPrimitive(input, preferredType).- Return input.
ながっ
いやまあそりゃそうですよね、、、
っということで関係あるところだけ抜き出してざっくり言うとこう!
(inputはarray)
a. exoticToPrim := input.prototype[Symbol.toPrimitive] //大体これが呼び出される
b. if(exoticToPrim != undefined):
i. ~ iii. // hintは"default"か"string"か"number"
iv. result := exoticToPrim(hint) // thisはinputになる
v. return result;
vi. //エラー処理
c. preferredType ||:= "number"; //設定されてなかったら"number"
d. OrdinaryToPrimitive(input, preferredType)
基本的に、配列ではArray.prototype[Symbol.toPrimitiveが存在していないのでdが動きます。
し か し
Array.prototype[Symbol.toPrimitive] function(hint) {
return string;
}
とすることで、なんと任意の関数を実行することができます!!
FizzBuzz
これで黒魔術終わり!
だったら寂しいので、FizzBuzzもひと工夫してみましょう!
とりあえずこれ↓がArray->Stringで実行されることはわかったので
Array.prototype[Symbol.toPrimitive] = function(hint) {}
2行目以降を見てみよう
Array.prototype[Symbol.toPrimitive] = function(hint) {
this[1] ??= 1;
const num = this[1]++;
this[0] += (this[0].slice(4 - !(num%3) * 4, 4 + !(num%5) * 4) || num) + "\n";
if(num<1e2) return String(this);
return this[0].slice(8);
};
とりあえず、コードを見てわかるのは
thisが長さ2の配列っぽさそうなこと。
ちなみに、thisは
最後の行で呼び出していたこれ
console.log( "" + ["FizzBuzz"] ); // this := ["FizzBuzz"]
実際そうで
-
this[0]は0を含むFizzBuzzの答え -
this[1]は カウンター、いわゆるi口調迷子
そしてfor文もwhile文もないので多分再帰だろうという事
2行目
this[1] ??= 1;
ヌル値合体代入演算子(名前かっこいい)で、
this[1] == undefinedだったら1になる(初期値)
3行目
const num = this[1]++;
num を this[1]にした後に𝒊𝒏𝒄𝒖𝒓𝒊𝒎𝒆𝒏𝒕𝒐している。
4行目
this[0] += (this[0].slice(4 - !(num%3) * 4, 4 + !(num%5) * 4) || num) + "\n";
FizzBuzzのすべてを担っている場所
で、まず思い出してほしいのがthisの初期値
最後の行で呼び出していた。
console.log( "" + ["FizzBuzz"] ); // this := ["FizzBuzz"]
これを見ると、this[0]の初期値は"FizzBuzz"だと分かるんご!!
で、this[0]は4行目でしか変更されなくて
+=となっていることから最初の8文字は変わらない。
一番最初の"FizzBuzz"は変わることはないんだよね。
なので
"FizzBuzz".slice(start, end)
このように表すことができる!
そして、対応表はこんな感じ
| FB | start | end | 3で割れるか | 5で割れるか |
|---|---|---|---|---|
| "" | 4 | 4 | 割れない | 割れない |
| "Fizz" | 0 | 4 | 割れる | 割れない |
| "Buzz" | 4 | 8 | 割れない | 割れる |
| "FizzBuzz" | 0 | 8 | 割れる | 割れる |
ということは
start := 4 - !(num%3) * 4
end := 4 + !(num%5) * 4
と表せる!
あとは
(this[0].slice( ...... ) || num) + "\n";
でおわり
5行目
if(num<1e2) return String(this);
このコードを面倒くさくしている全ての元凶
なぁにこれぇ
まず、
String(this)
は型変換なので、この関数を呼び出すトリガーとなります。
thisの中身は
this[0] = "FizzBuzz1\n2\nFizz\n4\nBuzz";
this[1] = 6;
のような感じになっています。これによって次の関数にインクリメントした値と現在の結果を渡すことができます。やったね!
で、(num < 1e2)によって100未満までに抑えられます~
そしてラスト!
6行目
return this[0].slice(8);
先頭についている"FizzBuzz"を削って返す!終わり!!!
return時に何かしら計算しているわけじゃなくて引数として渡して順々に計算して言ってるから本当に終わり!
string->stringになってるくらい!!
コード再掲
Array.prototype[Symbol.toPrimitive] = function(hint) {
this[1] ??= 1;
const num = this[1]++;
this[0] += (this[0].slice(4 - !(num%3) * 4, 4 + !(num%5) * 4) || num) + "\n";
if(num<1e2) return String(this);
return this[0].slice(8);
};
console.log( "" + ["FizzBuzz"] );
あとがき
ここまでお読みいただきありがとうございました!
私たちとクソコード、書きませんか。(Discordで活動中)
感想