JavaScript
ECMAScript
HelloWorld
中級者

JavaScript中級者向けHelloWorld

この記事は、JavaScript2 Advent Calendarの9日目の記事です。「javascript に関することなら初心者〜玄人まで歓迎」と書いてあったので、間を取って中級者向けの記事を書きます。(そもそも玄人向けの記事は私には書けませんが…)

内容は、HelloWorldです。中級者の方は、もちろん分かりますよね。
初心者の方はヒントを見ながら(ちょっと厳しいかもしれませんが)、上級者の方は鼻で笑いながら、最後まで読んで頂けると幸いです。


このコードの振る舞いを完全に説明できれば、あなたはJavaScript中級者です。

コード

((_, ...args) => Function.prototype.call.call(...args))`${
   function() { console.log(this.valueOf()) }
}${
   String.raw({
      raw: {
         length: {
            [Symbol.toPrimitive]: ({length}) => length / 2
         },
         ...[...[, {
            toString: Array.prototype.join.bind`Hello${[][[]]} world!`
         }, , ]].map(value => value || '')
      }
   }, ...'""')
}`

出力

"Hello, world"

ヒント

きっちり並べてあるわけではありませんが、下にいくほどレベルが高くなります。

解説

※長いので、読まなくても分かる方は一気に下まで飛ばしてください。

まず、大まかな構成を見てみましょう。

((_, ...args) => Function.prototype.call.call(...args))`${A}${B}`

テンプレートリテラルによる関数の適用を普通の関数呼び出しに書き換えます。

((_, ...args) => Function.prototype.call.call(...args))(['', '', ''], A, B)

argsには[A, B]が格納されるので、これは次のように解釈されます。

Function.prototype.call.call(A, B)

Aの部分は関数(Functionオブジェクト)なので、さらに簡単に書くことができます。

A.call(B)

これはつまり、関数Aを、Bthisとして実行しているということです。

ここで、Bの部分を見てみると、

String.raw(...)

となっているので、Bは文字列だと分かります。

さて、Aの関数の定義を見てみましょう。

function() { console.log(this.valueOf()) }

Bの型はstringですが、FUnction.prototype.callthisに割り当てる値をobject型に変換するので、Bの値がボックス化されてStringオブジェクトとしてthisに格納されます。そこで、文字列として出力するためにString.prototype.valueOfメソッドを呼び出しています。

つまり、Aは文字列Bをそのままコンソールに出力する関数だということです。このことから、コード全体は以下のように簡略化できます。

console.log(B)

次に、Bの部分に着目します。

String.raw({raw: C}, ...'""')

...'""'の部分は、文字列を1文字ずつに分解しているだけです。

String.raw({raw: C}, '"', '"')

String.raw関数は1番目の引数に、rawプロパティが配列様であるオブジェクトをとります。したがって、オブジェクトCは配列様である必要があります。


Cの定義は

{
   length: {
      [Symbol.toPrimitive]: ({length}) => length / 2
   },
   ...D
}

String.raw関数がClengthプロパティを所得した時、まずオブジェクトから数値への変換を試みるため、length[Symbol.toPrimitive]メソッドが呼び出されます。この時、number型の値が期待されるため、引数に'number'が渡されます。

よって、Cの要素数は次の計算を行った結果となります。

(({length}) => length / 2)('number')

'number'.lengthは6なので、要素数は3ということになります。

つまり、Cは以下と等価になります。

{
   length: 3,
   ...D
}

続いて、Dを見てみましょう。

[...[, {
   toString: Array.prototype.join.bind`Hello${[][[]]} world!`
}, , ]].map(value => value || '')

今度は内側から考えていきます。

{toString: Array.prototype.join.bind`Hello${[][[]]}` world!`}

まず、[][[]]は暗黙の型変換により[]['']、すなわちundefinedに変わります。したがって、(undefinedは引数を省略するのと同じことになるので)次のように書き換えられます。

{toString: Array.prototype.join.bind(['Hello', ' world!'])}

これは

{toString: () => 'Hello, world!'}

とするのと同じことですね。

1つ外側を見てみます。

[, {toString: () => 'Hello, world!'}, , ]

これは、要素数3(配列リテラルの最後のコンマは無視されることに注意)の配列で、要素が省略された所には値がありません。このままではArray.prototype.mapメソッドを実行しても、省略された値については無視されます。

そこで、一度配列を展開するという方法をとっています。

[...[, {toString: () => 'Hello, world!'}, , ]]

こうすることで、省略されていた部分にはundefined値が設定されます。

[undefined, {toString: () => 'Hello, world!'}, undefined]

さらに外側まで見てみると、

[undefined, {toString: () => 'Hello, world!'}, undefined]
   .map(value => value || '')

ここで、Array.prototype.mapメソッドでは、要素の値がtruthyな場合にはその値を返し、それ以外の時は空文字列を返しています。

したがって、Dは最終的に以下のようになります。

['', {toString: () => 'Hello, world!'}, '']

ここからさらに、Cに戻って考えます。

{
   length: 3,
   ...['', {toString: () => 'Hello, world!'}, '']
}

オブジェクト内で配列を展開しているので、これは次のように展開されます。

{
   length: 3,
   0: '',
   1: {toString: () => 'Hello, world!'},
   2: ''
}

ここで、配列様オブジェクトCの各要素は文字列であると期待されるため、オブジェクトはtoStringメソッドによって文字列に変換されます。

したがって、配列様オブジェクトCは、配列

['', 'Hello, world!', '']

と同じように振る舞うことが分かります。


さて、ここでようやくBに戻ることができます。

String.raw({raw: ['', 'Hello, world!', '']}, '"', '"')

このString.raw関数の適用は、逆にテンプレートリテラルを使って書いた方が理解しやすくなります。

String.raw`${'"'}Hello, world!${'"'}`

そしてこの関数は文字列

'"Hello, world!"'

を返します。


したがって、最終的にコード全体をまとめると

console.log('"Hello, world!"')

ということになります。

というわけで、コンソールへの出力は

"Hello, world!"

となります。

まとめ

前日の夜に勢いで書いてしまったので、拙い記事になってしまいました。

ただ、コードについては、結構な自信作です。JavaScriptの面白さが詰まっていると思います。私自身、まだ中級者と呼べるかどうか、といったレベルですが、この記事を書くことで、より理解が深まったような気がします。

皆さんご存知なかったかもしれませんが、HelloWorldって、中級者のためのものだったんですよ。
というわけで、皆さんも是非、一味違ったHelloWorldを書いてみてください。


明日(12/10)は@dashimakiさんの記事です。