Help us understand the problem. What is going on with this article?

ECMAScript 2015 JavaScript クラスで始める、オブジェクト指向プログラミング入門

More than 1 year has passed since last update.

ECMAScript 2015から導入された、JavaScript クラス。
再利用性が高く、きれいな保守性の高いコードを書きやすくなりました。

この記事では、JavaScript初学者向けに、クラスを利用したプログラミングに必要な、オブジェクト指向プログラミングや、クラスの基礎的な解説ES5までの擬似的なクラスと、ES2015のクラスの比較をしています。

一つ上のJavaScriptプログラミングのために、一緒に学習していきましょう!

index

  • オブジェクト指向プログラミング
  • ES5の(擬似的な)クラス
  • ES2015のクラス
  • 関数のみとクラスの比較
  • まとめ

オブジェクト指向プログラミング

オブジェクト指向プログラミングとは?

オブジェクト指向プログラミング(オブジェクトしこうプログラミング、英: object-oriented programming, OOP)は、コンピュータ・プログラミングのパラダイムのひとつで、オブジェクト指向の概念や手法を取り入れたものである。プログラムを、データとその振舞が結び付けられたオブジェクトの集まりとして構成する、などといった特徴がある。このパラダイムを指向しているプログラミング言語がオブジェクト指向プログラミング言語である。

ウィキペディア:オブジェクト指向プログラミングより抜粋

なんだか、難しい言葉がたくさんありますが、ざっくりいうと、「オブジェクトを利用し、再利用性を高めたプログラミング」です。

オブジェクト指向プログラミングの方式

オブジェクト指向プログラミングには、以下の方式があります。

  • クラスベース方式(継承ベース)
  • プロトタイプベース方式(インスタンスベース)
  • mixin方式

JavaScriptはプロトタイプベース方式のオブジェクト指向プログラムです。

クラス

クラスとは?

オブジェクトを利用し、データや操作を隠蔽(カプセル化)して、再利用性を高めるプログラミングの手法です。

クラスの用語解説

  • コンストラクタ関数:オブジェクトの設計図の役割を果たす関数(JavaScriptにおける、クラスの本体)
  • インスタンス:コンストラクタ関数(クラス)から生成したオブジェクト
  • メンバ変数:インスタンスが保持するデータ
  • インスタンスメソッド:インスタンスが保持する関数(メンバ関数)
  • プロトタイプメソッド:コンストラクタ関数(クラス)のprototypeプロパティに追加された関数

注意:JavaScriptがクラスベース方式を採用したわけでない

MDNのクラスの冒頭に、このような記述があります。

ECMAScript 2015 で導入された JavaScript クラスは、JavaScript にすでにあるプロトタイプベース継承の糖衣構文です。クラス構文は、新しいオブジェクト指向継承モデルを JavaScript に導入しているわけではありません。

JavaScriptのクラスは、ES5で用いられていた、クラスのような擬似的な機能を、書きやすい形にしたもので、根本的なJavaScriptとしての機能や仕様に変更があったわけではありませんので、ご注意を!

ES5の(擬似的な)クラス

まずは、従来どおりのES5での記述を見ていきましょう。
ES5までは、以下のような形で、クラスのような機能を擬似的に作っていました。

インスタンスメソッド

インスタンス(オブジェクト)の、インスタンスメソッドを呼び出すパターン。

// コンストラクタ関数
function Foo(fuga) {
  this._fuga = fuga;
  this.method = function() {
    console.log(this._fuga);
  };
};

// インスタンス化
var foo = new Foo('foo');

// function Foo(fuga) {
//   // var this = {}; // new はこう解釈する
//   this._fuga = fuga;
//   this.method = function() {
//     console.log(this._fuga);
//   };
//   // return this; // new はこう解釈する
// };

// fooオブジェクトのインスタンスメソッド実行
foo.method(); // -> foo

// foo2オブジェクトのインスタンスを作り、インスタンスメソッドを実行
var foo2 = new Foo('foo2');
foo2.method(); // -> foo2

// foo.method と foo2.methodは、異なるオブジェクトのインスタンスメソッド
console.log(foo.method === foo2.method); // -> false

インスタンスメソッドは、インスタンス(オブジェクト)毎に作られてしまうので、コンストラクタ関数をインスタンス化するたびにインスタンスメソッドが重複し、メモリを圧迫してしまいます。
特別な理由がない限りは、以下のプロトタイプメソッドのパターンがおすすめです。

プロトタイプメソッド(推奨)

コンストラクタ関数のprototypeプロパティに、関数を登録するパターン。

// コンストラクタ関数
function Foo(fuga) {
  this._fuga = fuga;
};

// プロトタイプに追加
// 関数を作った時点で、prototypeプロパティが生成される
Foo.prototype.method = function() {
  console.log(this._fuga);
};

// インスタンス化
var foo = new Foo('foo');

