テキスト:JavaScriptでオブジェクト指向
オブジェクト指向とは?
コンピュータプログラムの設計や実装についての考え方の一つで、互いに密接に関連するデータと手続き(処理手順)をオブジェクト(object)と呼ばれる一つのまとまりとして定義し、様々なオブジェクトを組み合わせて関連性や相互作用を記述していくことによりシステム全体を構築していく手法。
プログラミング言語の多くは(程度の差こそあれ)オブジェクト指向プログラミングをサポートしている。
大抵の言語にはある(学ぶ意義の一つでもある)
オブジェクト指向を学ぶ意義(個人的な意見)
オブジェクト指向ばかりが使われているかというと、そういうわけではないとは思う。
オブジェクト指向をしっかりやろうとすると、深い仕様の理解が必要である印象。
さまざまな設計手法を紐解く鍵となる基礎。
「抽象化」の流れを考えるいい練習になる。
例1
デスクトップに「study」というフォルダを作成
(既に作成ずみの場合は飛ばしてください)
↓
「study」ファルダの中に「object」というフォルダを作成
↓
「object」フォルダの中に「cat.js」という名前でファイルを作成
↓
VScodeで「object」フォルダを開く
CUI
code:console
$ mkdir study
$ cd study
$ mkdir object
$ cd object
$ touch cat.js
$ code ./
code:cat.js
// クラス
class Cat {
// コンストラクタ
constructor() {}
name;
purr() {
console.log(this.name + 'が鳴く');
}
sleep() {
console.log(this.name + 'は眠る');
}
}
// 利用する処理
// インスタンス化
const mike = new Cat();
mike.name = 'ミケ';
const taro = new Cat();
taro.name = 'タロウ'
mike.purr();
mike.sleep();
taro.purr();
taro.sleep();
実行
code:console
$ node cat.js
オブジェクト : モノ
車・動物・パソコン・机・財布・スマホ
時間・アイデア
「目に見えるもの」も「目に見えないもの」も総じて「モノ」
クラス : 概念・分類・型
猫そのものではなく、「猫とはどういうものか」を記述したもの。
「猫とはどんなものか」を考えて作成する。
今回
猫には名前が必ずあって、それぞれ違う。
猫は「鳴く」し「眠る」
インスタンス : クラスを実態化したもの
実際の「ミケ」という名前の猫
「new」がキーワード。
クラスの書き方について
code:cat.js
// クラス
class Cat {
// コンストラクタ
constructor() {}
name;
purr() {
console.log(this.name + 'が鳴く');
}
sleep() {
console.log(this.name + 'は眠る');
}
}
クラスの構成要素
猫の名前
他には体重・大きさ・空腹度などが考えられる。
上記は状態である。
コードではフィールドである。
「鳴く」「眠る」
他に歩く・食べるなどが考えられる
上記は振る舞いである
コードではメソッドである
クラスとは「状態」と「振る舞い」で概念を記述したもの
正解があるというより、どう考えたかが重要
必要なものだけ記述する。
記述方法
class 「クラス名」で定義
クラス名は大文字から始まる
constructor() {}というものを書く。
フィールドを書く
メソッドを書く
インスタンス化
code:cat.js
// 利用する処理
// インスタンス化
const mike = new Cat();
mike.name = 'ミケ';
const taro = new Cat();
taro.name = 'タロウ'
mike.purr();
mike.sleep();
taro.purr();
taro.sleep();
new 「クラス名()」でインスタンス化できる。
キーワードはnew
呼び出されるのはコンストラクタ(後述)
mike.name = 'ミケ';
mikeというインスタンスのnameにアクセスして「ミケ」を代入している。
mike.purr();
mikeというインスタンスのpurr()メソッドを呼び出している。
※変数の中身はインスタンスの参照である。
以下をcat.jsに追加
code:cat.js
~ 省略 ~
const sansho = taro;
taro.purr();
sansho.purr();
taro.name = 'ジロウ';
taro.purr();
sansho.purr();
sanshoの中にはtaroのインスタンスの参照が入る。
taroの名前が変わればsanshoの名前も変わる
演習1)
Deviceクラスの作成
table:field
フィールド名 説明
name 名前(識別名)
energy エネルギー。数値として扱う
table:method
メソッド名 引数 戻り値 説明
confirm なし なし 画面に「名前 : name, 現在のエネルギー : energe」
name・energyはフィールドで設定した値。
確認用コード
code:device.js
const device = new Device();
device.name = 'デバイス1';
device.energy = 10;
device.confirm();
演習2)
Deviceクラスに以下のメソッドを追加してください。
table:method
メソッド名 引数 戻り値 説明
move なし なし energeがあるとき
energeを1消費して「動作しました。残りエネルギー:energe」と画面表示
energeがないとき
「エネルギーが足りなかったので、動作しませんでした」と画面表示
確認用コード(一部コメントのみ)
code:device.js
const device = new Device();
device.name = 'デバイス2';
device.energy = 3;
device.confirm();
// deviceを四回動かす
device.confirm();
コンストラクタ
コンストラクタとは
新たなオブジェクトを生成する際に呼び出されて内容の初期化などを行なうメソッドのこと
クラスではconstructorの部分
クラスをnewしたときに実行される(インスタンス化の際実行される)
catを変更
コンストラクタでnameを初期化するように変更する。
code:cat.js
// クラス
class Cat {
// コンストラクタ
constructor(name) {
this.name = name;
}
// name;
purr() {
console.log(this.name + 'が鳴く');
}
sleep() {
console.log(this.name + 'は眠る');
}
}
// 利用する処理
// インスタンス化
const mike = new Cat();
const taro = new Cat('タロウ');
mike.purr();
mike.sleep();
taro.purr();
taro.sleep();
code:constructor.js
constructor(name) {
this.name = name
}
一種のメソッド
引数をもつことができる。
どういった場合に引数を書くか?
そのフィールドがオブジェクトにとって必要なとき(なければオブジェクトが成立しないとき)
むしろ引数がないときのconstructor(){}は省略できる
thisとは
thisはインスタンス自身を表す。
「自分自身」くらいで訳すとよい。
this.name = name
this.nameはフィールドのname
nameは引数のname
定義
クラス内でconstructorとかく。
別の言語だとクラス名と同じにする。(呼び出し時はそちらの方が分かりやすいかも)
戻り値は書かない。(書けない)
また、JavaScriptではconstructorの中で値を設定していたら、クラス上にnameを書く必要はない。
code:main.js
const mike = new Cat();
const taro = new Cat('タロウ');
mike.purr();
mike.sleep();
taro.purr();
taro.sleep();
呼び出し
newで呼び出す。
フィールドに値を入れ損ねると「undefined」(未定義)となる。
Javaだと、引数ありのコンストラクタしかなくて引数なしで呼び出すとコンパイルエラーになる。
複数のコンストラクタを用意する
JavaScriptではconstructorを複数書くことはできない。
演習3)
今までのdeviceクラスのコンストラクタを、nameとenergyで初期化するように修正してください。
演習4)
演習3)で作成したコンストラクタで、値が設定されずに呼び出された場合に以下の値をフィールドに設定するように変更してください。
name : デフォルト
energy : 10
補足
今回、nullも無効と考えていいです。
確認用コード
code:device.js
const device = new Device('デバイス4', 7);
device.confirm();
const defaultDevice = new Device();
defaultDevice.confirm();
オーバーロード
※JavaScriptでは基本的にサポートされていない。
以下オーバーロードのイメージ
code:cat.js
// クラス
class Cat {
// コンストラクタ
constructor(name) {
this.name = name
}
purr() {
console.log(this.name + 'が鳴く');
}
// 同じ名前で、引数の異なるメソッドを定義
purr(num: number) {
for(let i = 0; i < num; i++) {
this.purr();
}
}
sleep() {
console.log(this.name + 'は眠る');
}
}
オーバーロード
→ メソッドの多重定義
継承
オブジェクト指向の三大要素の一つ
あるクラスを引き継いで新しいクラスを作成すること
つまり、継承元のクラスの状態や振る舞いを引き継ぐことができる。
例)Catクラスを元にRussianBlueクラスを定義する。
余談
ここからはrequireを使って、別ファイルからクラスを読み込みます。
※後で全体的に書き換えるかも。
エクスポート側
module.exports = クラスA;
インポート側
const クラスA = require('ファイルパス');
code:cat.js
class Cat {
constructor(name) {
this.name = name
}
purr() {
console.log(this.name + 'が鳴く');
}
sleep() {
console.log(this.name + 'は眠る');
}
}
module.exports = Cat;
code:russian-blue.js
const Cat = require('./cat');
class RussianBlue extends Cat {
// override
purr() {
console.log(this.name + 'がニャーと鳴く');
}
// 新しいメソッド
scratch() {
console.log(this.name + 'が引っ掻く');
}
}
module.exports = RussianBlue;
継承のキーワードは「extends」
code:main.js
const RussianBlue = require('./russian-blue');
const blue = new RussianBlue('ブルー');
// ①overrideしたメソッド
blue.purr();
// ②親クラス(スーパークラス)のメソッド
blue.sleep();
// ③RussianBlueで新規に追加したメソッド
blue.scratch();
よく出力結果を確認してみてください。
①RussianBlueクラスで上書きした(オーバライドした)メソッドの結果が出力されている。
②RussianBlueクラスで定義されていないメソッドで、Catクラスのメソッドの結果が出力される。
同じ処理なら、改めて定義しなくても利用できる。
③RussianBlueクラスにしかないメソッドの結果が出力されている
また、RussianBlueにはconstructorの定義がなくてもnew RussianBlue(・・・)が呼び出せる。
Catクラス
親クラス・スーパークラスと呼ばれる。
RussianBlueクラス
子クラス・サブクラスと呼ばれる。
https://gyazo.com/8a931e603d244cab9a187f6206ff31a3
用語
オーバーライド
親クラスのメソッドを子クラスで再定義する行為
子クラスのメソッドで親クラスのメソッドを呼び出すことも可能。
以下、(面白味のある処理ではないが)purrに親クラスの処理を追加する処理
code:russian-blue.js
const Cat = require('./cat');
class RussianBlue extends Cat {
// override
purr() {
console.log('親クラスのpurr');
super.purr();
console.log('以下新しい処理');
console.log(this.name + 'がニャーと鳴く');
}
// 新しいメソッド
scratch() {
console.log(this.name + 'が引っ掻く');
}
}
module.exports = RussianBlue;
code:main.js
const RussianBlue = require('./russian-blue');
const blue = new RussianBlue('ブルー');
// ①overrideしたメソッド
blue.purr();
演習5)
Deviceクラスを継承したKeyboardクラスを作成してください。
Keyboardクラスのメソッドは以下です。
table:method
メソッド名 引数 戻り値 説明
move なし なし energeがあるとき
energeを1消費して「文字を入力しました。残りエネルギー:energe」と画面表示
energeがないとき
「エネルギーが足りなかったので、動作しませんでした」と画面表示
enter なし なし energeがあるとき
energeを1消費して「入力情報を確定しました。残りエネルギー:energe」と画面表示
energeがないとき
「エネルギーが足りなかったので、動作しませんでした」と画面表示
moveメソッドはオーバーライド
enterメソッドは新規作成。
動作確認コード
code:main-exe.js
const Keyboard = require('./keyboard');
const keyboard = new Keyboard('logicool', 5);
keyboard.confirm();
keyboard.move();
keyboard.enter();
keyboard.confirm();
演習6)
Deviceクラスを継承したMicrowaveOvenクラスを作成してください。
MicrowaveOvenクラスのメソッドは以下です。
table:method
メソッド名 引数 戻り値 説明
move なし なし energeがあるとき
energeを1消費して「動作しました。残りエネルギー:energe」と画面表示
その後)「温め中」と画面表示
energeがないとき
「エネルギーが足りなかったので、動作しませんでした」と画面表示
warm seconds なし seconds分moveメソッドを呼び出す。
※moveの実行の前に「~秒経過」と秒数も表示する。
moveメソッドはオーバーライド
warmメソッドは新規作成。
動作確認コード
code:main-exe.js
const MicrowaveOven = require('./microwave-oven');
const microwaveOven = new MicrowaveOven('panasonic', 5);
microwaveOven.confirm();
microwaveOven.move();
microwaveOven.warm(5);
microwaveOven.confirm();
コンストラクタの継承
サブクラスでコンストラクタを定義する場合、親クラスのコンストラクタと紐づけて定義する必要がある。
RussianBlueクラスに「weight」フィールドを追加したい場合
code:russian-blue.js
const Cat = require('./cat');
class RussianBlue extends Cat {
constructor(name, weight) {
// 親クラスのコンストラクタの呼び出し
super(name);
this.weight = weight;
}
// override
purr() {
console.log('親クラスのpurr');
super.purr();
console.log('以下新しい処理');
console.log(this.name + 'がニャーと鳴く');
}
// 新しいメソッド
scratch() {
console.log(this.name + 'が引っ掻く');
}
// 新しいメソッド
showStatus() {
console.log('体重 : ' + this.weight)
}
}
module.exports = RussianBlue;
code:main.js
const RussianBlue = require('./russian-blue');
const blue = new RussianBlue('ブルー', 15);
// ①overrideしたメソッド
blue.purr();
// ②親クラス(スーパークラス)のメソッド
blue.sleep();
// ③RussianBlueで新規に追加したメソッド
blue.scratch();
blue.showStatus();
code:constructor.js
constructor(name, weight) {
// 親クラスのコンストラクタの呼び出し
super(name);
this.weight = weight;
}
ポイント
constructorを上書きするときは、親のconstructorを先に呼び出す必要がある。
演習7)
Keyboardクラスのコンストラクタにキーボード配列(arrangement)を追加
Keyboardクラスに以下の新しいメソッドを追加。
table:method
メソッド名 引数 戻り値 説明
showArrangement なし なし 「キーボード配列:arrangement」と画面表示
動作確認コード
code:main-exe.js
const Keyboard = require('./keyboard');
const keyboard = new Keyboard('logicool', 5, 'JIS配列');
keyboard.confirm();
keyboard.showArrangement();
ポリモーフィズム
オブジェクト指向の三大要素の一つ
言葉の意味としては「多態性」
オブジェクト指向においては、「メッセージの送信側とメッセージの受信側が動的に決まる」ということ
同一名称メソッドであるが振る舞いは異なるメソッドとして働く。
※JavaScriptだと分かりにくい、かも
例)
code:russian-blue.js
const Cat = require('./cat');
class RussianBlue extends Cat {
constructor(name, weight) {
// 親クラスのコンストラクタの呼び出し
super(name);
this.weight = weight;
}
// override
// purrをシンプルに変更
purr() {
console.log(this.name + 'がニャーと鳴く');
}
// 新しいメソッド
scratch() {
console.log(this.name + 'が引っ掻く');
}
// 新しいメソッド
showStatus() {
console.log('体重 : ' + this.weight)
}
}
module.exports = RussianBlue;
code:persia.js
const Cat = require('./cat');
class Persia extends Cat {
// override
purr() {
console.log(this.name + 'がニャニャンと鳴く');
}
}
module.exports = Persia;
code:main.js
const RussianBlue = require('./russian-blue');
const Persia = require('./persia');
const catArray = [];
const blue = new RussianBlue('ブルー', 15);
const persia = new Persia('ペル');
catArray.push(blue);
catArray.push(persia);
for(let i = 0; i < catArray.length; i++) {
}
Q
実行結果はどうなる?
catArrayはどんな型の値のかたまり?
メモ:配列とは、基本「1つの変数で同じ型の値をいくつも管理するための機能」である。
https://gyazo.com/534fec56110f49ddb3f1a894c4f9a22e
参考
(キーボードの)ドライバ
どんなパソコンでも、どんなキーボードをつけても(大抵)動くのはなぜか?
利用者側は「logicool」の処理を直接実行しているのではなく、もっと上位のクラスのメソッドを実行している。
実装者は機器それぞれに合うように、キーボードの処理をオーバーライドしている。
ポリモーフィズム(インターフェース)の仕組み
オブジェクト指向が設計図といわれる所以
code:sample.js
Cat[] catArray = [];
Cat blue = new RussianBlue('ブルー', 15);
Cat persia = new Persia('ペル');
型付けがより厳密な言語であれば、上記の「blueでCatクラスに存在しないscratchメソッドは呼び出せない」といった型に即した制御がかかる。
抽象クラス
※JavaScriptでは難しい
インスタンス化できないクラス
code:main.js
const cat = new Cat('タマ');
const blue = new RussianBlue('ブルー', 15);
const persia = new Persia('ペル');
Catだけ階層が違う。
Catをインスタンス化してほしくない。
Catは継承元としてだけ、活用する。
また、継承を前提としているため「処理を定義しないメソッド」を定義することが可能
「処理を定義しないメソッド」 → 抽象メソッド
インプットなる引数と、(アウトプットとなる戻り値)の型だけを先に決定し、処理の詳細については後回しにする。
code:cat.js
class Cat {
constructor(name) {
this.name = name
}
purr() {
console.log(this.name + 'が鳴く');
}
sleep() {
console.log(this.name + 'は眠る');
}
// 処理を含まないメソッド
newMethod()
}
module.exports = Cat;
インターフェース
※JavaScriptでは難しい
抽象メソッド
インプットなる引数と、(アウトプットとなる戻り値)の型だけを先に決定し、処理の詳細については後回しにする。
全てのメソッドを抽象メソッドにする、という考え方もできる。
この振る舞い(メソッド)の仕様だけを定義したものを「インターフェース」という
インターフェースは、現場によってはめちゃくちゃつかう!!
→Mixin
多重継承やインターフェースのような役割に近いもの
ミックスイン (mixin) は、継承をおこなわずに他のオブジェクトやクラスに再利用可能な機能を追加することができるオブジェクトです。
code:animal-mixin.js
const animalMixin = {
move() {
console.log(${this.name}が動く)
},
}
module.exports = animalMixin;
code:main.js
const animalMixin = require('./animal-mixin.js');
const Cat = require('./cat');
// Cat.prototypeにanimalMixinを追加する形
Object.assign(Cat.prototype, animalMixin);
const tama = new Cat('タマ');
tama.purr();
tama.sleep();
tama.move();
console.log(Cat.prototype);
すべての列挙可能な自身のプロパティの値を、 1 つ以上のコピー元オブジェクトからコピー先オブジェクトにコピーするために使用されます。変更されたコピー先オブジェクトを返します。
Catでもmoveを利用することができる。
演習8)
wifi
復習キーワード
オブジェクトとは?
クラスとは?
インスタンスとは?
クラスの構成要素は何と何だった?
コンストラクタとは?