まえがき
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.
しかし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では0
はfalse
、それ以外は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
ではないため、prototype
にforEach()
メソッドが継承されていないのです。
そのためArrayLikeObject
でforEach()
を使いたい場合、まず純粋な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
という仕様を利用した、global
なwindow
オブジェクトを取得するためのテクニックです。
間接的に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にはここで紹介している以外にもたくさんの黒魔術が存在しているかと思います。
みなさんはこれまでどのような魔術に遭遇してきましたでしょうか?
一見パルプンテに見えるコード達も、術式を丁寧に読み解いてみると案外面白いのかもしれません。