前回の記事では関数について学びました。今回は新しいデータ構造、配列・オブジェクトについて学びましょう。

連載目次

  1. 基礎と文法
  2. 関数
  3. 配列とオブジェクト(この記事)
  4. オブジェクト指向
  5. データ構造とアルゴリズム(予定)
  6. 未定
  7. 未定

今回学ぶ内容

今回学ぶのは、配列オブジェクトです。

配列もオブジェクトも、値を保持するデータ構造です。これまでは値は値単体で扱ってきましたが、複数の値をひとつのデータ構造にまとめることで、効率的な処理を可能にします。これらは多量の複雑な値を扱う時に重宝します。

前回までの記事は、どちらかというと「処理の流れ」にフォーカスが当たった内容でした。今回は少し方向性が変わり、「値(データ)の扱い方」に焦点を当てていきたいと思います。

配列

配列を作って利用する

配列は、内部に複数の値を保持するためのデータ構造です。変数とは異なり、配列自体は値(データ)であり、「値を保持する値」ということになります。ややこしい……。

配列は値なのですが、イメージとしては、変数が複数個くっついたようなものとみなして大丈夫です。

配列は長さを持ち、作成時にいくつの値を持つかを指定します。たとえば値を3つ保持する配列は以下のように作ります:

ここでnewというキーワードが出てきました。newは新しい値を生成するためのキーワードです。「では、なぜ数値や文字列ではnewは不要だったのか?」という疑問がわくと思いますが、あれらは少し特殊な値だからです(後述します)。とにかくここでは長さ3の新しい配列(Array)を作成してます。

配列を作成できたら、あとは好きな値を入れていきます。配列のn番目にアクセスするには、「[n]」を配列の後ろにつけます。

console.log関数に配列を渡すと、見やすいように表示してくれます。

配列は0番目から始まります。長さ3のときは、0番目、1番目、2番目の3つになります。そこに好きな値を格納することができます。このとき「n番目」のnのことを添え字インデックスと呼びます。添え字には変数を使うことも可能です。添え字はもちろん整数値である必要があります。

添え字を使えば個別の値にアクセス可能です。各値をひとつずつ表示するコードは以下のようになります:

配列に格納する値に制限はありません。数値、文字列、真偽値、関数、なんでも入ります。配列の中に配列を入れることも可能です。値の型を揃える必要も特にありません。バラバラの型の値が入っていても大丈夫です:

また、配列に格納されている値のことを、配列の要素と言います。

しかし毎度こんなことをやっていると、コードが長くなりすぎてしまいます。よって配列の生成には簡易記法が存在します。こっちの簡易記法を使うことの方が多いです。簡易記法では、「[値1, 値2, 値3, …, 値n]」のように書きます:

1行で済みましたね。こっちの方が簡単そうです。こういった、newなどを使わずにコード中に直接値を記述する方式をリテラルと言います。配列リテラル以外にも、今まで使用してきた数値もリテラルですし、文字列もリテラルです。真偽値もリテラルです。

constと配列

そういえばconstで宣言された変数は値の変更が不可能だったように思います。ですが先ほどの例では配列の中身を変更しています。いったいどういうことでしょう。

実はここには言葉の綾というかトリックが潜んでいて、constは「値の結びつきを後から変更することはできない」であって、これは「既に結びつけられた値の中身が変わっても無頓着」ということにもなります。そうです、別の配列に付け替えることは不可能でも、同じひとつの配列の中身をいじることは全く問題はないのです。

この性質には十分注意すべきです。constキーワードは、配列の中身の固定については保証しません。

ループと配列

配列はその性質上、ループと非常に相性がいいです。特にforループのカウンタと、配列の添え字を連動させたコードはよく使われます。例えば以下のようにです:

これで配列の中に0, 1, 2, 3, 4…が入ります。

大抵の場合、配列はループと一緒に活用します。配列単体で出てくることはあまりないので、ループについてもしっかり復習しておきましょう。

for-ofループ

for文はwhile文の簡易版のようなものでしたが、配列に対してはさらに簡易版が存在します。for-ofと呼ばれるもので、以下のように使用します:

