JavaScriptのコードを書いていて、ふと変態的コードに慣れ切った自分に気づいてしまったので、 自戒の意を込めてJavaScriptでよく使われるおかしなトリックについてまとめておきます。
他言語からきた人が首を傾げるであろうアレらです。
文法編
(function(){...})()
みんな大好き即時関数。
英語だとIIFE(Immediately-Invoked Function Expression)と言います。
英語の意味を考えればわかりますが「即座に実行される関数式」のことです。
function(){...}
の部分が関数式で、最後の()
で関数を実行しています。
function(){...}
を囲っている括弧は関数宣言とみなされないためのおまけで、書く場所によっては要りません
*1。
即時関数はスコープを分けるために使います。
JavaScriptにはグローバルスコープと関数スコープしかないため、
一時的にしか使用しない変数の寿命を延ばさないためには関数の内部に書くほかありません。
要はJavaやC#におけるブロック{}
の代わりです。
var x; // グローバルスコープ (function(){ var x; // 関数スコープ })();
グローバルスコープからの隔離の他に、 よくクラスなどの複雑なオブジェクトの定義に使われます。
var Foo = (function(){ function Foo(){} Foo.prototype.bar = function() { baz(); }; function baz() {...} // プライベートメソッド return Foo; })();
グローバルオブジェクトやundefined
を取得する派生形もあります。
(function(global, undefined){ ... })(this);
!!x
感嘆符(ビックリマーク、エクスクラメーションマーク)を二つ並べた形の式。
感嘆符!
ひとつで論理否定の意味なので、!!x
はxの二重論理否定です。
二重否定したら元に戻って意味がないかのように見えますが、
論理否定演算子には任意の型をBoolean型に変換するという副作用があるため、
評価値は必ずBoolean型になります。
要はBoolean型への型変換です。
~~x
チルダを二つ並べた同じような形の式。
チルダ~
ひとつでビット否定の意味なので、~~x
はxの二重ビット否定です。
ビット否定演算子には任意の型を32bit整数Number型へと変換する副作用があるため、
評価値は必ず32bit整数Number型になります。
要は32bit整数Number型への型変換です。
x | 0
縦線(パイプ)に0がついた形の式。
先ほどのチルダがビット否定で、この縦線|
はビット論理和なので、
だいたい予想はつくと思いますが、別バージョンの32bit整数Number型への型変換です。
0はどんな数字と論理和をとっても効果がないため、副作用である型変換のみが行われます。
x || 0
縦線(パイプ)二つに0がついた形の式(0は別の値の場合もあります)。
ひとつ前と同じような形ですが、実は意味は全く異なります。
縦線二つ||
なのでこれは論理和を表しますが、
この演算子の評価値は勝手にBoolean型に変換されたりしません。
論理和演算子は左辺値をBoolean型として評価して、
true
なら左辺値を、false
なら右辺値を評価値とします。
つまり、左辺値か右辺値のどちらかが(型変換なしに)そのまま返ってきます。
この定義だと、仮に両辺の値がBoolean型だった場合に論理和に一致する値が返ります、うまいもんですね。
この式は左辺値がundefined
だった場合に右辺値が返ることを利用して、
よくオプション引数のデフォルト値に使用されます。
function foo(x) { x = x || 0; ... }
右辺値は好きな値にできますが、 xが空文字列や0だった場合にも右辺値が使われてしまうことに注意が必要です。
x && x.y
アンド(アンパサンド)二つを挟んでプロパティを参照する形の式。
先ほどの形の論理積版です。
アンド二つ&&
なので論理積を表しますが、
評価値が勝手にBoolean型に変換されることはありません。
論理積演算子は左辺値をBoolean型として評価して、
true
なら右辺値を、false
なら左辺値を評価値とします。
論理和と同様に、両辺の値がBoolean型だった場合に論理積に一致する値が返るように定義されています。
この式は左辺値がnull
やundefined
だった場合に右辺値が評価されないことを利用して、
よくnullチェックに使用されます。
var x = foo && foo.bar();
ただし、左辺値がfalseと評価された場合には左辺値がそのまま返るということをお忘れなく(特に空文字列)。
x >> 0
大なり(不等号)二つに0がついた形の式。
大なり二つ>>
が右ビットシフト演算子であることを考えれば、
この0ビットシフトが32bit整数Number型への変換であることはもうわかると思います。
無意味なビット演算系はだいたいこれですね。
x >>> 0
大なり(不等号)三つに0がついた形の式。
大なり三つ>>>
が右ビットシフト演算子であることを考えれb(ry
・・・と言いたいところですが、実は若干違います。
ポイントは>>>
が”論理”右ビットシフト演算子であること(>>
は”算術”右ビットシフト演算子)。
論理シフト演算は符号ビットの存在を無視する演算であることから、
この時の変換は32bit”非負”整数Number型への変換になります。
そもそもNumber型に32bitも非負もあるのかとツッコまれそうですが、 32bit非負整数として計算した結果がNumber型として返ってくるという意味です。 Number型は符号ありの浮動小数点数ですが、 64bitもあるので高々32~33bitくらいの整数は誤差なく余裕で表現できます。
-1 >> 0; // -1 -1 >>> 0; // 4294967295 (UInt32.Max)
+x
数字でもない値にプラス記号がついた形の式。
単項演算子のプラス記号+
は符号をそのままに数値を評価します。
単項マイナス演算子と対をなすためだけにあるような演算子ですが、
副作用として任意の型をNumber型に変換する効果があるため、
Number型への型変換に使われます。
ちなみに!!x
的なアイデアで--x
としたり、
意味もなく++x
と二つ並べるとインクリメント演算子やデクリメント演算子になってしまいます。
演算子って難しい・・・。
"" + x
空文字列に値を足した形の式。
文字列に対して何かを加算するとそれは文字列の連結とみなされ、 文字列でない値はString型へと変換されます。 ここでは空文字列と連結しているため、 評価値は単純にxをString型へと変換した値となります。
ちなみにこの式のプラス記号をマイナス記号に置き換えると、 今度は文字列の方が数値に変換されるというのはJavaScriptの気持ち悪い挙動として有名です。
void 0
キーワードvoid
に0をつけた形の式(void(0)
と書いても同じです)。
他言語を書いているとvoid
は型のような気がしてしまいますが、JavaScriptのvoid
は演算子です。
void
演算子は任意の値をundefined
として評価します。
つまり、void 0
はundefined
の取得を表します(短いので慣例的に0が使われますがそれ以外の値でもかまいません)。
ちなみに、何故わざわざこんな演算子を使ってundefined
を取得するかというと、
JavaScriptでundefined
と書いた場合、
これはキーワードではなく単なる変数であり、書き換えられている可能性があったためです。
まあ、現在の仕様では書き換え不可能となっているそうなので、結局は慣習ですね
*2。
typeof x === "undefined"
typeof
の結果と"undefined"
という文字列を比較する形の式。
これについては意味はすぐわかると思います。
xの型がundefined
であるかどうかを判定しています。
ただ、自分が始めたばかりの頃、たかが型判定に文字列比較を使うことに衝撃を受けたので入れてみました。
undefined
の判定方法にもいろいろありますが、
グローバル変数としてxが存在するかどうかを判定する場合には多分これが一番きれいな形です。
変数宣言なしに変数を参照する式を書くとReferenceError
が発生してしまうので。
API編
[].concat(x)
空配列のconcat
を別の配列に適用している形の式。
concat
関数の、配列を結合して新しい配列を返すという仕様を利用した、配列の複製処理です。
x.concat()
やx.slice()
といった別形もあります。
ES6からArray.from
が導入されるのでいらなくなる予定。
余談ですが、concat
はなかなか柔軟に配列を結合してくれるため、次のようにflatMap
の実装にも使えるそうです。
Array.prototype.flatMap = function(lambda) { return Array.prototype.concat.apply([], this.map(lambda)); };
Array.prototype.slice.call(arguments)
配列のslice
をarguments
に対して適用している形の式。
JavaScriptにおいてarguments
変数は配列のようでありながら実は配列ではないという奇妙なオブジェクトです。
slice
関数は配列ライクなオブジェクトに対して実行可能で、配列を結果として返すため、
arguments
を配列へと変換するのに使用できます。
また、slice
関数の仕様上、さらに引数を加えると、任意の範囲を配列へと変換できます。
Array.from
が配列ライクなオブジェクトにも適用できるため、これもES6からいなくなる予定です。
Object.prototype.toString.call(x)
Object.prototype
のtoString
を変数に適用している形の式。
Object.prototype
のtoString
というのは、toString
のデフォルト実装にあたります。
そして、toString
のデフォルト実装で型名が返ってくるというのはまあ自然だと思います。
JavaScriptではあろうことかこれを利用して型判定をすることがままあります。
Object.prototype.toString.call([]) === "[object Array]"; // true
気持ち悪いですね。
ES5からはnull
やundefined
も対応した値を返すようになりました。
null.toString()
やundefined.toString()
と書けないにも関わらず、内部的には対応しているという謎。
Object.create(null)
create
関数にnull
を与えて呼び出している形の式。
一見すると、空のオブジェクトリテラル{}
と同じように思えますが、
こちらに対応するのはObject.create(Object.prototype)
です。
プロトタイプオブジェクトにnull
を指定すると、オブジェクトを純粋な連想配列として扱えます。
要はtoString
やhasOwnProperty
なんかが邪魔な場合に使用します。
オブジェクトかどうかを判別する式のひとつであるx instanceof Object
がfalse
を返すので扱いには注意。
(0, eval)("this")
0
とeval
とthis
な形の式。
有名な黒魔術。
冗談のようですが実際なかなか便利です。
eval
関数は直接呼び出すとローカルスコープで実行されますが、
式の評価値などで間接的に呼び出すとグローバルスコープで実行されるという仕様になっています。
これを利用してグローバルスコープでthis
を評価することにより、
グローバルオブジェクトを取得するという荒業。
eval
を式の評価値とするためにコンマ(カンマ)演算子を使っています。
これは左辺値と右辺値を順に評価し、右辺値を評価値とするという演算子です。
結構いろんな言語にありますが、どの言語でも影薄いんですよね、コレ。
ちなみに0は短いのでよく使われるだけで、別の値でもかまいません。
おわりに
とりあえず一通り書いてみましたが、やはり型関係が多いですね。 JavaScriptの型は本当にややこしい・・・。 最近、この型地獄から抜け出したくてTypeScriptを学んでいるのですが、期待したほど状況は変わりませんね。 というか、むしろさらなる深みに嵌っていってるような感覚すらある。
JavaScript歴はわりと浅いので、漏れとか間違いとかあるかもしれません。 何か気づいたことがあれば、Web上のどこかに書いておいてください。