21
11

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 5 years have passed since last update.

本格JavaScript記号プログラミング(2) 記号プログラム変換器を作ろう

Posted at

おことわり:
前回の記事を投稿したところ、「モバイル端末で読もうとすると、ページを開いた瞬間にブラウザがフリーズして落ちる」というお声を頂きました。
単行で1万文字超にも及ぶ記号プログラムにシンタックスハイライトがかかることで、大量のspanタグが生成されて高負荷となるようです。
対策として、横幅が画面に収まらないほどの巨大な記号プログラムについてはハイライトを行わない、または記事中には掲載せず、代わりにGistへのリンクを記載するようにしています。


第1回ではJavaScript記号プログラミングについての基礎的な考え方を中心として取り上げました。
そのため、実際に記号だけでコードを書く場面では、「このようにすればできますね」というように方向性のみを示すにとどめた箇所が多くあります。
今回の記事では、JavaScriptプログラムを記号化するプログラムの作成を題材として、前回の記事をなぞりながら、実際に記号だけで書かれたプログラムを扱っていきます。

レギュレーション

記号プログラムの制約は、前回に引き続き以下のようなものとします。

  • プログラム中に使用してよいのは、[]()+=の6種類の記号のみ。
  • ブラウザとnode.jsの両方で動作しなければならない。
    • よって、明示的なwindowglobalへのアクセスは禁止。globalThis1も(この記事を書いている途中に殆どの処理系で実装が進みましたが)まだ存在しないということにしておきます。
    • 実行環境は記事執筆時点でのnode.jsとChromeの最新安定版とします。

記号プログラム変換器を作る

今回は、記号プログラミングの実践も兼ねて「JavaScriptプログラムを受け取り、記号化されたプログラムに変換するプログラム」を作ってみます。
ただし、まずは「とりあえず動く」ものを目指すことにします。
記号プログラミングの常としてものすごい長さのプログラムになるかもしれませんが、今回はそのあたりは特に気にせず進めていきます。

仕様と方針

作るプログラムの仕様は、以下のようにします。

  • JavaScriptで書かれたプログラムを文字列として入力する
  • []()+=の6種類の記号のみを使ったプログラムを文字列として出力する
  • 出力結果をJavaScriptとして実行すると、入力として与えたプログラムを実行した場合と同じ処理をする

入力文字列を1文字ずつ記号で表現して、最後にevalに通すことにすれば、実装が楽そうでいいですね。そうしましょう。
JavaScriptプログラム中にはどのような文字が現れるかわからないので、文字ごとに対応する記号表現を用意するのは大変です。
そこで、String.prototype.codePointAt()で全ての文字を単なる整数に変換してしまいましょう。
そして、記号プログラムの実行時にString.fromCodePoint()でもともとの文字列を復元し、それをevalで実行させます。

たとえば、1+1を実行する場合、"1""+"のコードポイント値はそれぞれ49, 43ですから、"1+1"という文字列は49, 43, 49という3つの数値の列で表すことができます。
よって、

eval(String.fromCodePoint(49, 43, 49));

というプログラムで、1+1を実行して計算結果である2を得ることができますね。
これを記号で表現したコードが生成できれば、どのような入力が来ても大丈夫そうです。

ただし、実のところ前回の記事で解説した内容ではメソッド呼び出し時に2つ以上の引数を渡すことが困難です。
手っ取り早く済みそうなのは、以下のような形式で書いてしまうことです2

eval(eval("String.fromCodePoint(49, 43, 49)"));

evalに加えて"eval", "String.fromCodePoint"という文字列、数値、および"'", "(", ")", ","の文字が記号で表現できれば、このようなプログラムを記号で書くことができます。
今回は、このような方針で記号プログラム変換器を作っていきます。

evalを得る

前回の記事のおさらいをしましょう。

[]["find"]["constructor"]("return eval")()

