【STEP.28】
JavaScriptのオブジェクト指向をマスターしよう!
~Part.2~

外部からアクセスできないプロパティ/メソッドを定義する

クラス外部からアクセスできないメンバー(プロパティ/メソッド)のことをプライベートメンバーといいます。

クラス内部でのみ利用するメンバーに対して、外から安易にアクセスできるのは(意図しない操作の危険があるという意味で)望ましいことではありません。

そのようなメンバーは、できるだけプライベートメンバーとして定義することで、クラスの部品としての安全性を高めることができます。

ちなみに、クラス内外から自由にアクセスできるメンバーのことをパブリックメンバーといいます。

JavaScriptでは、何も考えずにメンバーを定義すれば、パブリックメンバーとなります。

パブリックメンバーとプライベートメンバー

もっとも、JavaScriptは標準で、このようなプライベートメンバーの仕組みを持っていません。

プライベートメンバーを定義するには、モジュールとシンボルを併用した、やや特殊なコーディングが必要になります。

プロパティのプライベート化

たとえば以下は、プライベートプロパティとしてNAMEBIRTHを持つPersonクラスの例です。

const NAME = Symbol();
const BIRTH = Symbol();

export class Person {
  constructor(name, birth) {
    //プライベートメンバーを初期化
    this[NAME] = name;
    this[BIRTH] = birth;
  }
  //プライベートメンバーにアクセスするためのメソッド
  getName() {
    return this[NAME];
  }
  getBirth() {
    return this[BIRTH];
  }
}
import { Person } from './class_private_lib.js';

let p = new Person('横浜流星','1996/9/16');
  console.log(JSON.stringify(p));
  //結果:{}

for(let value in p) {
  console.log(value);
  //結果:(何も表示されない)
}

console.log(p.getName());
//結果:横浜流星
console.log(p.getBirth());
//結果:1996/9/16

プライベートメンバーを定義するには、NAMEBIRTHプロパティの名前をシンボルとして準備し(「const NAME = Symbol();」「const BIRTH = Symbol();」)、「this[シンボル] = ~」でプロパティを準備します。(「this[NAME] = name;」「this[BIRTH] = birth;」)

this.シンボル= ~」ではないので注意が必要です。

シンボルの値はモジュールの外からはわからないので、利用者は[NAME][BIRTH]プロパティにアクセスできない、というわけです。

JSON.stringifyで文字列化したり、for…in命令で列挙しようとしても同様です。(class_private.jsの3~10行目)

メソッドのプライベート化

同じように、メソッドもプライベート化できます。

以下は、MyClassクラスに対して、プライベートメソッド[GET_PRIVATE]と、これにアクセスするためのパブリックメソッドgetPublicを定義する例です。

const GET_PRIVATE = Symbol();
		
export	class MyClass {
  //シンボル経由でしかアクセスできないプライベートメソッド
  [GET_PRIVATE](){
    return 'private value';
  }
  //プライベートメソッドにアクセス
  getPublic() {
    return 'Public:' + this[GET_PRIVATE]();
  }
}
import { MyClass } from './class_private_lib2.js';
		
let cls = new MyClass();
console.log(cls.getPublic());
//結果:Public:private value
console.log(cls[GET_PRIVATE]());
//結果:private value

[GET_PRIVATE](){...}」のような表記は、いわゆるComputed property namesです。

シンボルの値から動的にメソッド名を生成しています。

Computed property namesについては以下のリンクを参照ください。

【注意】厳密にプライベート化できるわけではない

ただし、シンボルによって定義されたメンバーも、完全に存在を隠蔽できるわけではありません。

というのも、Object.getOwnPropertySymbolsメソッドを利用することで、シンボルプロパティにも強制的にアクセスできてしまうからです。

let prop = Object.getOwnPropertySymbols(p)[0];
console.log(p[prop]);
//結果:横浜流星

そもそもシンボルの値を隠蔽できるのは、あくまでモジュールの外部に対してだけです。

同じモジュールであれば、定数NAMEBIRTH経由でアクセスが可能です。

let p = new Person('横浜流星','1996/9/16');

console.log(p[NAME]);
//結果:横浜流星
console.log(p[BIRTH]);
//結果:1996/9/16

オブジェクトの継承

オブジェクト指向言語を理解する上で、重要な概念の一つが継承です。

継承とは、元になるオブジェクト(クラス)の機能を引き継いで、新たなクラスを定義する機能のことをいいます。

これを利用すれば、共通した機能を複数のクラスで重複して定義する必要がなくなり、元となるクラスから差分の機能だけを記述するだけで済むようになります。(差分プログラミングといいます。)