// function Foo(fuga) {
//   // var this = {}; // new はこう解釈する
//   this._fuga = fuga;
//   // return this; // new はこう解釈する
// };

// fooオブジェクトのプロトタイプメソッドを実行
foo.method(); // -> foo

// foo2オブジェクトを作成し、のプロトタイプメソッドを実行
var foo2 = new Foo('foo2');
foo2.method(); // -> foo2

// 同じメソッド
console.log(foo.method === foo2.method); // -> true

親のprototypeにメソッドを追加し、それを参照することにより、メモリの圧迫を防ぐことができます。

ECMAScript 2015のクラス

次に、ECMAScript 2015で追加された、クラスの記述を見ていきましょう。

Classの基本構文

// クラス宣言
class Person {
  // コンストラクタ関数
  constructor(firstName) {
    this._lastName = 'Otsuka';
    this._firstName = firstName;

    // インスタンスメソッド
    // this.insMethod = () => {
    //   console.log('[instance]', `私は、${this._firstName} ${this._lastName}です。`);
    // }
  }

  // プロトタイプメソッド
  method() {
    console.log('[prototype]', `私は、${this._firstName} ${this._lastName}です。`);
  }
}

// クラス式
// 宣言でも式でも、巻き上がりは起こらない。
// const Person = class {};
// or
// const Person = class Person {};

// クラスは関数。やっていることは、ES5の頃と同じ。
// console.log(typeof Person); // -> function

// インスタンス化
const person = new Person('Yuhi');

// インスタンス化することにより、Object になる
// console.log(typeof person); // -> object

// プロトタイプメソッドの実行
person.method();
// -> [prototype] 私は、Yuhi Otsukaです。

// インスタンスメソッドの実行(インスタンス毎に作成される)
// person.insMethod();
// -> [prototype] 私は、Yuhi Otsukaです。

コンストラクタ関数がインスタンス化し、オブジェクトになる工程や、インスタンスメソッド・プロトタイプメソッドがある点など、記述の仕方が変わっているだけで、ES5の頃と仕組みは同じことがわかると思います。

まとめると、

  • クラスの書き方は、クラス宣言とクラス式がある
  • 宣言も式も、巻き上がりは起こらない
  • コンストラクタは必須(記述がなくても作られるので、書かなくてもOK)
  • インスタンスメソッドも、プロトタイプメソッドも作れる
    • こちらも、特別な理由がなければ、プロトタイプがおすすめ
  • インスタンス化 -> メソッド実行の流れはES5と同じ

こんな感じでしょうか。

静的メソッド

クラスでは、インスタンス化するまえに実行できる、静的メソッドを使用することができます。
使用方法は、以下の通り、プロトタイプメソッドの記述の前に、staticキーワードを追記します。

// クラス宣言
class Person {
  // コンストラクタ関数
  constructor(firstName) {
    this._lastName = 'Otsuka';
    this._firstName = firstName;
  }

  // プロトタイプメソッド
  method() {
    console.log('[prototype]', `私は、${this._firstName} ${this._lastName}です。`);
  }

  // 静的メソッド
  static staMethod() {
    console.log('[static]');
  }
}

// インスタンス化するまえでも、実行できる
Person.staMethod(); // -> [static]

// これは駄目
person.method(); // -> Uncaught ReferenceError: person is not defined

まとめると、

  • static キーワードをつけると、静的メソッドになる
  • 静的メソッドは、インスタンス化前に実行できる

こんな感じです。

getterとsetter

getterとsetterを使って、メンバ変数を変更したりもできます。

// クラス宣言
class Person {
  // コンストラクタ関数
  constructor(firstName) {
    this._lastName = 'Otsuka';
    this._firstName = firstName;
  }

  // getter
  get changeLastName() {
    return `婿入りして、${this._firstName} ${this._lastName}になりました。`;
  }

  // setter
  set changeLastName(val) {
    this._lastName = val;
  }

  // プロトタイプメソッド
  method() {
    console.log('[prototype]', `私は、${this._firstName} ${this._lastName}です。`);
  }
}

// インスタンス化
const person = new Person('Yuhi');

// メソッドの実行
person.method();
// -> [prototype] 私はYuhi Otsukaです。

// setter
// _lastNameプロパティを上書き
person.changeLastName = 'Kato';

// プライベート変数の上書きはご法度!
// person._lastName = 'Kato';

// getter
// changeLastNameプロパティに文字列を追加
console.log('[getter]', person.changeLastName);
// -> [getter] 婿入りして、Yuhi Katoになりました。

まとめると、

  • getter は get name() {};
  • setter は set name(val) {};
    • 引数は必須
  • どちらかだけの利用でもOK
  • プライベート変数の上書きはご法度なので、getter or setterを使用する

こんな感じです。

継承(サブクラス)

クラスは継承という形で、拡張することができます。

// クラス宣言
class Person {
  // コンストラクタ関数
  constructor(firstName) {
    this._lastName = 'Otsuka';
    this._firstName = firstName;
  }