このコードは以下とほぼ同じです:

変数iがないのが大きな違いですね。for-ofはカウンタ変数を利用しないという欠点がありますが、カウンタ変数が不要なら積極的に使っていきましょう。

例題

例題:配列[1, 3, 5, 7, 9, 11, 13, 15, 17, 19]と配列[2, 4, 6, 8, 10, 12, 14, 16, 18, 20]を作成し、それぞれのi番目同士の和を配列sumのi番目に格納せよ。また、配列sumをコンソール上に表示せよ。

これはまずそれぞれの配列を作成して、また配列sumも作成しておきます。あとはfor文でループを回して0番目から9番目までを処理するだけです:

オブジェクト

オブジェクトの作成と利用

JavaScriptで使えるデータ構造には、配列の他にもオブジェクトというものがあります。配列は整数値(添え字)と値を結びつけるようなデータ構造でしたが、オブジェクトは文字列と値を結びつけるデータ構造です(※このデータ構造のことをオブジェクトと呼ぶのはJavaScript特有で、他の言語では連想配列と呼びます)。

オブジェクトは配列と同様、newで生成する方法と、リテラルを使用する方法があります。まずはnewを使用する方法を見ていきましょう:

配列とだいたい同じですね。違うのは添え字が文字列であることです。このとき添え字のことをキー、格納されてる値のことをバリュー(値)と言います。

リテラル記法では「{key1: value1, key2: value2, …, keyN: valueN}」と表記します。リテラルの場合は、キーはクォートでくくりません:

長くなりそうなら途中で改行しても大丈夫です(配列のリテラルも同じです):

配列と同様、格納できる値には制限はありません。ただしキーは文字列に限ります。

プロパティとメソッド

オブジェクトにおいて、キー/バリューのペアのことを「プロパティ」と言います。

プロパティには括弧を使った「[‘キー’]」の他にも、ピリオド記法でアクセスすることができます:

ピリオド記法の欠点はプロパティ名が変数名・関数名と同じ制約を受ける(名前が数字で始まってはいけない)点ですが、普通は問題にはならないでしょう。

また、プロパティとして関数を格納することも可能です(JavaScriptにおいて関数は値です)。このとき特に値が関数のプロパティのことをメソッドと言います。例えば上のpointオブジェクトに、座標を表示するメソッドprintを格納すると以下のようになります:

メソッドにもピリオド記法でアクセスできます。このとき、メソッド内部からプロパティまたは他のメソッドを呼び出すには、先頭に「this.」をつけます。「this.x」や「this.y」のようにです。thisはそのメソッドが格納されているオブジェクトのことを指します。例えばここではthisはpointオブジェクトです。

thisをつけない場合は、そのメソッド内の変数を指します。「x」と「this.x」では全く意味が違ってきます。

メソッドを活用することで、データと操作をひとつのオブジェクトにまとめることができます。プログラムが複雑になってきたら、オブジェクトにまとめることを検討してもいいでしょう。

consoleオブジェクトのlogメソッド

ところで気づいた人もいるかと思いますが、これはconsole.logと似たようなことをしています。そう、実はconsole.logはconsoleオブジェクトのlogメソッド呼び出しだったのです。

consoleオブジェクトはコンソールに関する様々なメソッドを持っています。詳しく知りたい人は以下のURLをご覧ください。

例題

例題:新規にオブジェクトrectangle(英語で四角形という意味)を作成し、rectangleに、width(幅)プロパティとheight(高さ)プロパティ、および自身の面積を計算するcalcAreaメソッドを追加せよ。ここでwidthプロパティとheightプロパティの値は任意とする。また、calcAreaメソッドが正常に動作することを確かめよ。

この問題はリテラルで解いてもnewで解いても、どちらでも大丈夫です。今回はリテラルでやってみましょう:

メソッド内でプロパティにアクセスするときはthisを忘れないようにしましょう。

プリミティブ型とオブジェクト型

JavaScriptでは、値の型は以下の7つだけでした:

  • undefined
  • null
  • 数値
  • 文字列
  • 真偽値
  • シンボル
  • オブジェクト