継承において、継承元となるクラスのことを基底クラス(親クラス)、継承によってできたクラスのことを派生クラス(子クラス)といいます。

クラスを継承するには、extendsキーワードを利用します。

クラスの継承

class ChildClass extends ParentClass {
  ...definitions...
}
ChildClass   クラス名
ParentClass  継承元のクラス
definitions  クラスの本体

たとえば以下は、Personクラスを継承して、BusinessPersonクラスを定義する例です。

class Person {
  constructor(name) {
    this.name = name;
  }
  show() {
    return `${this.name}です。`;
  }
}
		
//Personクラスを継承したBusinessPersonクラス
class BusinessPerson extends Person {
  work() {
    return `${this.name}はセッセと働きます。`;
  }
}

let bp = new BusinessPerson('横浜流星');
console.log(bp.show());
//結果:横浜流星です。
console.log(bp.work());
//結果:横浜流星はセッセと働きます。

BusinessPersonクラスで定義されたworkメソッドはもちろん、Personクラスで定義されたshowメソッドが、BusinessPersonクラスのメンバーとして呼び出せていることが確認できます。

MEMO
ー単一継承とはー
1つのクラスが継承できるのは1つのクラスのみです。

これを単一継承の性質と呼びます。

反対語は多重継承(複数のクラスを同時に継承)といいます。

JavaScriptでは、多重継承には対応していません。

基底クラスのプロパティ/コンストラクターを上書きする【オーバーライド】

派生クラス(子クラス)では、基底クラス(親クラス)に新たなメソッドを追加するばかりではありません。

基底クラスで定義されたメソッドを、派生クラスで上書きすることもできます。(これをメソッドのオーバーライドといいます。)

たとえば以下は、Personクラスで定義されたコンストラクター/showメソッドを、BusinessPersonクラスでオーバーライドする例です。

class Person {
  constructor(name) {
    this.name = name;
  }
  show() {
    return `${this.name}です。`;
  }
}
	
class BusinessPerson extends Person {
 //新たなjobプロパティを追加
  constructor(name, job) {
    super(name);
    this.job = job;
  }
  //基底クラスのshowメソッドをオーバーライド
  show() {
      return `${super.show()}(${this.job})`;
  }
}	
let bp = new BusinessPerson('横浜流星','俳優');
console.log(bp.show());
//結果:横浜流星です。(俳優)

オーバーライド(上書き)とはいっても、基底クラスの機能を完全に書き替えてしまう状況はまれです。

一般的には、基底クラスの機能を流用しつつも、派生クラス側では差分の機能だけを追加していくことになります。

そのような場合に利用するのが、superキーワードです。

superを経由することで、派生クラスから基底クラスのメソッド/コンストラクターを呼び出せます。(「super(name);」「`${super.show()}(${this.title})`;」)

superキーワード

super(parameters,...) コンストラクター
super.method(parameters,...) メソッド名
parameters  引数
method      メソッド名

なお、コンストラクターでsuper呼び出しをする場合には、先頭の文でなければなりません。

さもないと「Must call super constructor in derived class before accessing 'this' or returning from derived constructor」(thisアクセスの前にsuperを呼び出しなさい)のようなエラーとなります。

オブジェクトの型を判定する

JavaScriptには、厳密な意味での「クラス」という概念はありません。

そのため、型という概念も正確にはありませんが、constructorプロパティ、instanceofin演算子、isPrototypeOfメソッドなどを利用することで、緩い型を判定できます。

生成元のコンストラクターを取得する【constructorプロパティ】

constructorプロパティは、インスタンスの生成元となるコンストラクターを取得します。

class Person {}
class BusinessPerson extends Person {}

let p = new Person();
let bp = new BusinessPerson();

console.log(p.constructor === Person);
//結果:true
console.log(bp.constructor === Person);
//結果:false
console.log(bp.constructor === BusinessPerson);
//結果:true

