読者です 読者をやめる 読者になる 読者になる

エフアンダーバー

個人でのゲーム開発

JavaScriptの謎コードまとめ

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型だった場合に論理積に一致する値が返るように定義されています。

この式は左辺値がnullundefinedだった場合に右辺値が評価されないことを利用して、 よく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 0undefinedの取得を表します(短いので慣例的に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)

配列のsliceargumentsに対して適用している形の式。

JavaScriptにおいてarguments変数は配列のようでありながら実は配列ではないという奇妙なオブジェクトです。 slice関数は配列ライクなオブジェクトに対して実行可能で、配列を結果として返すため、 argumentsを配列へと変換するのに使用できます。 また、slice関数の仕様上、さらに引数を加えると、任意の範囲を配列へと変換できます。

Array.fromが配列ライクなオブジェクトにも適用できるため、これもES6からいなくなる予定です。

Object.prototype.toString.call(x)

Object.prototypetoStringを変数に適用している形の式。

Object.prototypetoStringというのは、toStringのデフォルト実装にあたります。 そして、toStringのデフォルト実装で型名が返ってくるというのはまあ自然だと思います。 JavaScriptではあろうことかこれを利用して型判定をすることがままあります。

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

気持ち悪いですね。

ES5からはnullundefinedも対応した値を返すようになりました。 null.toString()undefined.toString()と書けないにも関わらず、内部的には対応しているという謎。

Object.create(null)

create関数にnullを与えて呼び出している形の式。

一見すると、空のオブジェクトリテラル{}と同じように思えますが、 こちらに対応するのはObject.create(Object.prototype)です。

プロトタイプオブジェクトにnullを指定すると、オブジェクトを純粋な連想配列として扱えます。 要はtoStringhasOwnPropertyなんかが邪魔な場合に使用します。

オブジェクトかどうかを判別する式のひとつであるx instanceof Objectfalseを返すので扱いには注意。

(0, eval)("this")

0evalthisな形の式。

有名な黒魔術。
冗談のようですが実際なかなか便利です。

eval関数は直接呼び出すとローカルスコープで実行されますが、 式の評価値などで間接的に呼び出すとグローバルスコープで実行されるという仕様になっています。 これを利用してグローバルスコープでthisを評価することにより、 グローバルオブジェクトを取得するという荒業。

evalを式の評価値とするためにコンマ(カンマ)演算子を使っています。 これは左辺値と右辺値を順に評価し、右辺値を評価値とするという演算子です。 結構いろんな言語にありますが、どの言語でも影薄いんですよね、コレ。 ちなみに0は短いのでよく使われるだけで、別の値でもかまいません。

おわりに

とりあえず一通り書いてみましたが、やはり型関係が多いですね。 JavaScriptの型は本当にややこしい・・・。 最近、この型地獄から抜け出したくてTypeScriptを学んでいるのですが、期待したほど状況は変わりませんね。 というか、むしろさらなる深みに嵌っていってるような感覚すらある。

JavaScript歴はわりと浅いので、漏れとか間違いとかあるかもしれません。 何か気づいたことがあれば、Web上のどこかに書いておいてください。

*1:とはいえ普通は書きますが・・・

*2:undefinedと書くよりvoid 0と書いた方が短いというのも理由のひとつかも