uhyohyo.net

JavaScript初級者から中級者になろう

九章第七回 プリミティブとオブジェクト

今回はプリミティブとオブジェクトの関係について解説します。

プリミティブとは、

"あいうえお"
123
true

のように、オブジェクトではない値のことでした。

プリミティブのプロパティ

ここで、次のサンプルを見てみます。

var aaa = "aiueo";

console.log(aaa.length);
          

「5」と表示されます。実はこのlengthプロパティは文字列の長さがわかるものなのですが、なかなか便利です。しかし、おかしいと思いませんか?

変数aaaの中身は"aiueo"です。これはプリミティブだから、プロパティなどないはずです。

実は、プリミティブのプロパティを呼び出そうとしたとき、内部的にオブジェクトが作られて、そのプロパティを参照しているのです。

それでは、どんなオブジェクトが作られているというのでしょう。

実はこれは、プリミティブの型ごとに決まっています。プリミティブには文字列・数値・真偽値がありましたが、それぞれString,Number,Booleanのインスタンスが作られます。

これらのインスタンスは自分でも作れます。

var abc = new String("あああ");
console.log(abc);
          

「あああ」が表示されます。

つまり、aaa.lengthは、内部的に

(new String(aaa)).length

というようにして処理されていたのです。

だから、String.prototypeにプロパティを追加すれば、文字列リテラルのプロパティとして利用できるかもしれません。もっとも、使い道があるかは分かりませんし、そのようなことはあまり勧められません。

String.prototype.abcabc = "test";

console.log("あああ".abcabc);
          

この例のように、いちいち変数に代入しなくてもプロパティを参照したりできます。

型変換

先の例のように、Javascriptでは、Stringのインスタンスを文字列と同じように使うことができました。Numberも同様なのですが、注意すべき点があります。

それは型変換です。JavaScriptでは、異なる型どうしを演算子で扱う場合など、型の変換をして同じ型に合わせてから処理します。それについて解説します。

真偽値への変換

まず最初に真偽値への変換を解説します。これは、if文である変数や値を直接評価するときなどによく出現します。

if(aaa){ ・・・ }

というような場合ですね。

if文は真偽値にしないと評価できないので、全て真偽値に変換されます。二章第十四回で出てきた感じの話題です。二章第十四回では、if文で処理するとき真になるか偽になるかという解説をしましたが、実はこれは真偽値に変換するとtrueになるかfalseになるかということだったのです。

さて、変換結果は、null・undefinedはfalseに変換されます。数値は0ならfalse、それ以外は全てtrueです。文字列も、""ならfalse、他は全てtrueです。そして、オブジェクトは全てtrueです。

ここで、さっきのオブジェクトが出てきます。真偽値には、Booleanのインスタンスが対応しています。

console.log(new Boolean(false));

これで真偽値を表せそうな気がしますが、これをif文で使うとどうでしょう。

if(new Boolean(false)){
  console.log("真");
}else{
  console.log("偽");
}
          

なんと「真」となります。

falseのBooleanオブジェクトのはずなのにtrueに変換されてしまったということです。

これはなぜかというと、オブジェクトは全てtrueに変換されるからです。このようなことが起こるので、値を表すならばプリミティブ値を使うようにしましょう。StringやBooleanなどのインスタンスを使う機会はめったにありません。

等価演算子

さて、等価演算子==)でもさまざまな型変換が行われます。実は、比べる型が違う場合、どのように処理するかは細かく決まっています。

まず、nullとundefinedは型が違いますが、これらを比べた場合trueです。

次に、数値と文字列を比べた場合、文字列が数値に変換されます。つまり、"3"なら3、"7"なら7・・・という感じです。

ですから、たとえば3=="3"はtrueになります。

その次は、真偽値とそれ以外を比べる場合、とりあえず真偽値を数値に変換します。例えば真偽値と文字列とか、それでも同じに成らない場合、これは「数値と文字列」の比較となるので、文字列が数値に変換されて、晴れて数値どうしになるわけです。

ところで、真偽値を数値に変換するとどうなるかですが、これは単純です。trueなら1、falseなら0です。

true == "1"	//これはtrue
false == 0	//これもtrue
true == 2	//これはfalse
        

さて、これでもまだうまくいかないのは、オブジェクトとプリミティブの比較の場合です。オブジェクトと真偽値を比較する場合は前述の通り真偽値が数値に変換されるので、オブジェクトと、文字列または数値の比較になります。この場合、オブジェクトをプリミティブに変換します。これにより、プリミティブどうしの比較となり、解決できます。

オブジェクトの変換

さて、オブジェクトをプリミティブに変換するにはどうするのでしょう。実は、オブジェクトが変換されるときに呼び出されるメソッドがあります。それは、toStringvalueOfです。一般に、toStringは文字列に変換したいとき、valueOfは数値に変換したいときに呼び出されることになっています。自分でオブジェクトを作ったとき、これらのメソッドを作っておけば変換されたときの値を操作できるというわけです。

var aaa={
  toString: function(){return "aaa!";}
};
console.log("aaa!"==aaa);
          

また、文字列がほしい場合でも、toStringがない場合はvalueOfを呼び出します。逆もありえます。ただ、toStringは実はObject.prototypeにあるので、nullを代入するなどして使えないようにしないとこれは起こりません。

var aaa={
  toString:null,
  valueOf:function(){return "aaa!";}
};
console.log("aaa!"==aaa);
          

toStringもvalueOfもない場合、変換しようとするとエラーになります。