  // getter
  get changeLastName() {
    return `婿入りして、${this._firstName} ${this._lastName}になりました。`;
  }

  // setter
  set changeLastName(val) {
    this._lastName = val;
  }

  // プロトタイプメソッド
  method() {
    console.log('[prototype]', `私は、${this._firstName} ${this._lastName}です。`);
  }
}

// 継承
class Child extends Person {
  childMethod() {
    // 親のコンストラクタを継承
    console.log('[extends]', `私の名前は、${this._firstName}です。`);

    // 親メソッドの呼び出し
    super.method();
  }
}

// 親クラスのコンストラクタ関数を継承し、new してインスタンス化
const child = new Child('Takeshi');

// プロトタイプメソッドの実行
child.childMethod();
// -> [extends] 私の名前は、Otsukaです。
// -> [prototype] 私は、Takeshi Otsukaです。

// 親クラスの get と set を実行
child.changeLastName = 'Kato';

// extends getter
console.log('[extends][getter]', child.changeLastName);

まとめると、

  • 継承は、class ChildClassName extends ClassName {}
  • new すると、親のコンストラクタでインスタンス化
  • super で親のメソッドを呼べる
  • 親のgetter や setterを使える

こんな感じです。

関数のみとクラスの比較

クラスと関数のみのプログラムを比較して、どのような違いがあるか見てみましょう。
以下は、this名前を知りたいプログラムの一例です。

関数のみ

const funcPerson = (firstName, lastName) => {

  // それぞれの関数を定義
  const sayThis = () => {
    console.log(this);
  };

  const sayFirstName = () => {
    console.log(firstName);
  };

  const sayLastName = () => {
    console.log(lastName);
  };

  const sayFullName = () => {
    console.log(firstName, lastName);
  };

  // 必要なものを実行
  sayThis();
  sayFirstName();
};

// thisを知りたい and ファーストネームを聞きたい
funcPerson('Yuhi', 'Otsuka');
// -> window{}
//    Yuhi

funcPerson('Takeshi', 'Kato');
// -> window{}
//    Takesi

// ラストネームを聞きたい
// -> funcPerson関数の sayFirstName(); を sayLastName(); に書き換えて、
//    sayThis()をコメントアウトして、実行

// フルネームを聞きたい
// -> funcPerson関数の sayFirstName(); を sayFullName(); に書き換えて、
//    sayThis()をコメントアウトして、実行

なんかすごくアレなプログラムですがw
いろいろややこしくなってくると、気がつくとこんな感じのコードになっていることもあるかと思います。

ぼくです((´^ω^))

また、thisがグローバルになってしまっているので、安全性を高めるために、スコープしたいですよね。

クラス

class ClassPerson {
  constructor(firstName, lastName) {
    this._firstName = firstName;
    this._lastName = lastName;
  }

  // それぞれのメソッドを設定
  sayThis() {
    console.log(this);
  }

  sayFirstName() {
    console.log(this._firstName);
  }

  sayLastName() {
    console.log(this._lastName);
  }

  sayFullName() {
    console.log(this._firstName, this._lastName);
  }
}

const classPerson = new ClassPerson('Yuhi', 'Otsuka');
const classPerson2 = new ClassPerson('Takeshi', 'Kato');

classPerson.sayThis(); // -> ClassPerson{}

// ファーストネームを聞きたい
classPerson.sayFirstName(); // -> Yuhi
classPerson2.sayFirstName(); // -> Takeshi

// ラストネームを聞きたい
classPerson.sayLastName(); // -> Otsuka
classPerson2.sayLastName(); // -> Kato

// フルネームを聞きたい
classPerson.sayFullName(); // -> Yuhi Otsuka
classPerson2.sayFullName(); // -> Takeshi Kato

クラスの書き方ですと、ほしい情報を返すプロパティメソッドを実行することができるので、全体的にコードがスマートになっていますね。
また、thisがそのクラスになっているので、安心です((´^ω^))

まとめ

  • オブジェクト指向プログラミングとは、オブジェクトを利用し、再利用性を高めたプログラミング
  • JavaScriptはプロトタイプベース方式のプログラミング言語
  • ES5では擬似的にクラスを表現していた
  • ES2015からClassが使えるようになった
  • クラスを使うと、再利用性の高いコードが書きやすい
otsukayuhi
ゆめみ所属のフロントエンドウェブデベロッパーです。藤子・F・不二雄先生を尊敬する、キーボードマニアでもあります。
https://otsukayuhi.app/
yumemi
みんなが知ってるあのサービス、実はゆめみが作ってます。スマホアプリ/Webサービスの企画・UX/UI設計、開発運用。Swift, Kotlin, PHP, Vue.js, React.js, Node.js, AWS等エンジニア・クリエイターの会社です。Twitterで情報配信中https://twitter.com/yumemiinc
http://www.yumemi.co.jp
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away