evalへの参照が得られるので、このコード中の文字列をすべて記号で表現したものがevalに対応する記号プログラムとなります。
ここに含まれている文字は、半角スペースとacdefilnorstuvです。
これらの文字が全て記号で表現できれば、+で足していくことで"find", "constructor", "return eval"を構築できます。

この記事中で数百文字・数千文字の記号コードを全部提示していると紙面がいくらあっても足りませんので、
https://gist.github.com/Tatamo/5762f14941889675c4932972ed17deb8#file-eval-js
evalを得るまでのコードを記載しています。
実際に実行することで、記号で書かれたevalが出力されます。曰く、

// === eval
[][[[[]==[]]+[]][+[]][+[]]+[[+[]==[]]+[+[]]+[][+[]]+[]][+[]][++[+[]][+[]]+[+[]]]+[[+[]==[]]+[][+[]]][+[]][++[+[]][+[]]+[+[]]]+[[][+[]]+[]][+[]][++[++[+[]][+[]]][+[]]]][[[][[[[]==[]]+[]][+[]][+[]]+[[+[]==[]]+[+[]]+[][+[]]+[]][+[]][++[+[]][+[]]+[+[]]]+[[+[]==[]]+[][+[]]][+[]][++[+[]][+[]]+[+[]]]+[[][+[]]+[]][+[]][++[++[+[]][+[]]][+[]]]]+[]][+[]][++[++[++[+[]][+[]]][+[]]][+[]]]+[[+[]==[]]+[][[[[]==[]]+[]][+[]][+[]]+[[+[]==[]]+[+[]]+[][+[]]+[]][+[]][++[+[]][+[]]+[+[]]]+[[+[]==[]]+[][+[]]][+[]][++[+[]][+[]]+[+[]]]+[[][+[]]+[]][+[]][++[++[+[]][+[]]][+[]]]]][+[]][++[+[]][+[]]+[+[]]]+[[+[]==[]]+[][+[]]][+[]][++[+[]][+[]]+[+[]]]+[[[]==[]]+[]][+[]][++[++[++[+[]][+[]]][+[]]][+[]]]+[[+[]==[]]+[]][+[]][+[]]+[[+[]==[]]+[]][+[]][++[+[]][+[]]]+[[+[]==[]]+[]][+[]][++[++[+[]][+[]]][+[]]]+[[][[[[]==[]]+[]][+[]][+[]]+[[+[]==[]]+[+[]]+[][+[]]+[]][+[]][++[+[]][+[]]+[+[]]]+[[+[]==[]]+[][+[]]][+[]][++[+[]][+[]]+[+[]]]+[[][+[]]+[]][+[]][++[++[+[]][+[]]][+[]]]]+[]][+[]][++[++[++[+[]][+[]]][+[]]][+[]]]+[[+[]==[]]+[]][+[]][+[]]+[[+[]==[]]+[][[[[]==[]]+[]][+[]][+[]]+[[+[]==[]]+[+[]]+[][+[]]+[]][+[]][++[+[]][+[]]+[+[]]]+[[+[]==[]]+[][+[]]][+[]][++[+[]][+[]]+[+[]]]+[[][+[]]+[]][+[]][++[++[+[]][+[]]][+[]]]]][+[]][++[+[]][+[]]+[+[]]]+[[+[]==[]]+[]][+[]][++[+[]][+[]]]]([[+[]==[]]+[]][+[]][++[+[]][+[]]]+[[+[]==[]]+[]][+[]][++[++[++[+[]][+[]]][+[]]][+[]]]+[[+[]==[]]+[]][+[]][+[]]+[[+[]==[]]+[]][+[]][++[++[+[]][+[]]][+[]]]+[[+[]==[]]+[]][+[]][++[+[]][+[]]]+[[+[]==[]]+[][+[]]][+[]][++[+[]][+[]]+[+[]]]+[[][[[[]==[]]+[]][+[]][+[]]+[[+[]==[]]+[+[]]+[][+[]]+[]][+[]][++[+[]][+[]]+[+[]]]+[[+[]==[]]+[][+[]]][+[]][++[+[]][+[]]+[+[]]]+[[][+[]]+[]][+[]][++[++[+[]][+[]]][+[]]]]+[]][+[]][++[++[++[+[]][+[]]][+[]]][+[]]+[++[+[]][+[]]]]+[[+[]==[]]+[]][+[]][++[++[++[+[]][+[]]][+[]]][+[]]]+[[][[[[]==[]]+[]][+[]][+[]]+[[+[]==[]]+[+[]]+[][+[]]+[]][+[]][++[+[]][+[]]+[+[]]]+[[+[]==[]]+[][+[]]][+[]][++[+[]][+[]]+[+[]]]+[[][+[]]+[]][+[]][++[++[+[]][+[]]][+[]]]]+[]][+[]][++[++[+[]][+[]]][+[]]+[++[++[++[+[]][+[]]][+[]]][+[]]]]+[[[]==[]]+[]][+[]][++[+[]][+[]]]+[[[]==[]]+[]][+[]][++[++[+[]][+[]]][+[]]])()