この中で、オブジェクト型以外の型をプリミティブ型と言います。プリミティブ型は原始的な値で、オブジェクト型のようなプロパティやメソッドを持ちません。

プリミティブ型を作成するときはnewキーワードは不要で、リテラルを書けばそのまま値が作成されます。これは今までの例から見ても明らかですね。逆にいうとnewキーワードが必要なのはオブジェクト型ということになります。

ラッパーオブジェクトとオートボクシング

プリミティブ型でもプロパティやメソッドを利用できた方が便利です。なので、JavaScriptにはプリミティブ型をオブジェクト型に変換するラッパーオブジェクトが存在します。

例えば数値型に対してはNumberオブジェクトというラッパーオブジェクトが存在します:

Numberオブジェクトは様々なメソッドを持っており、例えばtoPrecisionメソッドは指定した桁数の精度の文字列を返します。このように、文字列に対してはStringオブジェクト、真偽値に対してはBooleanオブジェクトがラッパーオブジェクトとして存在します。

ですが、毎回ラップするのは面倒です。なのでJavaScriptには便利な機能がありまして、プリミティブ型に対してメソッド呼び出しを行うと、自動的に対応するオブジェクトでラップしてくれます。これをオートボクシングと言います。オートボクシングを使えばプリミティブ型かオブジェクト型か気にせずメソッド呼び出しを行えます:

これだけで先ほどの例と同じように動作します。なお、小数点とメソッド呼び出しのピリオドを区別するため、数値を括弧でくくっています。

オブジェクト型と等価性

等価演算子を用いてオブジェクト型を比較するときは注意が必要です。オブジェクト型に対しては、等価演算子は中身を比較するのではなく全く同一のオブジェクトかどうかを判定します。例えば以下の例をみてください:

obj1とobj2は中身は同じです。ですが比較するとfalseとなります。これは異なるオブジェクトだからです。一方obj1とobj1を比較するとtrueとなります。これは同一のオブジェクトだからです。

オブジェクト型と変数

まずは以下のコードを見て結果を予想してみてください:

objをコピーしようとしてobjCopy変数へ代入して、objCopyのプロパティを変更しています。これの実行結果は以下のようになります:

なんとコピー元のobjのプロパティまで変更されています。これはいったいどういうことでしょう?

実はプリミティブ型と違って、オブジェクト型はコピーされないのです。これはオブジェクト型は大きなデータとなる可能性があるため、毎回コピーしていると効率が悪いからです。

オブジェクト型を変数に代入した場合、その変数にはオブジェクトそのものではなく、参照値というデータのアドレス(住所)が入ります。他の変数にコピーした場合も、オブジェクトそのものではなく参照値がコピーされます。

参照値がコピーされるため、結局どちらの変数も同じオブジェクトを指していることになります。

これは特に関数において注意が必要になります。関数の引数にオブジェクトを渡すと、オブジェクトではなく参照値が渡されます。このため関数の中でオブジェクトを変更すると呼び出し元のオブジェクトが変更されるという結果になります。

関数の中でオブジェクトを扱うときは、細心の注意が必要です。

また、余談ですが、JavaScriptにおいて具体的な参照値を知ることは不可能です。

オブジェクトとしての配列

ところで配列の型は何になるのでしょう。typeofを使って調べてみましょう:

オブジェクト型となりました。そうです、配列はオブジェクトなのです。

配列のプロパティとメソッド

配列はオブジェクトなので、多数のプロパティとメソッドを持ちます。例えばlengthプロパティはその配列の長さを表します:

他にも、配列に値を追加するpushメソッドなどがあります:

こういったプロパティ・メソッドを活用することで、配列の活用の幅が広がります。

配列と参照値

配列もオブジェクトなので、変数には参照値が入っています。そのため、扱いには注意してください。

オブジェクトとプロトタイプ

プロトタイプの作成

同じ構造を持ったオブジェクトを複数量産しようとすると、なかなか面倒なことになります。例えばx座標とy座標を持つオブジェクトを複数作るとします。このオブジェクトには2点間の距離を測るメソッドdistanceTo(other)もあるとします。このオブジェクトを複数作ろうとすると以下のようになります:

正直言って面倒ですし、やってられません。オブジェクト2個でこれなのですから、10個や20個になってくるともう耐えられません。

もう少し賢い方法を考えましょう。オブジェクトのテンプレートを作って、そのテンプレートの構成を複製すればいいのです。このときのテンプレートのことをプロトタイプと言います。

JavaScriptではプロトタイプを作成する方法が2つありますが、今回はそのうちの簡単な方を紹介します。プロトタイプはclassキーワードを使って作成できます。以下のようにします:

今回はPointという名前のプロトタイプを作成しています。一般的にプロトタイプ名は大文字から始めます。

あとは中にメソッドを書いていくだけです。このとき、必要であればconstructorという名前のメソッドを書いておきます。constructorはオブジェクトを複製した時に自動的に実行されるメソッドで、ここでオブジェクトの初期化処理等を行います。

コンストラクタ内でthisが使用されています。このthisは新たに生成されたオブジェクトを指すthisで、新しいオブジェクトが作られた時にプロパティxとプロパティyの値を設定しています。他のメソッドでも、自身のプロパティ/メソッドにアクセスするにはthisが必須です。

class内でのメソッド定義にはfunctionを使いません。そのままメソッド名を書けば関数とみなされます。

プロトタイプを作成できたら、newキーワードでオブジェクトを作成します:

プロトタイプをもとにオブジェクトを生成するには、「new プロトタイプ名(コンストラクタに渡す値)」の書式で書きます。今回は「new Point(x, y)」ですね。これでプロトタイプをもとにオブジェクトが作成されます。

ここで、functionとは違い、classは巻き上げられないことに注意です。簡単に言うとclass定義前にnewすることはできないということです。

プロトタイプとインスタンス

プロトタイプをもとに作られたオブジェクトのことを、インスタンスと言います。先ほどの例では、Pointのインスタンスを作成しました。

各インスタンスはそれぞれ独立したプロパティを持っています。一方メソッドは各インスタンスに共通なので、無駄を減らすためプロトタイプに格納されており、メソッドを呼び出した場合はプロトタイプにアクセスします。

プロトタイプを直接いじる

これはあまり重要ではないのですが、classキーワードで作成したプロトタイプを、後からいじることもできます。プロトタイプには「プロトタイプ名.prototype」でアクセスできます。例えば先ほどの例ですと、「Point.prototype」です。

これを利用すれば既存のプロトタイプにもプロパティ・メソッドを追加することができます。例えばArray.prototypeをいじれば、配列にプロパティやメソッドを追加できます。ただし、既存プロトタイプをいじるのはかなり行儀が悪い行為とされていて、普通は使いません。

プロトタイプとクラス

ところで「プロトタイプ」なのにキーワードは「class」となっていますね。これは他の言語ではオブジェクトのテンプレートのことを「クラス」と呼ぶからで、その文化に合わせた結果です。

もちろんJavaScriptでもクラスという言葉を使って問題ありません。プロトタイプとクラスの間にそこまで大きな役割の違いはないからです。自分の好きな呼び方を使ってください。この連載では、これからは「クラス」の方を採用します

getterとsetter

メソッドは関数なので、呼び出しには当然括弧が必要です。「Point.distanceTo(other)」のようにです。しかし引数がゼロ個のメソッドの場合、括弧を省略してまるでプロパティのように呼び出したい場面が稀にあります。

これはgetterという機能を利用すれば実現できます。getterを定義するには、メソッド名の前に「get」をつけます。以下が例です:

この例ではRectangleクラスに「area」というgetterを定義しています。getterは必ず引数ゼロで、戻り値があります。getterを定義すると、プロパティのように括弧無しで呼び出すことができます。例えば今回は最後の行の「rect.area」のようにです。

getterの反対の機能として、setterというものもあります。setterはプロパティに値が代入された際、代入以外の処理を実行するための機能です。メソッド名の前に「set」をつけることでsetterを定義できます。大抵はgetterと一緒に使用されます。以下のように使います:

setterは引数ひとつ戻り値無しのメソッドで、プロパティに値が代入された時に呼び出されます。setterで代入以外の作業を行うことで、入力値のチェックや、連動して他の値を更新するなどが実現できます。

