黒魔術(JavaScript)まとめ

  • 1084
    いいね
  • 4
    コメント

まえがき

JavaScript、書いてますか?
JavaScriptは今や世界中の人々に愛されています。
stackoverflowの2016年の調査によるとJavaScriptは地球上で最も一般的に使用されているプログラミング言語だそうです。

JavaScript is the most commonly used programming language on earth. Even Back-End developers are more likely to use it than any other language.

link

しかしJavaScriptは愛されすぎているが故、しばしば黒魔術のようだと比喩されることも少なくありません。
愛と憎しみが紙一重とはこのことですね。

ということでそんなこんなはどうでもいいのですが、自分もJavaScriptは大好きです。
今回は黒魔術まとめということで、今までに見かけた奇々怪々なJavaScriptコード達をまとめてみました。

※そのコードから感じられる闇レベルを独断と偏見で定量化してあります。

入門編(闇レベル:★☆☆)

indexOf及び~演算子

var foods = ['apple', 'banana', 'orange'];

if (~foods.indexOf('apple')) {
    // appleが含まれていた時の処理
}

まずは入門編、第一章第一節。
結構よく見かける表現なのではないかと思います。使ったことのある方も多いのではないでしょうか。
リーダブルコード的にはあれだけど、書きやすいし知ってる人同士の間ではとってもリーダブル?な一品。

これは何をしているのかと言うと、foodsという配列の中にappleという要素が含まれているか否かを判定しています。
.indexOf()メソッドは配列内に検索対象が見つかればそのインデックスを、見つからなければ-1を返すため、下記のように判定されることが多いです。

if (foods.indexOf('apple') !== -1) {
    // appleが含まれていた時の処理
}

// 又は
if (foods.indexOf('apple') > -1) {
    // appleが含まれていた時の処理
}

まぁこれでも全く問題ありませんが、なんとなく冗長な感じですね。そこでチルダ演算子の出番です。
チルダ演算子はビット反転演算子と呼ばれます。整数に対してビット反転を適用すると符号を反転させて1を引いた数になります。

よって次のような演算結果を得ることが出来ます。

console.log(~-2); // 1
console.log(~-1); // 0
console.log(~0);  // -1
console.log(~1);  // -2
console.log(~2);  // -3

ポイントは-1をビット反転させた場合です。-1の時だけ0になっているのがわかると思います。
JavaScriptでは0false、それ以外はtrueなので結果的にこのロジックで判定が可能というわけです。


※追記(2016-10-11)

@think49 さんよりindexOf()の挙動に関するコメントを頂きました。

indexOf()による配列内検索ですが、NaN値の存在を確認したい場合、意図した挙動にならないため注意が必要です。

[NaN].indexOf(NaN); // -1

NaN値が配列内に含まれているかの確認を行いたい場合はArray.prototype.includes()を用いると意図した挙動となります。

[NaN].includes(NaN); // true

Function Declarations vs Function Expressions

次のコードの内シンタックスエラーになる書き方があります。

// pt1
void function() {
    return 'ok';
}();

// pt2
function() {
    return 'ok';
}();

// pt3
[function() {
    return 'ok';
}()]

// pt4
var hoge = function() {
    return 'ok';
}();

正解はpt2です。デバッグコンソールに貼り付けて実行してみるとUncaught SyntaxError: Unexpected token (と言われると思います。
pt2はその他のパターンと一見似て見えますが、決定的に違う点が一つだけあります。
それはFunction Declarations(関数宣言)であるか、Function Expressions(関数式)であるかです。

JavaScriptは関数を定義する際、ソースコードがfunctionから始まる場合にのみFunction Declarationsとなり、それ以外は全てFunction Expressionsとなります。
Function Declarations}の後に強制的に;が挿入されるため、pt2はJavaScriptエンジンには下記のように解釈されます。

// pt2
function() {
    return 'ok';
};
(); // ← SyntaxError