2,077文字らしいです。これを手で書くのは少し苦労するかもしれませんね。

"String.fromCodePoint"を得る

こちらも前回の記事で取り上げた内容と被っていますが、String.fromCodePoint(またはString.fromCharCodeを得るのは少し複雑な過程を経る必要がありました。
今回はString.fromCodePointというメソッドそのものではなく、文字列としての"String.fromCodePoint"を用意する必要がありますが、基本的な方法は変わりません。

Object.getOwnPropertyNames(String).sort()[1]

というコードを実行して、"fromCharCode"の文字列を取得することにします。3
また、"String"という文字列はString.nameから得ることができます。

これらを文字列を用いたプロパティアクセスの形で書き直して、

""["constructor"]["name"]+"."+[]["entries"]()["constructor"]["getOwnPropertyNames"](""["constructor"])["sort"]()[1]

ここに含まれる文字は、acegimnoprstuwyNOP.です。
この中で、入手にやや難がある文字は"p", "w", "P"でしょう。
これらは、以下のようにして取得する必要がありました。

const p = (25).toString(36); // "p"
const w = (32).toString(36); // "w"
const P = (Function("return async "+Function())()()+"")[8] // "[object Promise]"[8] === "P"

ここで、"toString"を構築するための"String"""["constructor"]["name"]で取得でき、他の文字は既に取得済みのものです。

以上の内容をもとに、"String.fromCodePoint"の記号表現を得るためのコードは
https://gist.github.com/Tatamo/5762f14941889675c4932972ed17deb8#file-fromcodepoint-js
となります。
これを実行してみると、22,564文字の記号プログラムが出力されます。さすがに長いですね。

自然数を記号に変換する

次は、実行したいプログラムのそれぞれの文字のコードポイント値を記号の形で表現することを考えましょう。

とりあえずインクリメントで作ってみる

0+[]で表現することができ、++[n][0]と書けばn+1を得ることができるのでした。
よって0以上の整数は帰納的に構築可能であり、これに従うと、自然数を記号表現に変換する処理は以下のように書くことができます。

function convertNumber(n) {
  const zero = "+[]";
  if (n === 0) return zero;
  return `++[${convertNumber(n - 1)}][${zero}]`;
}

eval(convertNumber(n))とした結果がnとなりますが、このプログラムには以下の3つの問題があります。

  1. 出力結果が長すぎる
  2. $n=20000$ ぐらい4convertNumber(n)がスタックオーバーフローを起こす
  3. それより前に、$n=800$ ぐらいでeval(convertNumber(n))がスタックオーバーフローを起こす

1.の問題として、数値をインクリメントするのに9文字必要になるので、単純に作りたい数を9倍した数(に0を表現するための3文字分を足した数)がその数値を表現するためのコードの文字数になります。
そもそも記号プログラムを書いている時点でコードがものすごい長さになるのは織り込み済みですが、しかし100を表現するのに900文字では、さすがに割に合わない気がします。

2.は単純に再帰呼び出しをネストしすぎてコールスタックを使い果たすために起きるエラーであり、何も考えずに再帰関数を書いた際に起きがちな問題ですね。
ES2019がリリースされたというのに、未だにES2015仕様である末尾再帰最適化をサポートする主要な処理系がSafariしかない5ので、この関数を末尾再帰の形に書き直したとしても問題は解決しません。

再帰を使わずにループで書き直せば1.の問題は解決する、と思いきや、結局3.の問題が生じます。
convertNumber(n)で記号コードの生成に成功しても、それをevalするときに配列の添字アクセスがネストしすぎてしまうため、やはりスタックオーバーフローが発生してしまいます。6

一桁ずつ生成し、文字列として結合する

そこで、はじめからすべて数値として作るのではなく、一桁ずつ個別に生成したのち、文字列として繋ぎ合わせて目的の数を作ることにします。
たとえば42を作る場合、"4"+"2""42"を作り、+"42"とすれば目的の42を得ることができますね。
さらに記号プログラム的な書き方に寄せると、 +[[4]+[2]]と書くことができます。

([4]+[2]の部分では、二項演算+によって[4][2]が暗黙的にプリミティブに変換され、それぞれ"4""2"になって7文字列の結合が行われます。つまり+["42"]になりますが、hint値"number"のもとでの配列からプリミティブへの変換は、まず["42"].valueOf()["42"]自身を返すために失敗し、フォールバックとして["42"].toString()が呼び出されることになり、結局["42"]は暗黙的に"42"に変換されるため、+"42"が評価されて42になります。8 9)

そのように書き直したのが、以下のコードになります。10

function convertNumber(n) {
  const str = [...n.toString(10)]
    .map(c => Number.parseInt(c, 10))
    .map(d => `[${"++[".repeat(d)}+[]${"][+[]]".repeat(d)}]`)
    .join("+");
  return `+[${str}]`;
}

eval(convertNumber(42)) などと実行してみれば、正しく42が返ることがわかります。
convertNumber(Number.MAX_SAFE_INTEGER)のように大きな数を与えても問題なく実行できますね。

ただし、ここでは単項演算子+で数値型に再変換していますが、配列の添え字アクセスで使用する場合などは文字列のままでも全く問題ない(a[1]a["1"]は同じ)ため、わざわざ数値に戻す必要がないことも多いです。
今回の場合も、必要とされているのは"42"のような「数値を表す文字列」なので、return `+[${str}]`; のかわりに return str;で文字列として返すだけで十分です。11
以後、この記事ではconvertNumber(n)[4]+[2]のようにして得られる文字数をそのまま返す実装とみなします。

残りの文字を集める

"("")" は適当な関数を文字列キャストすることで取得できます。

[]["find"]+[] // === "function find() { [native code] }"
[[]["find"]+[]][0][13] // === "("
[[]["find"]+[]][0][14] // === ")"

","は、要素が複数ある配列をjoin()するか、文字列に変換することで得ることができます。12

[]["constructor"](2)+[] // Array(2)+"" === ","

これらの記号表現は、

// === "("
[[][[[][[]]+[]][+[]][++[++[++[++[+[]][+[]]][+[]]][+[]]][+[]]]+[[][[]]+[]][+[]][++[++[++[++[++[+[]][+[]]][+[]]][+[]]][+[]]][+[]]]+[[][[]]+[]][+[]][++[+[]][+[]]]+[[][[]]+[]][+[]][++[++[+[]][+[]]][+[]]]]+[]][+[]][++[+[]][+[]]+[++[++[++[+[]][+[]]][+[]]][+[]]]]

// === ")"
[[][[[][[]]+[]][+[]][++[++[++[++[+[]][+[]]][+[]]][+[]]][+[]]]+[[][[]]+[]][+[]][++[++[++[++[++[+[]][+[]]][+[]]][+[]]][+[]]][+[]]]+[[][[]]+[]][+[]][++[+[]][+[]]]+[[][[]]+[]][+[]][++[++[+[]][+[]]][+[]]]]+[]][+[]][++[+[]][+[]]+[++[++[++[++[+[]][+[]]][+[]]][+[]]][+[]]]](+[[++[++[+[]][+[]]][+[]]]])

// === ","
[[][[[][[[[]==[]]+[]][+[]][+[]]+[[+[]==[]]+[+[]]+[][+[]]+[]][+[]][++[+[]][+[]]+[+[]]]+[[+[]==[]]+[][+[]]][+[]][++[+[]][+[]]+[+[]]]+[[][+[]]+[]][+[]][++[++[+[]][+[]]][+[]]]]+[]][+[]][++[++[++[+[]][+[]]][+[]]][+[]]]+[[+[]==[]]+[][[[[]==[]]+[]][+[]][+[]]+[[+[]==[]]+[+[]]+[][+[]]+[]][+[]][++[+[]][+[]]+[+[]]]+[[+[]==[]]+[][+[]]][+[]][++[+[]][+[]]+[+[]]]+[[][+[]]+[]][+[]][++[++[+[]][+[]]][+[]]]]][+[]][++[+[]][+[]]+[+[]]]+[[+[]==[]]+[][+[]]][+[]][++[+[]][+[]]+[+[]]]+[[[]==[]]+[]][+[]][++[++[++[+[]][+[]]][+[]]][+[]]]+[[+[]==[]]+[]][+[]][+[]]+[[+[]==[]]+[]][+[]][++[+[]][+[]]]+[[+[]==[]]+[]][+[]][++[++[+[]][+[]]][+[]]]+[[][[[[]==[]]+[]][+[]][+[]]+[[+[]==[]]+[+[]]+[][+[]]+[]][+[]][++[+[]][+[]]+[+[]]]+[[+[]==[]]+[][+[]]][+[]][++[+[]][+[]]+[+[]]]+[[][+[]]+[]][+[]][++[++[+[]][+[]]][+[]]]]+[]][+[]][++[++[++[+[]][+[]]][+[]]][+[]]]+[[+[]==[]]+[]][+[]][+[]]+[[+[]==[]]+[][[[[]==[]]+[]][+[]][+[]]+[[+[]==[]]+[+[]]+[][+[]]+[]][+[]][++[+[]][+[]]+[+[]]]+[[+[]==[]]+[][+[]]][+[]][++[+[]][+[]]+[+[]]]+[[][+[]]+[]][+[]][++[++[+[]][+[]]][+[]]]]][+[]][++[+[]][+[]]+[+[]]]+[[+[]==[]]+[]][+[]][++[+[]][+[]]]](++[++[+[]][+[]]][+[]])+[]][+[]]

となります。

完成

さて、これで

eval(eval("String.fromCodePoint(49, 43, 49)"));

を実行するために必要なものが揃いましたね。

あとは、入力プログラムを受け取って、コードポイントの列に変換して、さらに記号に変換することができれば完成です。

/**
 * @returns {string}
 */
function convertNumber(n) {
  // ここでは上の例とは異なり、数値に再キャストせずに文字列のまま返している
  return [...n.toString(10)]
    .map(c => Number.parseInt(c, 10))
    .map(d => `[${"++[".repeat(d)}+[]${"][+[]]".repeat(d)}]`)
    .join("+");
}
// 実際にはすべて記号だけで書かれた表現を入れる
const _eval = "eval"; // ここだけevalするとeval自身になる、他は文字列となることに注意
const _String_dot_fromCodePoint = '"String.fromCodePoint"';
const _comma = '","';
const _lparen = '"("';
const _rparen = '")"';
function convertProgram(src){
  const codes = [...src]
    .map(c => c.codePointAt())
    .map(convertNumber)
    .join(`+${_comma}+`);
  return `${_eval}(${_eval}(${_String_dot_fromCodePoint}+${_lparen}+${codes}+${_rparen}))`;
}

ここではコード長の制約から、本来記号で表現するところを元の表現で書いています。
実際に[]+=()の6種類の記号のみからなる記号プログラムを出力するコードは、
https://gist.github.com/Tatamo/5762f14941889675c4932972ed17deb8#file-convertprogram-js
に掲載しています。

convertProgram('console.log("Hello, World!");');

console.log("Hello, World!");というプログラムを記号のみで表したものが得られます。
(https://gist.github.com/Tatamo/5762f14941889675c4932972ed17deb8#file-helloworld-js )
得られた出力をコピーしてJavaScript処理系に投げるか、または

eval(convertProgram('console.log("Hello, World!");'));

とすれば、コンソールにHello, World!と表示されます。

結果

出力されたHello, World!プログラムの長さを数えてみたところ、60,831文字でした。
前回の記事冒頭に記載したものは14,178文字でしたから、大幅に悪化している気がしますね?

今回作成した変換器で出力されるプログラムは、

eval(eval("String.fromCodePoint(1,2,3,...)"));

という形のコードを記号だけで書いたものでした。
このコードのそれぞれの部分の文字数を見てみると、

文字数
eval 2,077
"String.fromCodePoint" 22,564
"(" 224
"," 1,123
")" 233
数字 1桁あたり3~84文字程度

のようになっています。
数字部分は、先述したように一桁ずつ生成してから文字列として結合しており、一桁ごとの文字数は、その数字を $d$ とすると $3+9d$ 文字となります。
ASCII文字に含まれる文字ならコードポイント値は128未満で収まりますから、ほとんどの場合で一文字ごとの文字数は多くても200文字と考えることができそうです。

"String.fromCodePoint"部分にかなりの文字数を要していますが、全体で見れば6万文字のうちの4割未満に収まっています。
どうやら今回のコードでは、","の文字がプログラム長の肥大化を招いているようです。
文字を一文字表現するたびに","で繋いでいくことになりますから、変換元のプログラムの長さが長くなるにつれて","を表す部分の割合がどんどん高くなっていくことが予想されます。
変換元のプログラムの長さが1文字増えるごとに、出力されるプログラムの長さが1,000文字以上増えることになりますから、これでは数値を短く表現できるようにした意味がありませんね。

まとめ・次回予告13

今回の記事では、実際に記号のみでJavaScriptプログラムを記述する過程を見ていくとともに、プログラムを記号化するプログラムを(記号プログラムではないJavaScript上で)実装していきました。
扱うプログラムが複雑になるにしたがって、記号プログラムを人力で記述していくのは困難になってくるため、「記号プログラムを文字列として出力するプログラム」を用意しておくことは重要になります。

とはいえ今回は第1回で示した概念の実証という意味合いが強く、結果として得られた変換器は出力するプログラムの大きさという点で大いに改善の余地があります。
そこで次回は、文字数短縮をはじめとした記号プログラミング(および記号プログラムを出力するプログラムのプログラミング)におけるテクニックの紹介などを書いてみたいと思っています。
また、そろそろ「記号プログラム」「文字列化したJavaScriptプログラム」「文字列化した記号プログラム」あたりの高階的な概念が頻出し始め、ごちゃごちゃになってきて混乱を招くので、いい感じに表現できるモデルが欲しい気もします。

いよいよ皆さんも記号化されたJavaScriptプログラムが書けるようになってきたことと思いますので、次回はより黒魔術と呼べるような闇の深いコードを提示できるように進捗を生んでいきたいと考えています。

おまけ

前回、記号プログラムコンパイラを記号プログラムで書くとか言っていたような気がしますね。
先程作った https://gist.github.com/Tatamo/5762f14941889675c4932972ed17deb8#file-convertprogram-js の全体をクロージャで囲って、

(function(){
  // ここから記号プログラム変換器
  const maping = ...
  ...
  function convertProgram(src){ ... }
  // ここまで

  return convertProgram;
})()

こういう感じのプログラムを作って、

const program = "(function(){ const mapping = ...; ...; return convertProgram;})()"

文字列にしてから14自分自身に食べさせて、

convertProgram(program); // 記号で書かれた記号プログラム変換器

できましたね。

敢えてソースコードを示すことはしませんが、やってみたところ6MB程度(6,000,000文字オーバー)の記号プログラムが生成されました。

eval(eval(convertProgram(program))('console.log("Hello, World!");'));

を実行してみたところ、結果が返るまでに数秒かかりましたが正しくHello, World!とコンソールに表示されました。
というわけで、使えたものではありませんが「記号プログラムで書かれた記号プログラムコンパイラ」が作成できました。めでたし。

  1. https://github.com/tc39/proposal-global, 👻globalThis👻と🌏global🌏と🌝this🌝

  2. 他に、eval(String.fromCodePoint(49)+String.fromCodePoint(43)+String.fromCodePoint(49));という形式で書くことも考えられます。とはいえこれは出力コードの長さがとんでもないことになりそうなので、今回は採用しないことにします。

  3. 前回の記事でも指摘した点ですが、この実装の動作はStringオブジェクトに存在する他のメソッドの名前に依存するため、将来に渡って正しく動作する保証はありません。今回は簡単のためこのようなコードにしていますが、この時点で手に入っている文字のみで確実に"C"を得られる方法として、 Object.getOwnPropertyNames(String).join("").split("").filter(eval("Function.bind(undefined,(undefined+[])[0])")("return [true][u.codePointAt()-67]"))[0]を提示しておきます。

  4. スタックに割り当てられている領域のサイズは処理系によって異なります。node.js(V8)のデフォルトは984KBで、本記事ではこれを基準としています。

  5. https://kangax.github.io/compat-table/es6/#test-proper_tail_calls_(tail_call_optimisation)

  6. とはいっても800前後でスタックオーバーフローになるのは結構意外でした。配列の添字アクセスって普通の関数呼び出しの数倍以上もコールスタックを消費するんでしょうか?

  7. 配列aをhint値"default"または"string"でプリミティブに変換すると、a.toString()が実行され、a.join()の結果が返ります。 https://www.ecma-international.org/ecma-262/#sec-array.prototype.tostring 2

  8. べつに真面目に読まなくていいです

  9. JavaScriptのプリミティブへの変換を完全に理解する

  10. 蛇足ですが、変換する数字のうち最も上の桁だけは配列で囲わずに1+[2]+[3]+... とすることができます。最初の+演算子の右辺は文字列となるため問題なく"123..."になり、これで2文字節約できます。ほかの桁で[]を外すと、+が複数個連続してしまうためにエラーとなります。 2

  11. むしろ、前者では先頭に単項演算子の+が存在するため、返り値に対して前から+二項演算子を適用しようとするとa++xのような形となってしまい、10と同様にエラーが発生するおそれがあるため注意が必要です。このような場合、文字数は増えてしまいますがa+[+x][0]a+(+x)のような形で囲うことで、意図しない+の連続を防ぐことができます。

  12. 7と同様にArray.prototype.join()が呼び出されますが、Array(2)よりlengthは2となっており、また0番目と1番目の要素はundefinedとして扱われますが、undefinednulljoin()の処理では空文字列に置換されるため、結果として","が返ります https://www.ecma-international.org/ecma-262/#sec-array.prototype.join

  13. 2本目を書くのに3ヶ月も空けたのにまだ次回作を書く気があるんですか???(大幅に間が空いてしまい申し訳ないです、第3回も気長にお待ちください)

  14. 書いたクロージャに名前をつけるなどしておき、文字列に型変換すれば文字列化された実装が得られます

21
11
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
21
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?