さて、valueOfは数値に変換したい場合と説明しましたが、toStringは「文字列に変換」なのに対して、実はvalueOfは「数値に変換」というわけではありません。「オブジェクトのデフォルト値」ということになっています。String,Number,Booleanのオブジェクトの場合は、対応するプリミティブ値が返りますが、Object.prototype.valueOfは、「自分自身を返す」というメソッドです。つまり、オブジェクトが帰ってくるということです。まったく変換できていません。このように、toStringやvalueOfがプリミティブ以外を返した場合、それは無効となります。つまり、先にtoStringが呼び出されてプリミティブが返されなかった場合、無効となって今度はvalueOfが呼び出されます。両方とも無効の場合はやはりエラーです。

オブジェクトを変換しようとしてtoStringやvalueOfで真偽値が帰ってきても、それはエラーにはなりません。等価演算子の場合、その真偽値がさらに数値に変換されて無事比較できるというわけです。また、等価演算子でオブジェクトを変換する場合、「数値に変換したいとき」に該当する感じで、valueOfが先に呼び出されます。

だから、次のコードはtrueとなります。

var aaa = {
  valueOf: function(){return false;}
};
console.log(aaa == 0);
          

これは、 「(オブジェクト) == (数値)」の形です。まずオブジェクトを変換するためにvalueOfが呼び出され、falseになるので、「(真偽値) == (数値)」のパターンとなります。真偽値は数値に変換されるので、数値どうしの比較となるわけです。

前述の、

var aaa={
  toString: function(){return "aaa!";}
};
console.log("aaa!"==aaa);
          

というケースではvalueOfではなくtoStringが呼び出されているように見えますが、これはvalueOf(Object.prototype.valueOfが呼ばれる)を呼び出した結果がオブジェクトだったので無効になり、次にtoStringが呼ばれて比較されたことによります。

厳密等価演算子

ここで、厳密等価演算子というものを紹介します。これは、===という、イコールを3つ並べたもので、等価演算子(==)と同じように使えます。

では、どこが違うかというと、型が違うものどうしは全てfalseという点です。等価演算子では、型変換をして調整してくれていましたが、厳密等価演算子では全てfalseにされます。等価演算子ではtrueだったnullとundefinedの比較も、falseになります。

たとえば普通の等価演算子だと、0==falseのようなものがtrueになるなど、直感的ではないかもしれない場合があります。厳密等価演算子では型変換を行わないのでこれもfalseになります。

実際、型変換をして比較してくれないと困るという場面は滅多に無く、型が違うならばfalseにしてよいという場面のほうが多いので、この厳密等価演算子がよく使われます。型変換が必要ない場面(というよりそれがほとんどですが)では厳密等価演算子を使うようになれば、一歩ステップアップです。

また、型が違えば型変換をすることなくfalseを返すので、普通の等価演算子よりわずかに処理速度が速いという利点もあるそうです。

逆に、普通の等価演算子を使う場面で多いのが、変数==nullというnullとの比較です。変数がundefinedまたはnullのときtrueを返します(厳密等価演算子の場合はundefinedならfalse)。undefinedとnullに共通する特徴は「プロパティを参照しようとするとエラーになる」ことなので、その場合を除外したい場合などにこの比較を用います。

ちなみに、===の否定形は!==です。

加法演算子

さて、次は加法演算子(+)について見ていきます。今までやたら説明が長かったですが、基礎がわかったのでこれからは簡単です。

加法演算子は、まず最初に両辺のオブジェクトをプリミティブに変換してしまいます。これもvalueOf優先です。

ここで、どちらか一方でも文字列であれば文字列の連結となり、もう片方が文字列でなくても文字列に変換してしまい、連結します。

そうではない場合、どちらも数値に変換されます。真偽値の場合などは、ここで数値になります。あとは足すだけです。だから、「2 + true」は、trueが1に変換されるので3です。「"2" + true」の場合、trueは文字列に変換されるので"2true"になります。

減法演算子

減法演算子は、文字列の連結機能がないのでもっと単純です。両方を数値に変換して計算するだけです。

そこで、数値以外を数値に変換する手軽な方法として、「0を引く」というものがあります。そうすれば減法演算子が左辺を数値に変換してくれて、0を引いても変わらないので結果左辺を数値に変換できたことになります。

console.log(true - 0);

また、falseも0に変換されるので、falseを引いてもいいです。

console.log(true-false);

こうすれば、一目見ただけでは何がしたいのか分かりません。コードを汚くするのに一役かってくれます。

ただ、ひとつ注意したいのが、NaNの存在です。

NaNというのは、実は数値の一種です。そのため、NaNはプリミティブであり、数値であるということです。ちなみにこれは、null,undefinedなどと同様に、NaN とするとNaNが得られます。また、NaNと他の数値どうしで数値計算をしようとすると、結果はNaNになります。例えば計算に複数の変数を使うとして、そのうち1つでも間違ってNaNになると、一瞬にしてNaNが広がっていきます。

また、NaNには大きな特徴があります。

console.log(NaN==NaN);

これがなんとfalseになります。NaNを等価演算子で比較しようとすると、無条件でfalseになってしまうのです(厳密等価演算子でもfalseです)。

さて、なぜこのNaNに注意するかというと、関数などを数値に変換する際、このNaNが現れる可能性があるということです。そうすると、せっかく0を引いてみたりしてもNaNのままです。まあ確かに数値ではありますが、あまり意味がないですね。

たとえば、"a" - 0のように数値に変換できない値を数値に変換しようとするとNaNになります。