JavaScriptにおける暗黙の型変換

こんばんは、south37です。 前回までの流れをぶったぎって、今回はJavaScriptの話をしたいと思います。JavaSciptの悪名高い機能の一つ、暗黙の型変換についてです。

暗黙の型変換

JavaScriptは型に関してとても緩く、違う型同士の演算を許容します。その際に暗黙の型変換が行われるのですが、しばしば直感的には理解しにくい変換が行われたり、エラーを突き止めにくくする原因になったりします。

例えば、あんちべさんのブログでは下記の例が書かれてました*1

// あんちべさんのブログから引用

1 + '1' > 11 //なるほど、数値と文字列で計算すると文字列に変換されるのね
3 * '3' > 9 //えっ!

(1+2)+'3' > '33'
1+(2+'3') > '123'
//結合法則とは何だったのか…

1 + true > 2 //やばい…どうしてこうなった… 
true == 1 > true //ふむふむ、どうやら==で確認してみるとtrueと1は同じか。さすがにtrue === 1はfalseだけど
"AA" + 1 > 'AA1'
"AA" + true > 'AAtrue' //'AA1'じゃないのかよ!!

数値演算子(-, *, /, %)は、どれも計算を行う前に引数を数値に変換しようとします。とくに+演算子は振る舞いが複雑で、数値の加算と文字列の結合の両方を実行するよう多重定義されている為、どちらを行うかは引数の型に依存することになります。 演算子の優先順位によってどれがどの型へ変換されるかが変わり、さらに変換の法則も非常に分かりにくい為、あまりよろしくない挙動といえます。

型チェック

って事で、しばしば型チェックが必要な場面がおとずれるかと思います。そういった時に使えそうな方法を紹介していきたいと思います。

typeofを使う

typeofはJavaScriptに標準で用意された演算子で、渡された値の型を返します。

// Numbers
typeof 37 === 'number';
typeof 3.14 === 'number';
typeof Infinity === 'number';
typeof NaN === 'number'; // Despite being "Not-A-Number"

// Strings
typeof "" === 'string';
typeof "bla" === 'string';

// Booleans
typeof true === 'boolean';
typeof false === 'boolean';

// Undefined
typeof undefined === 'undefined';

// Objects
typeof {a:1} === 'object';

// Functions
typeof function(){} === 'function';

とりあえず、これを使えば間違いなく目的の型である事は確認出来ます。例外としては、NaNというのはnumber型でありながらNot a Number(数値ではない)という意味をもっており、どんな数値演算をしてもNaNになってしまいます。ある値が数値である事を確認したい場合は、

if (typeof n === 'number' && !Number.isNaN(n)) {~}

のようにNumber.isNaN関数によるNaNであるかどうかの判定を入れた方が良いと思います*2

undefinedを判定

undefinedであるかどうかの判定は重要です。例えば、関数に引数が渡されなかった時のデフォルトの挙動を指定する為には、引数がundefinedかどうかをきちんと判定する必要があります。方法としては、上で述べたtypeof演算子を使う方法か、もしくはundefinedとの比較を行う方法が考えられます。

typeof x === 'undefined'; // typeof演算子を使う
x === undefined;          // undefinedとの比較

注意点としては、undefinedは一つの変数に過ぎない(undefinedという変数にundefinedという値が入ってる)為、中身を書き換える事が可能です。例えばこのページに詳しく載ってます。よっぽどの事情が無い限りundefinedが書き換えられる事は無いと思いますが、100%の安全を求めるならtypeofを使った方がいいかもです。

オブジェクトラッパーへの型変換

JavaScriptには、オブジェクトでないプリミティブ値が5種類あります。数値、文字列、boolean、null、そしてundefinedです*3。同時に、標準ライブラリでは数値、文字列、booleanをオブジェクトとしてラップする為のコンストラクタが用意されています。例えば、文字列をラップするStringオブジェクトは次のようにして作成出来ます。

var s = new String("hello");

ただし、Stringオブジェクトは実際にはほとんど使われません。では何故用意されているのかといえば、ユーティリティメソッドを提供する為です。 JavaScriptにおいては、プリミティブ型である文字列のプロパティを取得したりメソッドを呼び出したり出来ます。

'abc'.length; // 3
'abc'.toUpperCase() // 'ABC'

何故こんな事が出来るかと言えば、文字列からStringオブジェクトへの暗黙の型変換が行われているからです。その為、Stringオブジェクトが持つプロパティを取得したり、Stringプロトタイプオブジェクトが持つメソッドを呼び出したり出来るのです。

暗黙の型変換のたびに新しいStringオブジェクトが生成される為、プロパティを変更しても永続的な効果はありません。

s = 'hello';
s.length; // 5
s.length = 10;
s.length; // 5

この振る舞いは便利な面もありますが、オブジェクトを渡すべきところでプリミティブ値を渡してもエラーとはならない為、エラーの特定を難しくするというマイナス面もあります。

まとめ

JavaScriptの暗黙の型変換の挙動は複雑なので、気をつけましょう。

Effective JavaScript JavaScriptを使うときに知っておきたい68の冴えたやり方

Effective JavaScript JavaScriptを使うときに知っておきたい68の冴えたやり方

*1:けっこう色んな言語で問題になってるぽいですねw

*2:ちなみに、NaNであるかどうかの判定としてはisNaN関数というのも用意されています。ただ、こちらは引数を数値に型変換してからNaNかどうかの判定をする為、例えばundefinedやオブジェクトを渡した場合にもtrueになってしまいます。ECMAScript6でNaNだけを判別出来るNumber.isNaNという関数が策定された様なので、こちらを使った方が良いでしょう。このページが参考になります。

*3:仕様ではこうなのですが、実際にはnull演算子の型をtypeof演算子で見てみると'object'が出力されます。このへん実装と仕様にずれがある気がします。