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などもともと存在するオブジェクト(組み込みオブジェクトと呼ばれることもあります)に対してこのような方法で機能を追加するのは勧められたことではありませんので、実際にはやらないようにしましょう。


String.prototype.abcabc = "test";

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

また、new String("foo")のような、String,Number,Booleanのインスタンスは、あくまでオブジェクトでありプリミティブではありません。これらをプリミティブ値の代わりに使うことはできず、あくまでプリミティブのプロパティを参照する機能の実現のために存在しています。

なお、残り2種のプリミティブ、すなわちnullとundefinedは、プロパティを参照しようとしてもオブジェクトが生成されることはありません。nullやundefinedのプロパティを参照しようとするとエラーになります。

型変換

もうひとつプリミティブに関連する値として、型変換について紹介します。JavaScriptでは、型の変換を明示的に・あるいは暗黙的に行うことができます。

真偽値への変換

まず最初に真偽値への変換を解説します。以前にも説明したように、if文の条件に渡されるのは真偽値であるべきで、もし真偽値でない値が渡された場合は真偽値に変換されます。

真偽値への変換は次のように行われます。まず、null・undefinedはfalseに変換されます。数値は0とNaN(後述)はfalse、それ以外はtrueです。文字列については、""ならfalse、他は全てtrueです。そして、オブジェクトは全てtrueです。

このことを利用すると、ある値が「オブジェクトまたはnull」であることが分かっている場合に、それがオブジェクトである(nullではない)かどうか判定するために真偽値への変換を用いることができます。そのような例はDOMに多く見受けられます。例えばノードのpreviousSibling・nextSiblingは該当するノードがあればそのノード、なければnullが入っているプロパティです。ノードは当然オブジェクトなので、真偽値に変換するとtrueとなります。つまり、あるノードnodeに次の兄弟ノードが存在するかどうかで分岐したいときは、次のようにすることができます。


if(node.nextSibling){
  ...
}

もちろん、この場合nullか否かを判定できればいいので、node.nextSibling != nullとしてもよいでしょう。こちらのほうが意図が明示的なので好む人がいるかもしれません。

等価演算子

さて、等価演算子==)でも実はさまざまな型変換が行われます。異なる型の値を比べる場合、型変換で型を揃えてから処理されます。

まず、nullとundefinedは別の値ですが、実はこれらを比べた場合trueです。console.log(null == undefined);などとして確かめてみましょう。nullやundefinedやそれ以外の値とは==になりません。

次に、数値と文字列を比べた場合、文字列が数値に変換されます。文字列から数値への変換というのは、例えば"3"という文字列なら3という数値に、"7"という文字列なら7という数値に……という感じです。ただし、"あいう"のような、数値を表さない文字列の場合はNaN(後述)に変換されます。

ということは、例えば3=="3"はtrueになるということです。

その次は、真偽値とそれ以外を比べる場合です。この場合はとりあえず真偽値を数値に変換します。真偽値は、falseが0、trueが1に変換されます。例えば真偽値と文字列の場合など、真偽値を数値に変換しても型が同じにならない場合はさらに型変換が行われます。例えば、true == "1"はtrueになります。これは、まず真偽値trueが1に変換され、数値1と文字列"1"の比較となります。これは数値と文字列の変換となり、上で説明した場合と同様なので、"1"という文字列が1という数値になります。よって、両辺が1に変換され一致したのでtrueとなります。

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

オブジェクトの変換

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


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

この例では、文字列"aaa!"とオブジェクトaaaの比較なので、まずオブジェクトが文字列に変換されます。これはaaaのtoStringメソッドを呼び出した返り値となります。すなわち、"aaa!"です。これで両辺の文字列は一致するため"aaa!" == aaaが成り立ちました。

これは細かい内容ですが、オブジェクトを文字列に変換したい場合でも、toStringがない場合はvalueOfを代わりに呼び出します。その逆もありえます。

ただ、toStringは実はObject.prototypeにあるので、toStringを自分で定義しなくても普通のオブジェクトにはtoStringは存在しています。したがって、「toStringがない」という状況を作るには、例えば次のようにtoStringを上書きしておく必要があります。


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

toStringもvalueOfもない場合、変換しようとするとエラーになります。まあ、そんな変なオブジェクトを作る機会は無いと思うのでこんなことは忘れても構いません。

また、valueOfは数値に変換したい場合に呼び出されると説明しましたが、toStringは「文字列に変換」なのに対して、実はvalueOfは「数値に変換」という意味ではありません。実際、Object.prototype.valueOfは、「自分自身を返す」というメソッドです。例えば({}).valueOf(){}というオブジェクトを返します。プリミティブに変換したいのにこれは何の役にも立ちません。このようにtoStringやvalueOfがプリミティブ以外を返した場合、それは無効となり、メソッドが無かった場合と同様にもう一方が試されることになります。

したがって、例えば3 == {toString: function(){ return "3"; }}はtrueとなります。

厳密等価演算子

