はじめに
複数のデータをまとめたデータ構造を定義する際、配列の他にもオブジェクトを利用することができます。JavaScriptではほとんどのデータがオブジェクトとして定義されていますが、オブジェクトは変数として自分で定義することもできます。オブジェクトはJavaScriptの根幹たる要素なので、オブジェクトの理解はJavaScriptを学ぶ上でとても重要です。
この記事では、JavaScriptにおいてのオブジェクトの重要な要素について解説していきます。最も基本となる、オブジェクトの定義方法やプロパティの追加や更新・削除といった操作方法について解説し、オブジェクトと関連性の高い「コンストラクタ関数」と「クラス」について触れ、オブジェクトのデータ構造を再利用する方法について学びます。最後に、複雑なデータ構造を表現する際にどのようにオブジェクトと配列を使い分けるかについて、これらの基本的な特徴に焦点を当てて解説します。
この記事の対象者
- JavaScript初学者の方
- JavaScriptでのオブジェクトについて学びたい方
- 配列とオブジェクトの違いについて学びたい方
この記事で学べること
- オブジェクトの定義方法
- オブジェクトの操作方法
- オブジェクトのデータ構造を再利用する方法
- オブジェクトと配列の基本的な選び方
オブジェクトの基本
プログラミングにおいて一定の構造を持つデータを定義する際、配列や連想配列というものが使用され、JavaScriptにおいてのオブジェクトは連想配列に該当します。
配列は各データに対して自動で割り振られるインデックスを通じてデータにアクセスすることができます。それに対し、連想配列は各データに対して「キー」という任意の名前を付け、キーを指定することで連想配列内の特定のデータにアクセスします。
複数のデータをまとめるという点では配列と連想配列は同じ役割を持ちますが、JavaScriptにおいての連想配列はObjectというオブジェクトとして扱われ、Arrayオブジェクトである配列とは大きく異なる特徴があります。それを理解するために、まずはオブジェクトの定義方法やデータの追加・変更といった基本から学んでいきましょう。
オブジェクトの定義
JavaScriptでオブジェクトを定義する際の基本は、オブジェクトリテラルを使用した定義方法です。配列の定義では「 [] 」を使用しますが、オブジェクトの定義では「 {} 」を使用します。{}内にはキーという任意の名前と、それに対応するデータを記述していきます。以下が例です。
// {キー: 値}
let person = {name: '三郎', age: 22};
キーの末尾には「 :(コロン)」が必要です。カンマ区切りで複数のデータを記述していくところは配列と同じです。上記のように一行で定義するとデータの数が多い場合に可読性が悪くなるので、基本的には改行を入れます。
let person = {
name: '三郎', // nameプロパティ
age: 22 // ageプロパティ
};
プロパティとメソッド
オブジェクトにおいて、キーと値のペアを「プロパティ」と言います。また、値として関数を持っているプロパティを「メソッド」と言います。JavaScriptで定義されている多くのオブジェクトと同様、自分で定義したオブジェクト内のデータへのアクセスも「 .(ドット)」繋ぎで行うことができ、プロパティの値を呼び出したり、メソッドを実行するといったことができます。
let person = {
name: '三郎',
age: 22
};
console.log(person);
// 結果:{name: "三郎", age: 22}
console.log(person.name);
// 結果:三郎
console.log(person.age);
// 結果:22
「 . 」繋ぎではなく、配列のように「 [] 」でアクセスすることもできます。配列では[]内にインデックスを記述しますが、オブジェクトの場合はキー名を文字列として記述します。これを「ブラケット記法」といいます。
let person = {
name: '三郎',
age: 22
};
// ブラケット記法です
console.log(person['name']);
// 結果:三郎
console.log(person['age']);
// 結果:22
メソッドについても見ておきましょう。greetという名前のメソッドをpersonオブジェクトに追加します。
let person = {
name: '三郎',
age: 22,
greet: function() {
console.log('こんにちは!私は' + this.name + 'です!');
}
};
person.greet();
// 結果:こんにちは!私は三郎です!
メソッドを定義する際は「this」という特殊なオブジェクトがよく使われます。thisは動的に変化するオブジェクトで、どこでthisを呼び出すかによってthisとして扱われるオブジェクトが変わり、上記の場合はpersonオブジェクトを示しています。thisは省略できますが、名前が衝突する可能性があるので推奨されません。
let person = {
name: '三郎',
age: 22,
greet: function() {
// thisは省略できますが推奨されません
console.log('こんにちは!私は' + name + 'です!');
}
};
person.greet();
// 結果:こんにちは!私は三郎です!
例として、greet()メソッドにnameプロパティと同じ名前の引数を指定して意図的に衝突させてみます。
let person = {
name: '三郎',
age: 22,
greet: function(name) {
console.log('こんにちは!私は' + name + 'です!');
}
};
person.greet();
// 結果:こんにちは!私はundefinedです!
person.greet('四郎');
// 結果:こんにちは!私は四郎です!
上記の例では、nameプロパティの値よりもgreet()メソッドの引数のほうが優先されています。「 person.greet(); 」の結果が「こんにちは!私はundefinedです!」となることから、console.log()実行時のnameが、personオブジェクトのnameプロパティではなくgreet()メソッドの引数を参照していることが分かります。
let person = {
name: '三郎', // このnameは使われない
age: 22
greet: function(name) { // 引数で受け取るnameが使われる
console.log('こんにちは!私は' + name + 'です!');
}
};
オブジェクトの操作
オブジェクトには、新しいプロパティを追加したり、不要なプロパティを削除することができます。ここでは、オブジェクトの基本的な操作としてプロパティの追加・更新・削除する方法について学び、for文の一種である「 for…in 」を使用したオブジェクトの反復処理について解説します。
プロパティの追加と更新
既存のオブジェクトに新しいプロパティを追加するには、「 . 」繋ぎでキー名を指定し、それに対応する値を代入します。
let person = {
name: '三郎',
age: 22
};
// addressプロパティを追加
person.address = '東京都';
console.log(person);
// 結果:{name: '三郎', age: 22, address: '東京都'}
キー名が既存のオブジェクトのプロパティに既に定義されている場合は、「追加」ではなく「更新」という扱いになります。
let person = {
name: '三郎',
age: 22
};
// nameプロパティを更新
person.name = '四郎';
console.log(person);
// 結果:{name: '四郎', age: 22}
プロパティの削除
不要になったプロパティを削除するには、「 delete 」というキーワードを使用します。
let person = {
name: '三郎',
age: 22,
address: '東京都'
};
// addressプロパティを削除
delete person.address;
console.log(person);
// 結果:{name: '三郎', age: 22}
オブジェクトのループ処理
オブジェクトというデータ構造は、ループ処理と組み合わせることでコードの可読性や保守性の向上に大きく貢献します。オブジェクトを効果的に扱う基本として、ループ処理でオブジェクト内の各プロパティへアクセスし、各プロパティの値を使用して何らかの処理を行うことは一般的です。この方法について解説します。
基本的なfor文でのループ処理
基本的なfor文でオブジェクトのループ処理を行うには、配列の場合と似通いながらも少し工夫をする必要があります。配列はArrayオブジェクトの特徴としてlengthプロパティを持っていますが、Objectにはlengthプロパティが定義されていません。
let person = {
name: '三郎',
age: 22
};
console.log(person.length);
// 結果:undefined
上記のように、lengthプロパティにアクセスしてもundefinedが返ってきます。つまり、配列のようにループ処理を実装することはできません。
let person = {
name: '三郎',
age: 22,
};
// i < person.lengthはfalseのため実行されません
for (let i = 0; i < person.length; i++) {
// ループ処理
}
上記のfor文の条件式は、配列の各データに対してループ処理を行う基本的な例です。personはオブジェクトであり、person.lengthはundefinedのため「条件が満たされない」と解釈されて{}ブロック内のコードは実行されません。
オブジェクトに対して上記のような条件式でループ処理を行うには、Objectに定義されているkeys()メソッドを利用する方法が考えられます。keys()メソッドは、オブジェクト内の各プロパティのキーを配列として返します。以下はkeys()メソッドの使用例です。
let person = {
name: '三郎',
age: 22,
address: '東京都'
};
// オブジェクトのキーを配列として取得
let keys = Object.keys(person);
console.log(keys);
// 結果:['name', 'age', 'address']
keys()メソッドを使用することで、配列に近い感覚でオブジェクトのループ処理を実装できます。
let person = {
name: '三郎',
age: 22,
address: '東京都'
};
let keys = Object.keys(person); // ['name', 'age', 'address']
for (let i = 0; i < keys.length; i++) {
let key = keys[i];
console.log(key + ': ' + person[key]);
}
/*
* 結果:
* name: 三郎
* age: 22
* address: 東京都
*/
しかし、上記のコードはperson変数とkeys変数が別々で定義されているため、keys変数内に格納した配列に対して破壊的メソッドで変更を加えると予期せぬ結果になる可能性があり、その対策のために更にいろいろと工夫する必要が出てくるなど、将来的には保守性において問題が起きる可能性があることに注意が必要です。
ちなみに、オブジェクトのループ処理はドット記法では各プロパティにアクセスできません。必ずブラケット記法を使用します。
for (let i = 0; i < keys.length; i++) {
let key = keys[i];
// person[key]をperson.keyに変更
console.log(key + ': ' + person.key);
}
/*
* 結果:
* name: undefined
* age: undefined
* address: undefined
*/
for…inでのループ処理
オブジェクトのループ処理には、「 for…in 」という少し変わったfor文を使用することができ、for…inでループ処理を実装することにより簡潔な記述になります。以下はfor…inの使用例です。
let person = {
name: '三郎',
age: 22,
address: '東京都'
};
// for...inの基本構文です
for (let key in person) {
console.log(key + ': ' + person[key]);
}
/*
* 結果:
* name: 三郎
* age: 22
* address: 東京都
*/
for…inでは、初期化用の変数(ループカウンタ変数)を定義するところは通常のfor文と同じです。大きな違いは、for…inでは条件式を指定せず、代わりにinというキーワードが条件式の役割を持っています。初期化変数にはオブジェクトのキー名が入り、それを使用することでオブジェクトの各プロパティにアクセスします。
// (let 初期化変数 in 対象のオブジェクト)
for (let key in person) {
console.log(key + ': ' + person[key]);
}
for…inによるループ処理は、通常のfor文のように条件式を指定しないため内部で行われている処理が分かりにくく、ループの順番が保証されないという問題も挙げられます。しかし、for…inは簡潔な記述になるため初学者にとっても扱いやすいと思います。for…in以外にも、fJavaScriptでのループ処理の実装には様々な方法があります。
オブジェクトのデータ構造を再利用する
オブジェクトのデータ構造は「クラス」として定義することで再利用することができるようになります。ここではその方法について解説します。
まず前提として、JavaScriptではObjectというコンストラクタ関数を使用することでもオブジェクトを定義できます。
let parson = new Object({
name: '三郎',
age: 22
});
上記のように、newキーワードを使用してオブジェクトを生成することを「インスタンス化」と言います。また、生成されたオブジェクトは「インスタンス」と表現されます。コンストラクタ関数を使用して生成されたインスタンスは、プリミティブ型のデータであってもオブジェクトとして扱われます。
// こっちはstring型
let str1 = 'Hello';
console.log(typeof str2); // 結果:string
// こっちはobject型
let str2 = String('Hello');
console.log(typeof str2); // 結果:object
コンストラクタ関数は自分で定義することができ、これによりオブジェクトのデータ構造を使いまわすことができます。コンストラクタ関数の関数名は先頭を大文字にすることが慣習です。
// コンストラクタ関数を定義
function Parson(name, age) {
this.name = name;
this.age = age;
console.log(this);
}
// インスタンス化
let parson1 = new Parson('三郎', 22);
let parson2 = new Parson('四郎', 20);
console.log(parson1); // {name: '三郎', age: 22}
console.log(parson2); // {name: '四郎', age: 20}
メソッドの定義の解説でthisオブジェクトについて触れましたが、コンストラクタ関数でもthisはよく使われます。上記の例では、Person()コンストラクタ関数内でthisを呼び出しています。この場合のthisは、Person()を使用してインスタンス化を行った場合のインスタンスを指しています。
このように、オブジェクトのデータ構造を再利用できるようにすることを「クラス化」と言います。クラスは「オブジェクトの設計書」と表現することができ、オブジェクト指向プログラミングにおいての基本的な概念です。
コンストラクタ関数はJavaScriptの特徴的な要素の一つです。他のプログラミング言語では「 class 」というキーワードを使用してクラス定義を行いますが、JavaScriptは歴史的な経緯により、classキーワードを用いたクラス定義を記述することができませんでした。コンストラクタ関数はクラス定義を模倣したものになります。
JavaScriptではクラス定義ができないというのは昔の話で、最近のモダンJavaScriptではclassキーワードを使用したクラス定義を行うことができます。上記のコンストラクタ関数を以下のように書き換えることができます。
// クラスを定義
class Parson {
constructor(name, age) {
this.name = name;
this.age = age;
}
}
// インスタンス化
let parson1 = new Parson('三郎', 22);
let parson2 = new Parson('四郎', 20);
console.log(parson1); // {name: '三郎', age: 22}
console.log(parson2); // {name: '四郎', age: 20}
インスタンス化の方法はコンストラクタ関数と同じです。クラス定義では「 constructor 」という特別なメソッドを定義します。constructor()メソッドはインスタンス化の再に自動的に呼び出され、引数はインスタンス化を行う際の引数に対応しています。これによりインスタンスの初期化が行われます。
class Parson {
constructor(name, age) {
this.name = name;
this.age = age;
}
}
/* 第一引数の'三郎'がconstructorのnameに渡り、
* 第二引数の22がconstructorのageに渡ります */
let parson1 = new Parson('三郎', 22);
classキーワードを使用したクラス定義とコンストラクタ関数では特徴的な違いがあります。
JavaScriptでは、functionキーワードを使用して関数を定義した場合はコードの巻き上げ(ホイスティング)が発生します。通常では、コードは上から順に解析されます。しかし、ホイスティングの仕組みによって関数の記述場所に関わらずその関数を呼び出すことができます。
console.log(myFunction()); // 結果:true
function myFunction() {
return true;
}
コンストラクタ関数はfunctionキーワードを用いるためホイスティングが発生しますが、classキーワードを使用した場合はホイスティングが発生しません。以下の場合はエラーになります。
let parson1 = new Parson('三郎', 22);
let parson2 = new Parson('四郎', 20);
class Parson {
constructor(name, age) {
this.name = name;
this.age = age;
}
}
// Uncaught ReferenceError: Cannot access 'Parson' before initialization
at script.js:1:15
オブジェクトの基本は以上です。続いて、複雑なデータ構造を定義する際にどのようにしてオブジェクトと配列を使い分けるか、基本的な考え方について解説します。
オブジェクトと配列の違い
JavaScriptにおいてオブジェクトと配列は「複数のデータをまとめる」という点では同じ役割を持ちますが、これらは異なるデータ構造であり、それぞれ独自の特性を持っています。オブジェクトと配列がどのように異なるのかを理解することは、データ構造を適切な形で定義するための助けとなり、効率的なプログラムを実装するうえで重要です。
オブジェクトと配列の基本的な違い
オブジェクトでデータ構造を定義する場合、「キー」という形で明示的にデータの内容を示すことができるため、一般的にはコードの可読性が向上します。
しかし、前述のとおりループ処理を実装する際には少し工夫が必要であるうえ、オブジェクトの各プロパティは配列のようにインデックスがありません。これは、ループ処理の際にどのような順序で実行されるかが明示的ではないことを示し、順序に依存する操作を行う際に予期せぬ挙動を起こす可能性があります。
JavaScriptでは「プロトタイプチェーン」という仕組みによって特定のオブジェクトに他のオブジェクトのプロパティやメソッドを継承させることができますが、ループ処理の対象となるオブジェクトがプロトタイプチェーンによる継承を行っている場合、継承元となるオブジェクトのプロパティやメソッドも参照することがあり、余計な処理を実装しなければいけなくなる可能性があります。
一方、配列のデータ構造はインデックスによって配列内のデータの順序が保証されており、順序に依存する操作はオブジェクトよりも簡単に行うことができます。しかし、破壊的メソッドによって配列内のデータが削除された場合はインデックスがズレる可能性があるため、もとめているデータを取得できない場合があります。
// オブジェクトと配列で同じデータを表現しています
let obj = {
name: '三郎',
age: 22
};
let arr = [
'三郎',
22
];
// それぞれの'三郎'を削除しています
delete obj.name;
arr.shift();
// 配列はインデックスばズレるので
// 元のインデックスではアクセスできません
console.log(obj.age); // 結果:22
console.log(arr[1]); // 結果:undefined
オブジェクトと配列の使い分け方
複雑なデータ構造を定義する際にオブジェクトと配列のどちらを使うかは、初学者の方にとって判断が難しいと思います。オブジェクトと配列の利用シーンに焦点を当て、簡単な判断方法について解説します。
オブジェクトの利用シーン
配列はデータのインデックスがズレる可能性がある一方、オブジェクトの場合はキーを特定した記述になるので、明示的にプロパティを削除しない限りはデータへのアクセスが保証されます。
また、異なる型のデータ(文字列型、数値型、関数型など)をまとめる場合も基本的にはオブジェクトが選ばれます。これは、オブジェクトにループ処理を実装する際は各プロパティのデータ型を意識する必要があることを示します。
つまり、「データへのアクセスが主な目的」であればオブジェクトが適していると考えることができます。
配列の利用シーン
配列の特徴は、各データにインデックスが割り当てられることによりデータの順序を制御することが簡単になるところです。複数のデータをインデックス順に処理したり、並べ替えたりといったことができます。
一般的には配列内には同じ型のデータや、同じ特徴を持つ具体的なデータ(果物の名前など)をまとめて格納します。これは、オブジェクトに比べてループ処理の実装が簡単になることを示しています。また、配列では多くのループ処理用のメソッドが使用できますが、これらはArrayオブジェクトによって提供されているものなのでオブジェクトでは基本的には使用できません。
つまり、「ループ処理が主な目的」であれば配列が適していると考えることができます。
オブジェクトや配列はそれぞれ利点や制約があり、漠然とした選択ではなく根拠を持ってこれらを使い分けることは、効率良くプログラミングを学ぶために重要だと思います。
最後に
この記事では、JavaScriptにおいてのオブジェクトの重要な要素として、オブジェクトの定義や操作方法について学び、コンストラクタ関数やclassキーワードを用いてデータ構造を再利用する方法について解説しました。
オブジェクトの定義や操作方法はこれまででJavaScriptに触れている方であれば難しくないかと思います。オブジェクト指向プログラミングにおいての基本としてクラスについての解説を含めましたが、コンストラクタ関数やクラスについては初学者の方が扱うには複雑な要素かもしれません。
プログラミングは基本的に難しいものであり、学習は「1.学ぶ → 2.忘れる → 3.調べる → 4.思い出す」を繰り返すことで記憶として定着していきます。理解に苦しむことがあってもこだわりすぎず、「このようなものがある」と割り切って学習を継続する方が、「聞いたことある・見たことある・調べたことある」が増えて、結果的にプログラミングの理解が深まるかと思います。
クラスについての理解は、アプリケーション開発やJavaScriptライブラリの開発といった中規模以上の開発を行う場合は特に重要になります。
しかし、HTML・CSSによる基本的なウェブサイトの制作であれば小規模なプログラムを実装することが多く、クラスについて深い理解がなくても多くの機能を実装する力を身に付けることができます。ですので、DOM操作やwindowオブジェクト、documentオブジェクトなどの基本的なものに焦点を当てる方がJavaScriptを楽しく学ぶことができると思います。