ただし、派生クラスではconstructorプロパティが返すのも派生クラスのコンストラクターです。(console.log(bp.constructor === Person); //結果:false

継承ツリーをたどって、基底クラスとの型の互換性を判定するならば、instanceof演算子を利用します。

指定されたクラスのインスタンス化を判定する【instanceof演算子】

オブジェクト変数 instanceof クラス」の形式で、そのオブジェクトの元となるクラスを判定できます。

継承ツリーをさかのぼっての判定も可能です。

class Person {}
class BusinessPerson extends Person {}

let p = new Person();
let bp = new BusinessPerson();

console.log(bp instanceof BusinessPerson);
//結果:true
console.log(bp instanceof Person);
//結果:true
console.log(bp instanceof Object);
//結果:true(Objectはルートオブジェクト)

参照しているプロトタイプを確認する【isPrototypeOfメソッド】

instanceof演算子と似たようなメソッドとして、isPropertyOfメソッドもあります。

こちらはプロトタイプ(prototypeプロパティ)の確認として利用します。

class Person {}
class BusinessPerson extends Person {}

let p = new Person();
let bp = new BusinessPerson();

console.log(BusinessPerson.prototype.isPrototypeOf(bp));
//結果:true
console.log(Person.prototype.isPrototypeOf(bp));
//結果:true
console.log(Object.prototype.isPrototypeOf(bp));
//結果:true

メンバーの有無を判定する【in演算子】

JavaScriptのオブジェクトは、コンストラクターをもとにのみ作成されるわけではありません。

リテラルとして作成される場合もありますし、コンストラクターをもとに生成されたとしても、あとからインスタンスに対してメンバーが追加されることもあります。

より厳密に、その時点であるメンバーが存在するかどうかをチェックするのならば、in演算子を利用します。

let obj = {method1:function(){}, method2:function(){}};
		
console.log('method1' in obj);
//結果:true
console.log('method3' in obj);
//結果:false

オブジェクトの内容をfor…of命令で列挙可能にする【イテレーター】

イテレーターを利用します。

たとえば以下は、MyArrayクラスが保持している配列の内容を、for…of命令で列挙できるようにする例です。

class MyArray {
  //可変長引数の内容をvaluesプロパティに設定
  constructor(...values) {
    this.values = values;
  }
			
  //既定のイテレーターを取得するためのメソッド
  [Symbol.iterator]() {
    let i = 0;
    let that = this;
    return {
      //valuesプロパティから次の値を取り出す
      next() {
        return i < that.values.length ?
          { done: false, value: that.values[i++] }:
          { done: true };
      }
    }
  };
}
//MyArrayクラスの内容を列挙
let my = new MyArray('ぱんだ','うさぎ','こあら');
for (let v of my) {
  console.log(v);
  //結果:ぱんだ、うさぎ、こあら
}

for…of命令は、内部的には[Symbol.iterator]メソッドを介してイテレーターを取得します。

そのため、自作クラスを列挙可能にする際にも、[Symbol.iterator]メソッドを実装すれば良いということです。(ソースコード8~19行目)

イテレーターであることの条件はnextメソッド(ソースコード13~17行目)を持つことと、nextメソッドの条件は、

{ done: 末尾に到達したか, value: 値 }

形式の戻り値を返すことです。

ソースコード13~17行目で現在のインデックス値(i)を判定し、配列(that.values)のサイズ未満であれば

{ done: false, value: 現在値 }

を、さもなくば

{ done: true }

をそれぞれ返しています。(doneプロパティがtrueであるということは、イテレーターは終端に到達しているので、valueプロパティは不要です。)

let that = this;」(ソースコード10行目)で、thisthatに格納しているのは、thisが文脈によって変化するためです。

[Symbol.iterator]メソッド(ソースコード行8~19行目)配下のthisは現在のインスタンスですが、this.valuesとしても意図した値は取得できません。

そこで、[Symbol.iterator]メソッド配下のthisをいったんthatに退避しておくことで(let that = this;/ソースコード10行目)、nextメソッドの配下でもMyArrayクラスのメンバーにアクセスできるようにしています。

イテレーターをより簡単に実装する【ジェネレーター】

ジェネレーターを利用します。

以下は、上記のMyArrayクラスをジェネレーターを利用して置き換えたものです。

class MyArray {
  //可変長引数の内容をvaluesプロパティに設定
  constructor(...values) {
    this.values = values;
  }
			
  //valuesプロパティの内容を反復するためのジェネレーター
  *[Symbol.iterator]() {
    let i = 0;
    while (i < this.values.length) {
      yield this.values[i++];
    }
  }
}
//MyArrayクラスの内容を列挙
let my = new MyArray('ぱんだ','うさぎ','こあら');
for (let v of my) {
  console.log(v);
  //結果:ぱんだ、うさぎ、こあら
}

ジェネレーター関数の戻り値はiteratorと互換性のあるGeneratorオブジェクトです。

よって、クラス既定のイテレーターに対して、ジェネレーター関数を渡すことで、そのままイテレーターとして動作させることが可能です。

インスタンスメソッドでジェネレーターであることを表すには、メソッド名の頭に「*」を付与します。(*[Symbol.iterator]()/ソースコード8行目)

モジュールを定義する

JavaScriptのモジュールは、1つのファイルとして定義するのが基本です。

たとえば以下は、定数APP_NAME、関数getTriangle、クラスPersonを、それぞれutilモジュールとしてまとめたものです。

ファイル名がそのままモジュールと見なされます。

const APP_NAME = 'ユタスク【Utask】';

export function getTriangle(base, height) {
  return base * height / 2;
}

export class Person {
  constructor(name) {
    this.name = name;
  }
  show() {
    return `${this.name}です。`;
  }
}

モジュール配下のメンバーは、既定で非公開です。

外部からアクセスするには、それぞれ宣言の頭にexportキーワードを付与します。

utilモジュールの例であれば、getTriangle関数/Personクラスが公開の対象です。(定数APP_NAMEにはexportがないので、非公開です。)

モジュールを利用する

定義済みのutilモジュールを利用するコードは、以下の通りです。

import { getTriangle, Person } from './util.js'; 

console.log(getTriangle(5,10));
//結果:25

let p = new Person('横浜流星');

console.log(p.show());
//結果:横浜流星

モジュールをインポートするのは、import命令の役割です。

import命令

import { member, ... } from module
member    メンバー
module    モジュール

モジュールは、現在の.jsファイルからの相対パスで表します。

よってutilモジュールがサブフォルダーの/libに格納されている場合には、importも以下のように表します。

import { getTriagnle, Person } from './lib/util.js';

またモジュール側でexport宣言していても、利用側でimport宣言されなかったものにはアクセスできません。

たとえば、以下のようにimport宣言した場合には、Personクラスに対してのみアクセスが可能です。(getTriangle関数にはアクセスできません。)

import { Person } from './util.js';

モジュール名の表記

webpack/Browserifyなどのモジュールバンドラーを利用している場合には、

import { getTriangle, Person } from './util';

のように、モジュール名は拡張子なしの形で記述するのが一般的です。

ただし、この記法はモジュールハンドラーが既定の拡張子を認識して補っているにすぎません。

ブラウザー単体でモジュールを利用する場合には、拡張子は省略せずに記述する必要があります。

モジュールを利用する場合の<script>要素

モジュールを利用する場合は、<script>要素の記述も変化します。

といってもtype属性にmoduleを指定するだけです。

<script type="module" src="scripts/module_basic.js"></script>

もっとも、「type=”module”」属性はInternet Explorer 11では動作しないなどよく利用されているすべてのブラウザーに対応しているわけではありません。

不特定多数のユーザーを対象にしたサイトでは、まずは、モジュールを1つに束ねるためのモジュールハンドラーを利用します。

モジュールをインポートするさまざまな記法

import命令には、目的に応じてさまざまな記法があります。

ここでは、その中でもよく利用すると思われるものをまとめます。

モジュール配下のすべてのメンバーをインポート

*」でモジュール配下のすべてのメンバーをインポートできます。

その場合、as句によってモジュールの別名を指定する必要があります。

const APP_NAME = 'ユタスク【Utask】';

export function getTriangle(base, height) {
  return base * height / 2;
}

export class Person {
  constructor(name) {
    this.name = name;
  }
  show() {
    return `${this.name}です。`;
  }
}
import * as u from './util.js';
console.log(u.geTriangel(5,10));
//結果:25
let p = new u.Person('横浜流星');
console.log(p.show());
//結果:横浜流星

この例であれば、utilモジュールのすべてのエクスポートを「u.~」の形式でアクセスできるようにしています。

モジュール配下のメンバーに別名を付与

as句を利用することで、モジュール配下の個々のメンバーに別名を付与することも可能です。

モジュール間で名前の衝突があった場合などに利用します。

const APP_NAME = 'ユタスク【Utask】';

export function getTriangle(base, height) {
  return base * height / 2;
}

export class Person {
  constructor(name) {
    this.name = name;
  }
  show() {
    return `${this.name}です。`;
  }
}
import { getTriangle as myGetTriangle, Person as MyPerson } from './util.js';

console.log(myGetTriangle(5,10));
//結果:25
let p = new MyPerson('横浜流星');
console.log(p.show());
//結果:横浜流星です。

既定のエクスポートをインポート

モジュール配下の1つのメンバーに対してであれば、既定のエクスポートを宣言することもできます。

これには、以下のようにdefaultキーワードを付与するだけです。

既定のエクスポートでは、関数/クラスの名前は省略可能です。

export default class {
  static getSquare(base, height) {
    return base * height;
  }
}

これをインポートしているのが、以下のコードです。

これでutilモジュールの既定のエクスポートに、utilという名前でアクセスできるようになります。

import util from './util.js';

console.log(util.getSquare(10,5));
//結果:50

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です