3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【JavaScript】とにかく、黒魔術がしたい!!!

3
Posted at

本編から読みたい方へ

雑な導入

みなさん、プログラミングをやっていてこんな経験はありませんか?

・『なんだこの仕様、、、』
・『うわマジックナンバーじゃん...... 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を見ると、

  1. 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).
  2. 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]++;

numthis[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で活動中)

感想

3
0
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
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?