このように、透過演算子(==)は実は型変換を行ってから一致判定を行っています。ということは、厳密に「同じ」値でないもの(例えば文字列"3"と数値3)を判定してもtrueが返ることがあるということです。このことから、実は==(と!=)はJavaScriptプログラマ達からは嫌われており、実はあまり使われていません。そもそも、勝手に型を変換してしまうというのが直感的な挙動ではありませんね。

そこで、本当に「同じ」値の場合のみtrueを返す比較演算子があります。それが厳密等価演算子です。これは、===という、イコールを3つ並べた演算子で、使い方は等価演算子(==)と同じです。

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

この性質により、0 === false"3" === 3などはfalseを返します。

JavaScript初心者はこの===の存在をあまり知りません。型変換が必要ない場面(というよりそれがほとんどですが)では厳密等価演算子を使うようになれば、一歩ステップアップです。もし型変換が必要な場合、==による暗黙の型変換に頼るのではなく型変換の処理を明示的に書いたほうが分かりやすいと思います。

ただ、普通の等価演算子(==)をよく使われる場面が1つだけあります。それは、何か == nullというようにnullと比較する場合です。この場合の動作を正確に述べるならば、左辺の値がundefinedまたはnullのときこの式はtrueとなります。undefinedとnullは別物ですが、この2つを一緒くたに扱って差し支えない場面も多く、そのような場面にこの書き方が使われます。特に、undefinedとnullに共通する特徴は「プロパティを参照しようとするとエラーになる」ことなので、その場合を除外したい場合などにこの比較を用います。

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

加法演算子

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

ご存知の通り、加法演算子には2つの機能があります。1つは3 + 5のような数値の加算で、もう1つは"foo" + "bar"のような文字列の連結です。

これらは両方ともプリミティブ同士の演算であるということで、まず最初に両辺のオブジェクトはプリミティブに変換されます。これはvalueOf優先です。

ここで、どちらか一方でも文字列であれば文字列の連結となりなす。この場合、もう片方が文字列以外の場合は文字列に変換されます。

どちらも文字列でない場合(すなわち数値、真偽値、null、undefinedの場合)、数値の加算となり両辺が数値に変換されます。

例えば、2 + trueはtrueが1に変換されるので3です。また、"2" + trueの場合、trueは文字列に変換されるので"2true"になります。

その他の算術演算子

加法演算子以外の四則演算(具体的には-*/%)は、文字列の連結機能がないのでもっと単純です。両辺は必ず数値に変換されます。

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


console.log(true - 0); // 1

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


console.log(true-false); // 1

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

NaN

ただ、ひとつ注意したいのが、NaNの存在です。今回の説明ではNaNが出てきました。これは、数値への変換の結果として現れることがある特殊な数値です。NaNという名前はNot a Number(数値ではない)から来ており、数値ではないのに数値という不思議な値です。これは、必ず数値へ変換しなければならないのに数値への変換は不可能という困った状況をなんとかするために存在します。また、数値の演算の結果を数値で表せない場合にもNaNとなります(例えば0 / 0など)。

例えば、"foo"という文字列は数値を表す文字列ではないので数値に変換できません。例えば"foo" - 0とすると"foo"がNaNに変換されるため結果はNaNとなります。確かめてみましょう。

なお、NaNを含む四則演算は全てNaNになります。たとえば0 + NaNNaN * 3はNaNになります。

NaNの他の特徴は、NaNを含む比較演算の結果は必ずfalseとなることです。3 < NaNNaN <= 3などはfalseになります。さらに驚くべきことに、NaN === NaNですらfalseになります。

NaNはプログラム上ではNaNと書くと取得できます。これはundefinedと同様、NaNという変数にNaNが入っています。また、Number.NaNとしても取得できます(つまり、NumberオブジェクトのプロパティNaNにNaNが入っています)。後者のほうが新しくて推奨されています。

前述の理由から、ある数xがNaNかどうか判定するのにx === NaNという方法は使えません。NaNはx !== xとなる唯一の値なのでこの方法でもよいのですが、もっとスマートな方法があります。それは、isNaN(またはNumber.isNaN)です。これは、引数がNaNのときのにtrueを返す組み込み関数です。すなわち、isNaN(x)とすることでxがNaNかどうか判定できます。

NaNは前述のように、数値に変換できない値を数値に変換しようとしたときや、おかしな演算をした場合に発生します。意図せずNaNが発生した場合はこのような箇所を疑いましょう。

プリミティブの変換方法

今回はプリミティブの変換を扱いました。それは==+などの、プリミティブを扱う演算子に値が渡されたときに発生します。しかし、もっと明示的にプリミティブの変換を行う方法が存在します。それは、次のような方法です。


String(x)  // xを文字列に変換した値を返す
Number(x)  // xを数値に変換した値を返す
Boolean(x) // xを真偽値に変換した値を返す

先ほども出てきたString, Number, Booleanは、コンストラクタの名前だったはずです。しかし今回、これらは普通の関数として使われています。これらの関数は特殊で、普通の関数としての用法とコンストラクタとしての用法を持っています。組み込み関数はこのような動作をするものが他にもあります。(ここでは解説しませんが、そのような関数を自分で作ることもできます。)