値と演算子
オレのためのJavaScript入門、その5
JavaScriptでは値は式の中で演算子を使って扱う。オブジェクトを直接扱うことはできない。式の中でプロパティが変更されることによって、結果的にそのプロパティを乗せたテーブル(=オブジェクト)の特徴が変わる。と、これが前回と前々回の内容。今回は値と演算子についてもう少し細かく調べてみる。
とはいっても網羅的にやっていてはキリがないので、混乱しかねない部分だけを簡単に触れてみる。これは以前に「臨機応変」と揶揄した部分でもある。
演算子は1つ以上の値を要求する。そしてその要求に対して提供された値から1つの値を新しく作り出す。これが基本。
まずはじめに、値にはそれぞれの種類が決まっておりそこにあいまいさの入り込む余地はない、ということを確認しておきたい。JavaScriptは「型に対して柔軟」とか言われたりするが、実際のところは演算子が複数の種の値に対応する、という臨機応変さを持っているだけである。このことが「型に対して柔軟」という言葉であいまいにされているがために、しばしば混乱の原因になっているように思われる。
この「臨機応変」な対応は、正直なところ(現時点では)「柔軟」とは言いがたい。演算子が全てのプログラマに対して自然な振る舞いをすることができればそれは「柔軟」と言えるかもしれないが、これは非常に困難である。それを行うためには式に求められる文脈を読み取らなければならない。しかし現実は演算子は自らが要求した値の種類だけしか把握していない。式で扱われる値の種類はプログラマが正しく管理する必要がある。
混乱の原因に加えるなら、見た目上同じプロパティという器に対してどの種類の値でも代入することができる、という点も挙げられる。プロパティに代入されている値の種類も、やはりプログラマが正しく管理しなければならないだろう。これについてはプロパティの命名規則である程度の対応が可能かもしれない。(例えばobject種を保存するプロパティには~objとする、とか? しかしこうすることはやっとの思いで獲得した「自由」に対する冒涜かもしれない) これについては現実にすでに半ば規則化している部分もある。(コンストラクタとして使われるfunction種のプロパティにはUpperCamelCase、とか)
まずはじめに数値を扱う演算子を考えてみる。これらは一般的には算術演算子と呼ばれる。ビット演算子もこの範疇に入るだろう。
この演算子が要求する値は、当然のことながら基本的にnumber種。提供された値がそれ以外の種類だったときは演算子によって「種の変換」が行われる。値の種類に応じて演算子が「臨機応変」に立ち回る、というわけだ。
string種とboolean種は数値として扱われる。
数値化できないstring種はNaNという特殊な値として扱われる。NaNとは数値でないことを表すnumber種の値である。NaNはNaNを含むいかなる値とも一致せず、また、大きくも小さくもない、という特徴を持つ。(「そんなの数値じゃないじゃん」と突っ込まれそうだが、要するにそういうことなのだ) NaNを含む演算の結果はNaNになる。
boolean種においてはtrueは1、falseは0として扱われる。
この演算子の生み出す値は全てnumber種である。(この中にはNaNも含まれる。なお、値がNaNかどうか調べるにはisNaN関数を使う)
+演算子は数値を扱う加算演算子であるが、同時に文字列結合演算子でもある。提供された値にstring種が含まれていた場合は文字列結合演算子として機能する。このときに生み出される値は、string種である。+演算子のこの対応はとても「柔軟」とは言い難いが、まあ混乱することも少ないだろう。(ちなみに加算演算子と文字列結合演算子が別の言語もあり、個人的にはこの仕様の方が好きだ) 文字列結合演算子が要求する値がboolean種だった場合は'true'または'false'というstring種に変換される。
さて、これらの演算子に提供された値がobject種やfunction種だったらどうなるか。これらは参照値であり、JavaScriptにおいて上に挙げた演算子はこれらの値を演算することはできない。しかしエラーにはならない。
この場合はこれらの参照先のオブジェクト(以前にfunction種の参照先は「定義」と書いたが、これも実はオブジェクト。近いうちに詳しく解説する)の持つプロパティtoString関数の戻り値に変換される。この値はその名が示す通りstring種である。例外はNumberオブジェクトで、このオブジェクトへの参照を持つobject種の値に限りnumber種に変換される。(ただしこの場合でもtoString関数の戻り値はstring種)
そして演算によって作り出される値はstring種、またはnumber種の値である。
以下、蛇足。
式の中でのオブジェクトの操作をそのオブジェクトが持つプロパティへの代入のみに限定する、というのはアリだと思う。しかし上の扱いが「柔軟」な対応に見える人はあまりいないのではないだろうか。むしろかなり無理矢理な対応に見える。オブジェクトや関数を足したり引いたりしようとする人はあまりいないのかね。そんなこともないと思うけど。ここは素直に「そんなことはできません」としてしまった方が個人的にはすっきりする。
方向性としてはfunction種の値が返す文字列のようにevalできる文字列にする、というテもあるのだろうけど、これは機能して別のものかもしれない。
演算子をオーバーロードする機能がない、ということもあるのだろうけど。
話を戻す。
次は代入演算子。これは右辺の値を左辺のプロパティに代入する。この際、「種の変換」は行われない。object種やfunction種を扱ってもこれらの値の参照先のオブジェクトには何の変化も起きない、というのはこれまで散々書いてきた通り。
この演算によっても新しい値が作り出される。代入で扱われた値がそれである。よってa = (b = 1) + (c = 2);
のような記述ができる。ただしこの記法は一般的にあまり好まれない。
続いて比較演算子。左右の値を比較してその結果に応じたboolean種の値を生成する。ただし、ここでも「臨機応変」な種の変換が行われる場合がある。
===と!==については種の変換は行われない。値の種類が違えばその時点でfalseになる。また==と!=においても同じ種類の値を比較する場合も種の変換は行われない。気を付けなければならないのは、object種やfunction種同士の比較では参照先の内容が比較の対象ではない、ということ。(a = new Object()) == a
はtrueだが(new Object()) == (new Object())
はfalseだ。object種とfunction種を==で比較する場合には種の変換は行われないらしく、必ずfalseになる。(もちろん!=ではtrue)
種類の異なる値を==と!=で比較する場合と、不等号を含む比較演算子に提供される値に対しては例によって種の変換が「臨機応変」に行われる。まずobject種とfunction種はtoStringされた値(ただしNumberオブジェクトはnumber種)に変換される。さらに種類が異なる場合はnumber種に変換される。string種同士の比較は辞書順でされるようだ。
以下にサンプルを挙げる。どの種類の何の値に変換されているか考えてみてほしい。(NaNに変換される場合に注意) ヒマな人はシェルでいろいろ試してみるのもおもしろいかもしれない。
'123' < '9' //true
'123' < 9 //false
'0' == 0 //true
'' == 0 //true
true < '100' //true
true < '1' //false
false == '0' //true
false == '' //true
true < 'true' //false
true > 'true' //false
true == 'true' //false
(function(){}) <= (function(){}) //true
(function(){}) >= (function(){}) //true
(function(){}) == (function(){}) //false
obj = new Object();
obj.toString = function(){return 1};
func = function(){};
func.toString = function(){return 1};
obj == true //true
func == true //true
obj == func //false
strobj = new String('JavaScript');
tmpobj = new String('JavaScript');
str = 'JavaScript';
tmp = strobj;
strobj == tmp //true
strobj == str //true
tmpobj == str //true
strobj == tmpobj //false
ただ上のサンプルに挙げた種の変換が実際のプログラミングにおいて問題を起こすことはまずないだろう。なぜならこれらは比較すること自体にほとんど意味がないからだ。
思ったより長くなってしまったので、残りの演算子については次回。
PR