なお、getterもsetterも、名前が他のプロパティ名と被ることはできません。プロパティとgetter/setterの名前が被りそうになったら、プロパティの方に「_(アンダースコア)」をつけるのが一般的な回避策です。もちろん被らない全く違う名前にしても大丈夫です。今回はプロパティを「age」ではなく「_age」にしています。

staticメソッド

メソッドには静的(static)メソッドというものがあります。staticメソッドはインスタンスを作らずにアクセスできるメソッドです。平方根を求めるMath.sqrt(x)などがstaticメソッドの例です。このときnew Math()などとせずに、Mathクラスのsqrtメソッドに直接アクセスしています。

staticメソッドを作るには、メソッド名の前にstaticをつけるだけです:

staticメソッドは、主にMathクラスなどのユーティリティクラスで使用されます。

例題

例題:半径を表すプロパティradiusと、面積を計算するメソッドcalcAreaをを持つクラスCircleを作成せよ。また、実際にいくつかインスタンスを生成し、面積計算が正しいか確かめよ。ただし円周率にはMath.PIを使用すること。

まずclassを使ってCircleを作成し、各メソッドを定義します。今回はconstructorとcalcAreaだけでいいでしょう。以下のようになります:

クラスを作成できたらあとは実際にインスタンスを作成して、計算結果を確かめるだけです。

今回のまとめ

今回は、有名なデータ構造である配列およびオブジェクトについて学びました。配列は複数の値を格納することができ、添え字によって各要素にアクセスすることができました。オブジェクトも複数の値を格納できますが、こちらは文字列を用いてアクセスします。

配列もオブジェクトもどんな型の値でも格納できますが、ここでオブジェクトに格納された値のことをプロパティと言います。特に関数であるプロパティのことはメソッドと呼びます。

配列も実はオブジェクトであり、多数のプロパティとメソッドを持ちます。プロパティとメソッドを活用することで、複雑な処理も簡単に記述することができます。

また、同じ構造を持つオブジェクトを複数生成するとき、クラス(プロトタイプ)と呼ばれるテンプレートを用いると簡単に生成することができました。ここで、生成されたオブジェクトのことをインスタンスと呼びます。

今まではどちらかというと処理の流れを学びましたが、今回はデータの構造に着目しました。これらのデータ構造を用いることで、大きなデータも効率的に扱うことができます。

例題

例題1

例題:空の配列を作り、その配列にランダムな数値100個を格納し、ひとつずつコンソール上に表示せよ。なおランダムな数はMath.ramdom()メソッドで生成することができる。

空の配列は[]、またはnew Array()、あるいはnew Array(0)で作ることができます。ここではリテラルである[]を採用しましょう。次にランダムな数値を生成して配列に追加(push)する処理を書き、for文で100回実行します。表示もひとつずつ行うので、もう一回for文を回して、表示します。以下のようになります:

Math.random()の結果は0.0以上1.0未満のランダムな値になります。これを配列のpushメソッドに渡すことで配列にランダムな値を追加できます。

配列のn番目にアクセスするにはarray[n]のようにします。このとき、nは0から始まるので注意してください。

例題2

例題:プロパティとして実部reと虚部imを含む、複素数を表すオブジェクトを作成せよ。ここでreとimの値は適当なもので良い。次にこのオブジェクトにメソッドtoString()を追加し、’5+3i’のような文字列に変換する機能をつけよ。また、toString()メソッドを用いて実際に文字列として表示せよ。

これはリテラルを用いてもクラスを用いても、どちらでも良いです。まずはリテラルでやってみましょう。

まずプロパティreとimを書きます。この値は適当で構いません。今回はreは3、imは-10としておきましょう。次にtoString()メソッドを定義します。メソッド内部でプロパティにアクセスするには、thisをつけます。あとはconsole.logメソッドで表示するだけです。ここまでをまとめると以下のようになります:

これで複素数の文字列表現が表示されるはずです。

また、クラスを使う場合は以下のようになります:

例題3

例題:生徒の名前と数学の点数を格納した、以下の配列が与えられているとする:

このとき各生徒の成績を「田中の数学の点数は67点です」などと表示せよ。

この例題は、配列の中にオブジェクトが入っています。よくある問題です。こういうときはまずfor文か何かでひとつずつオブジェクトを取り出します。そして各オブジェクトに対して処理を行います。せっかくなので今回はfor-ofを使ってみましょう。以下のようにします:

for-ofによって、変数studentに配列scoresの値(オブジェクト)が入ります。あとはそのオブジェクトからnameとmathを取り出し表示するだけです。for-ofによってこれが3回繰り返されます。

これで各生徒のデータが表示できます。

課題

課題1(homework3-1.js)

課題:数値のみを含む、同じ長さの配列arrayAとarrayBが以下のように与えられているとする:

このときarrayAのi番目とarrayBのi番目を掛けた数値を、新しい配列prodのi番目に格納して、配列prodをコンソールに表示せよ。また、arrayAとarrayBの値や長さをいろいろ変えて、計算結果が正しいことを確かめよ。

  • ヒント1:配列prodの作り方は、あらかじめarrayA.lengthの長さを持つ配列を作るか、空の配列を作成してpushしていくか、好きに選ぶと良い。
  • ヒント2:例えばprod[0]にはarrayA[0] * arrayB[0]が入る。

課題2(homework3-2.js)

課題:プロパティfirstNameとプロパティlastName、およびプロパティfullNameを持つPersonクラスを作成せよ。次にPersonクラスのインスタンスを作成し、fullNameをコンソール上に表示せよ。

ヒント:Personクラスのコンストラクタはconstructor(firstName, lastName, fullName)でもいいが、constructor(firstName, lastName)にして、fullNameはlastNameとfirstNameの結合で算出することもできる。

課題3(homework3-3.js)

課題:課題2で作成したPersonクラスのインスタンスを、配列に3つほど格納せよ。また、格納したインスタンスのfullNameを順にコンソール上に表示せよ。

応用課題

応用課題1(homework3-4.js)

課題:配列はその中に配列を含むことができる。このときの配列を二次元配列という。二次元配列を用いれば数学における行列を表現できる。例えば以下のように作成できる:

二次元配列を利用して、適当な数値を含む2×2の行列matrix1とmatrix2を作成し、その和であるmatrixSumを求め、コンソール上に表示せよ。

  • ヒント1:二次元配列matrixがあるとき、row行目の配列はmatrix[row]である。row行目の配列のcolumn列目はmatrix[row][column]になる。
  • ヒント2:行列の足し算の方法はベクトルと同じである。つまり、同じ位置の値を足せば良い。
  • ヒント3:表示は以下のように簡易的なもので良い:

実行例:

  • matrix1 = [[1, 2], [3, 4]], matrix2 = [[5, 6], [7, 8]]のとき:matrixSum → [[6, 8], [10, 12]]

応用課題2(homework3-5.js)

課題:プロパティxとプロパティyを持つ、二次元ベクトルを表すVector2クラスを作成せよ。このときVector2クラスは、他のVector2インスタンスとの和を計算して新しいVector2インスタンスを返すadd(other)メソッドと、自身の文字列表現を返すtoString()メソッドを持つこととする。また、実際にいくつかVector2インスタンスを作り、和の計算結果が正しいことを確認せよ。

  • ヒント1:add(other)メソッドは、x同士を足したnxとy同士を足したnyを求め、return new Vector2(nx, ny);とすれば良い。
  • ヒント2:toString()メソッドが返す文字列表現の内容は自由で良い。ただしxとyの値がわかるようにすること。

その4:オブジェクト指向へ続く

次の記事 → その4:オブジェクト指向

わからないときは

この連載記事を読んで、わからないところ、不思議なところ、納得のいかないところ等出てくると思います。そのときはTwitterで@kfurumiyaまでご連絡ください。できる範囲で回答いたします。

また、Twitterを使えない、Twitterでは短すぎるという場合はメールアドレス:kotofurumiya@gmail.comまでご連絡ください。

私を信用できないという場合は、以下のプログラミング質問サイトを活用してください: