Day 74:オブジェクト指向の入口
Day 74では、JavaScriptのclass、constructor、メソッドを使って、同じ種類のデータと処理をひとまとめにする方法を学びます。実践では、ユーザーを表すUserクラスと、商品を表すProductクラスを作ります。
Day 73では、何度も使う処理を関数にまとめる方法を学びました。今回はその考え方を少し広げ、関連するデータと処理を「オブジェクト」として扱います。たとえば、ユーザーには名前やメールアドレスがあり、自己紹介文を作る処理もあります。商品には名前や価格があり、税込価格を計算する処理もあります。こうした関連する情報と処理を同じまとまりとして設計すると、コードの意味がわかりやすくなります。
オブジェクト指向という言葉は難しく見えますが、最初は「同じ種類のものを、同じ設計図から作る方法」と考えれば十分です。設計図にあたるものがclassで、そこから作られた個々のデータがインスタンスです。今回は、まずこの関係をつかみ、コードを少しずつ書いていきます。
まず結論をつかむ
今回学ぶ要素を簡単に整理すると、次の通りです。
- `class`:オブジェクトを作るための設計図です。
- `constructor`:新しいオブジェクトを作るときに、最初の値を設定する特別なメソッドです。
- メソッド:オブジェクトに関連付けられた関数です。
- インスタンス:`class`をもとに作られた個々のオブジェクトです。
- `this`:メソッドの中で、そのインスタンス自身を参照するときに使います。
たとえば、Userクラスを設計図として作ると、そこから異なる名前やメールアドレスを持つユーザーを複数作れます。
オブジェクトを設計図にたとえる
クラスとインスタンスの関係は、設計図と製品にたとえると理解しやすいです。たとえば、家の設計図があれば、その設計図をもとに住所や色が異なる家を複数建てられます。同じように、Userクラスを作れば、そのクラスをもとに名前やメールアドレスの異なるユーザーを作れます。
- `User`クラス:ユーザーを作るための設計図
- ユーザーのインスタンス:設計図から作った個々のユーザー
- プロパティ:名前やメールアドレスなど、そのユーザーが持つ情報
- メソッド:自己紹介を作るなど、そのユーザーに関係する処理
クラスは「データを入れる箱」だけではありません。そのデータに関係する処理も一緒に持たせられます。
ステップ1:まず普通のオブジェクトを振り返る
クラスの前に、オブジェクトリテラルを振り返ります。
// 1人分のユーザー情報をオブジェクトで表す
const user = {
name: "Aki",
email: "[email protected]",
// このユーザーの情報を使って自己紹介を返す
introduce: function () {
return `こんにちは、${this.name}です。`;
}
};
console.log(user.name); // "Aki"
console.log(user.introduce()); // "こんにちは、Akiです。"JavaScriptこのオブジェクトには、データであるnameとemail、処理であるintroduce()が入っています。this.nameは、このメソッドを呼び出したオブジェクトのnameを参照します。
このようなオブジェクトを1つだけ作るのであれば、オブジェクトリテラルで十分です。しかし、同じ形のユーザーを何人も作る場合、毎回同じ構造を書くのは手間です。そこでクラスを使います。
ステップ2:Userクラスを作る
classキーワードで、ユーザーの設計図を作ります。
// ユーザーを作るためのクラス
class User {
// インスタンス作成時に初期値を受け取る
constructor(name, email) {
this.name = name;
this.email = email;
}
// ユーザーの自己紹介文を返す
introduce() {
return `こんにちは、${this.name}です。`;
}
}JavaScriptconstructorは、クラスから新しいインスタンスを作るときに自動的に実行されます。ここでは、引数で受け取ったnameとemailを、作られるユーザーのプロパティとして設定しています。
this.name = nameは、右側のnameを左側のthis.nameに保存する処理です。右側のnameはconstructorの引数、左側のthis.nameは新しいインスタンスが持つプロパティです。
ステップ3:newを使ってインスタンスを作る
クラスからオブジェクトを作るときは、newを使います。
// Userクラスから2人のユーザーを作る
const user1 = new User("Aki", "[email protected]");
const user2 = new User("Mika", "[email protected]");
// それぞれのインスタンスが持つプロパティを確認する
console.log(user1.name); // "Aki"
console.log(user2.email); // "[email protected]"
// インスタンスのメソッドを呼び出す
console.log(user1.introduce()); // "こんにちは、Akiです。"
console.log(user2.introduce()); // "こんにちは、Mikaです。"JavaScriptnew User(...)を実行するたびに、新しいユーザーインスタンスが作られます。同じUserクラスから作られていても、それぞれが別の名前やメールアドレスを持てます。
constructorの役割をもう少し詳しく見る
constructorは、インスタンスが最初から必要なデータを持つようにするために使います。たとえば、Userを作るときに名前とメールアドレスを受け取る設計にしておけば、次のように作成できます。
// 名前とメールアドレスを渡してユーザーを作る
const user = new User("Ren", "[email protected]");JavaScriptもしconstructorで値を設定しなければ、インスタンス作成後に別々に代入することもできますが、設定漏れが起きやすくなります。
// 設定漏れが起きやすい作り方
const user = {};
user.name = "Ren";
user.email = "[email protected]";JavaScriptconstructorを使うと、「このクラスのインスタンスを作るには何が必要か」がわかりやすくなります。初心者の方は、クラスにとって必要な初期データをconstructorで受け取る、と考えるとよいです。
メソッドとは何か
メソッドは、クラスの中に定義する関数です。たとえば、ユーザーの自己紹介を作るintroduce()がメソッドです。
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
// メソッドはクラス内に定義する
introduce() {
return `${this.name}(${this.email})`;
}
}JavaScriptメソッドは、そのクラスが持つデータを使った処理を書くのに向いています。ユーザーの自己紹介はユーザー情報を使う処理なので、Userクラスのメソッドにするのが自然です。
thisが指すもの
thisは、メソッドが呼び出されたときに、そのメソッドを持つインスタンスを参照します。
const user = new User("Yui", "[email protected]");
// introduce()の中のthisは、このuserを指す
console.log(user.introduce());JavaScriptこのとき、introduce()の中のthis.nameはuser.nameを意味します。別のインスタンスから呼べば、そのインスタンス自身の情報が使われます。
初心者の方は、クラスのメソッド内にあるthisを「今この処理を呼び出しているインスタンス」と覚えるところから始めるとよいです。アロー関数などではthisの扱いが異なる場合がありますが、まずはクラスの通常のメソッドで基本を理解しましょう。
ステップ4:Productクラスを作る
次は商品を表すProductクラスを作ります。商品名と価格を持ち、価格を表示するメソッドと、割引後の価格を計算するメソッドを用意します。
// 商品を作るためのクラス
class Product {
// 商品名と価格を受け取り、プロパティに保存する
constructor(name, price) {
this.name = name;
this.price = price;
}
// 商品名と価格を読みやすい文字列にして返す
getSummary() {
return `${this.name}: ${this.price}円`;
}
// 割引率を受け取り、割引後の価格を返す
getDiscountedPrice(discountRate) {
return Math.round(this.price * (1 - discountRate));
}
}JavaScriptこのクラスでは、商品に関係するデータと処理をまとめています。商品名や価格を持つだけでなく、その商品についての表示文や割引価格も計算できます。
ステップ5:Productのインスタンスを作って使う
Productクラスから、商品を作ってみます。
// Productクラスから商品を作る
const product1 = new Product("ノート", 300);
const product2 = new Product("ペン", 150);
// 商品情報を表示する
console.log(product1.getSummary()); // "ノート: 300円"
console.log(product2.getSummary()); // "ペン: 150円"
// 20%割引後の価格を計算する
console.log(product1.getDiscountedPrice(0.2)); // 240JavaScriptgetDiscountedPrice(0.2)では、割引率として0.2、つまり20%を渡しています。割引率を20として渡す設計にするなら計算式も変える必要があります。引数の単位や意味を明確に決め、呼び出す側と関数の中で一致させることが大切です。
入力値を検証する
実際のプログラムでは、クラスへ不正な値が渡されることがあります。たとえば、商品価格に文字列や負の数が入ると、計算結果が意図しないものになるかもしれません。constructorで基本的な確認を行う方法があります。
class Product {
constructor(name, price) {
// 商品名が空でないことを確認する
if (typeof name !== "string" || name.trim() === "") {
throw new TypeError("商品名を入力してください。");
}
// 価格が0以上の有限な数値であることを確認する
if (typeof price !== "number" || !Number.isFinite(price) || price < 0) {
throw new TypeError("価格には0以上の数値を指定してください。");
}
// 入力を確認したあとでプロパティへ保存する
this.name = name.trim();
this.price = price;
}
// 商品情報を文字列として返す
getSummary() {
return `${this.name}: ${this.price}円`;
}
// 割引率は0から1までの数値として受け取る
getDiscountedPrice(discountRate) {
if (
typeof discountRate !== "number" ||
!Number.isFinite(discountRate) ||
discountRate < 0 ||
discountRate > 1
) {
throw new RangeError("割引率は0以上1以下で指定してください。");
}
return Math.round(this.price * (1 - discountRate));
}
}JavaScript入力チェックを入れると、間違った値がオブジェクトの中へ入る前に問題を検出できます。ただし、どの入力を許可するかはアプリの仕様次第です。たとえば無料の商品を扱うなら価格0を許可しますし、小数価格を扱うかどうかも決めておく必要があります。
UserとProductを一緒に使う例
クラスから作ったインスタンスは、配列にも入れられます。
// Userクラスからユーザーを作る
const users = [
new User("Aki", "[email protected]"),
new User("Mika", "[email protected]")
];
// Productクラスから商品を作る
const products = [
new Product("ノート", 300),
new Product("ペン", 150)
];
// ユーザーの自己紹介を表示する
users.forEach(function (user) {
console.log(user.introduce());
});
// 商品の情報を表示する
products.forEach(function (product) {
console.log(product.getSummary());
});JavaScriptこのように、同じ種類のインスタンスを配列にまとめると、forEach()などの配列処理と組み合わせて扱えます。
Day 71のES Modulesと組み合わせる
クラスもファイルに分けて、必要な場所から読み込めます。たとえば、次のように配置します。
project/
├── index.html
└── js/
├── app.js
├── User.js
└── Product.js
User.js:
// Userクラスを別ファイルから使えるようにする
export class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
// 自己紹介文を返す
introduce() {
return `こんにちは、${this.name}です。`;
}
}JavaScriptProduct.js:
// Productクラスを別ファイルから使えるようにする
export class Product {
constructor(name, price) {
this.name = name;
this.price = price;
}
// 商品情報を返す
getSummary() {
return `${this.name}: ${this.price}円`;
}
}JavaScriptapp.js:
// 各ファイルからクラスを読み込む
import { User } from "./User.js";
import { Product } from "./Product.js";
// クラスからインスタンスを作る
const user = new User("Aki", "[email protected]");
const product = new Product("ノート", 300);
// メソッドを呼び出して結果を確認する
console.log(user.introduce());
console.log(product.getSummary());JavaScriptHTMLでは、Day 71と同様にtype="module"を指定します。
<!-- app.jsをESモジュールとして読み込む -->
<script type="module" src="./js/app.js"></script>
クラスと関数はどちらを使うのか
すべての処理をクラスで書く必要はありません。ひとつの値を変換するだけの処理なら、Day 73で学んだUtility関数のほうが簡潔な場合があります。複数のインスタンスを作り、それぞれがデータと関連する処理を持つ場合には、クラスが役立ちます。
- 日付を文字列に変換するだけなら、関数が向いています。
- 複数のユーザーを作り、それぞれに名前や自己紹介処理を持たせるなら、クラスが向いています。
- 短い処理を無理にクラス化すると、コードがかえって複雑になることがあります。
初心者の方は、「クラスを使うことが目的」ではなく、「関連するデータと処理をまとめる必要があるか」で判断しましょう。
よくあるつまずき
Day 74では、次のような間違いが起きやすいです。
- `new`を付けずにクラスを呼び出す
- `constructor`の引数の順番と、インスタンス作成時に渡す順番を間違える
- `this.name`ではなく`name`だけを使い、プロパティに保存できていない
- メソッドを呼び出すときの`()`を忘れる
- 割引率など、引数の単位や範囲を決めないまま使う
エラーが出たときは、クラスの定義、newで渡した値、constructor内の代入、メソッド呼び出しの順に確認すると原因を見つけやすくなります。
セキュリティとデータの扱い
クラスを使っても、入力値が自動的に安全になるわけではありません。ユーザー名や商品名など、外部から受け取った文字列を画面に表示するときは、DOMのtextContentを使うのが基本です。また、価格などの数値は、型や範囲を確認してから計算します。
オブジェクトにデータを保存することと、そのデータが正しいか確認することは別の作業です。constructorで検証を行う設計にする場合も、アプリ全体の入力チェックと役割が重複しすぎないように、どこで何を確認するかを決めておきましょう。
練習課題
基本のコードを動かしたら、次の課題に挑戦してみましょう。
- `User`に`getEmailDomain()`メソッドを追加し、メールアドレスのドメイン部分を返します。
- `Product`に在庫数を追加し、在庫があるかを判定するメソッドを作ります。
- `Product`に消費税を含む価格を返すメソッドを追加します。
- 名前や価格に不正な値を渡したとき、どんなエラーになるか確認します。
- `User`と`Product`を別々のファイルへ分け、ES Modulesで読み込みます。
課題を進めるときは、各クラスが持つデータと、そのデータに関係するメソッドを先に紙やコメントで整理してから実装すると、設計しやすくなります。
Day 74のゴール
Day 74では、次のことができるようになっていれば十分です。
- `class`を設計図として説明できる
- `constructor`で初期値を設定できる
- `new`で複数のインスタンスを作れる
- メソッドと`this`の基本を理解できる
- `User`や`Product`のようなクラスを設計できる
- クラスと通常の関数を使い分ける意識を持てる
まとめ
Day 74では、class、constructor、メソッドを使って、関連するデータと処理をひとまとめにする方法を学びました。Userクラスではユーザー情報と自己紹介、Productクラスでは商品情報と価格計算を扱いました。クラスは設計図であり、newで作られる個々のオブジェクトがインスタンスです。constructorは初期値を設定し、メソッドはインスタンスの情報を使った処理を担当します。
大切なのは、すべてをクラスで書くことではありません。複数の同じ種類のオブジェクトを作り、それぞれに関連するデータと処理を持たせたいときにクラスを使います。今回のUserやProductを少しずつ拡張しながら、データと処理を自然なまとまりに設計する感覚を身につけていきましょう。