その為、最終行の();の部分でSyntaxErrorが起こるというわけです。
このようなコードは即時実行関数と呼ばれ、グローバルスコープ汚染対策として一般的に利用されています。
初めて即時実行関数を見た際、なぜ最初のfunction(){}は括弧で囲う必要があるのか?と思ったものですが、
下記コードの前者がNGで後者がOKな理由は、であるか、宣言であるか、の違いによるものだということがわかります。

// NG (SyntaxError)
function() {
    return 'ok';
}();

// OK
(function() {
    return 'ok';
})();

中級編(闇レベル:★★☆)

Array.prototype.slice.call()

Array.prototype.slice.call(document.querySelectorAll('div')).forEach(function(div) {
    console.log(div);
});

なかなかにATフィールドを感じさせるコードですが、これも分かってしまえば便利なヤツです。
通常、ArrayObjectの要素を一つずつ回したい場合ネイティブに実装されているforEach()メソッドを用います。

[1, 2, 3, 4, 5].forEach(function(item) {
    // 処理
});

なんの問題も無いですね。ArrayObjectはをこのforEach()メソッドを用いることで要素に一つずつアクセスすることが出来るのです。
Array.prototype.slice.call().forEach()なんて魔術を唱える必要はありません。

...が、JavaScriptの世界にはforEach()回すことの出来ない配列 が存在しています。
それらは厳密にはArrayObjectではなく、ArrayLikeObjectと呼ばれます。その名の通り 配列のようなオブジェクト です。
ArrayLikeObjectの詳細な解説は割愛しますが、超絶ざっくり説明するとlengthプロパティを持ち、数字添字でアクセス可能な配列のようなオブジェクトがArrayLikeObjectとして扱われます。

通常ArrayObjectは次のような処理を通すと[object Array]という文字列が得られます。

Object.prototype.toString.call([]); // "[object Array]"

しかしArrayLikeObjectの場合、Arrayではないため違う結果を返却されます。

Object.prototype.toString.call(document.querySelectorAll('div')); // "[object NodeList]"

これが配列のように見えていてもforEach()で回すことが出来ない原因です。
ArrayObjectではないため、prototypeforEach()メソッドが継承されていないのです。
そのためArrayLikeObjectforEach()を使いたい場合、まず純粋なArrayObjectに変換してやる必要があります。
そのための変換処理がArray.prototype.slice.call()ということになります。

多少記述が長いと感じる場合、次のように記述することも出来ます。

[].slice.call(arrayLikeObject).forEach(function(item) {
    // 処理
});

※参考: なぜ NodeList は Array ではないのか

他にも、document.getElementsByTagName()が返す、HTMLCollectionや、関数内で参照すると引数を取得出来るarguments等もArrayLikeObjectです。

上級編(闇レベル:★★★)

(0, eval)('this')

これぞJavaScriptといったところでしょうか。もはや意味がわかりません。
どうやら、かの有名なJavaScriptライブラリknockout.jsにて利用されている魔術のようです。

詳細は こちらの記事 にて解説されています。

ざっくりな概要ですが、これはJavaScriptのIndirect eval callという仕様を利用した、globalwindowオブジェクトを取得するためのテクニックです。
間接的にevalを実行することで、引数のコードを実行する際のスコープが必ずグローバルになることを利用しています。
実行してみるとわかりますが、次のようなコードにてしっかりとwindowオブジェクトを取得出来ています。

new function() {
    var a = eval('this'); // 直接eval
    var b = (0, eval)('this'); // 間接eval

    console.log(a, b); // Object {}, Window {...}
}

ただしこの仕様、ES5からのようで、ES3環境では下記のようにFunction()を利用する方法のほうがベターなようです。

new function() {
    var a = Function('return this')();

    console.log(a); // Window {...}
}

何れにせよ、ここまで来るとなかなか利用する機会はなさそうですね。
安易に利用するとプロジェクトメンバーから黒魔術師認定されてしまうかもしれません。

黒魔術(闇レベル:★★★++)

++[[]][+[]]+[+[]]

文字化けではありません、まずは実行してみましょう。

1 0 。

この黒魔術、ご存じの方もいるかもしれません。
元ネタはstackoverflowのJavaScriptカテゴリの質問です。
一見ネタにしか見えませんが、実はこの演算、細かく読み解いていくとJavaScriptの理解すべき型変換挙動のエッセンスが詰まっているのではないかと考えています。

この式ですが、まず最初に次のように2つのブロックに分解できます。

++[[]][+[]]
+
[+[]]

次に分解された内の後半部分、[+[]][0]となります。
配列は演算対象になった場合、配列自身が持つ.toString()が実行されその後演算処理に渡されるという処理フローを辿ります。
よって+[]はまず[].toString()により""に変換され、最終的に+""となり、結果0となります。

上記演算によって下記の状態になります。

++[[]][0]
+
[0]

次に++[[]][0]ですが、ここがなかなかに難解です。
[[]][0][[]]という配列の0番目を参照している式です。よって[[]]0番目、すなわち先頭要素の[]が返却されます。
よって++[[]][0]++[]という演算結果となります。
配列をインクリメント(++)すると、配列が数値化(0)された後に+1されるのでnumber型の1が返却されます。
よって++[[]][0]1となり、下記の状態になります。

1
+
[0]

しかし、上記演算の中の++[]のみを実行するとUncaught ReferenceError: Invalid left-hand side expression in prefix operationというエラーが発生します。

なぜ、++[[]][0]はOKで、++[]はNGなのでしょうか?
もしかしてバグ?と感じるかもしれませんが、これはJavaScriptのprefix operation(++, --, 等)の仕様です。

prefix operationは内部的にとあるAPIの呼び出しを行いますが、そのAPIは参照を必要とします。
[]という式は参照を生成しないため++[]はエラーとなります。
JavaScriptが参照を生成する条件は、式(Expression)が次の状態の場合です。

  • 変数を参照しているか
  • オブジェクトのプロパティアクセスであるか(例: [[]][0]

その証拠に下記のコードは正しく動作します。
aは変数の参照なので、参照が生成されます。

var a = [];
++a; // 1

一見動きそうな下記の式ですが、1という数字は参照を生成しないため同様のエラーが発生します。

++1; // Uncaught ReferenceError: Invalid left-hand side expression in prefix operation

多少脱線しましたが、これまでの演算結果により下記の状態になりました。

1
+
[0]

[0]は配列なので演算前に.toString()が呼ばれ"0"となります。
よって最終的に下記の状態になります。

1
+
"0"

JavaScriptでは文字列と数値の演算結果は文字列となるので
1 + "0"すなわち"10"となるわけです。

Q.E.D. 証明終了。 と言いたくなるボリュームですね。

_=$=+[],++_+''+$

これも演算すると"10"になります。
上記スレッドのコメント内で紹介されているネタですが、こちらも見た目にインパクトがあるのでピックアップしてみました。

こちらは特に難しい事はありません。

2つの式がカンマで連結されており、順番に実行されていきます。

_=$=+[]
,
++_+''+$

_=$=+[]_, $, という変数に対して+[]が代入されます。
+[]0なので現時点で_$0となります。

次に++_+''+$ですが、++__をインクリメントするので1になり最終的に変数を展開すると下記のような状態になります。

1 + '' + 0

1 + ''は文字列の"1"となり、"1" + 0で最終的に"10"となります。

"10"という文字列を得るためにここまで情熱をかけられるのは凄いですね。是非お友達になりたいです。

あとがき

JavaScriptにはここで紹介している以外にもたくさんの黒魔術が存在しているかと思います。
みなさんはこれまでどのような魔術に遭遇してきましたでしょうか?

一見パルプンテに見えるコード達も、術式を丁寧に読み解いてみると案外面白いのかもしれません。