JavaScriptのオブジェクト指向プログラミングと「this」の注意点を簡潔に解説

JavaScriptでのオブジェクト指向プログラミングの方法と「this」の取り扱いのポイントについて解説します。

オブジェクト指向プログラミングについて

コンピューター技術が発展しプログラミングして開発されるソフトウェアの規模が大きくなるにつれソースコードはどんどん複雑になってきました。

そこでコードの再利用や部品化を考慮して開発することが現在でも進められています。

プログラミングのソースコード自体にもそれらを考慮して仕組みが整理されています。

その中で構造化されたプログラミングとしてオブジェクト指向という考え方が確立されました。

ここでいうオブジェクトは、様々な情報から属性や機能を1つにまとめて抽象化したものです。

例えばこのオブジェクトを「人」とした場合、その「人」には名前、年齢、性別、身長、体重などの属性があり、喋ったり歩いたり走ったりする機能があります。

これをオブジェクト指向プログラミングでコーディングすると、例えば「人」を「Person」というクラスで宣言して抽象化し、new演算子でインスタンス(実体)を作成します。

そして、それぞれ個別の名前や年齢などの属性を持ち、独自の喋ったり歩いたりする機能(メソッド)を持ちます。

こうして「Person」クラスを再利用することで効率よく多数の異なる「人」を作成することができます。

これがオブジェクト指向プログラミングです。

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

オブジェクト指向プログラミングでよく知られるのはJavaやC++です。

その他のプログラミング言語であるPythonやPHP、そしてJavaScriptもオブジェクト指向プログラミングでコーディングできます。

ここではJavaScriptでオブジェクト指向プログラミングの方法について解説します。

JavaScriptはECMAScript2015(ES2015)から他の言語と同じようにクラス宣言することができるようになりました。

ES2015以降の手法でクラス宣言を使用することが増えてきていると思いますが、それまでの方法でオブジェクト指向プログラミングを実践しているものが混在している状態です。

なので両方の手法をできるだけ簡潔に解説したいと思います。

ポイントとなるのが、「オブジェクト」と「インスタンス」です。

オブジェクトは器をイメージするとわかりやすいかもしれません。

例えば「人」という器に名前が「あかり」、年齢が「20」のインスタンス(実体)を生成することで「人」を作ります。

そして「人」という器を再利用して、今度は名前が「ももか」、年齢が「18」の別のインスタンスを生成することで新しい違った「人」を作ることができます。

それではこの例をJavaScriptでコーディングしてみましょう。

まずはES2015より前の方法です。

従来(ES2015より前)の方法

function Person(name, age) {
 this.name = name;
 this.age = age;

 this.getName = function() {
  console.log(this.name);
 }
}

var a = new Person("あかり", 20);
var b = new Person("ももか", 18);
a.getName();
b.getName();
> "あかり"
> "ももか"

実はJavaScriptでは関数もオブジェクトです。

なのでnew演算子を使ってインスタンスを生成できます。

関数そのものがコンストラクタとなります。

上記では名前が”あかり”で年齢が20のインスタンスと、名前が”ももか”で年齢が18のインスタンスを生成しています。

getNameがメソッドとなり、実行するとそれぞれのインスタンスに応じた処理を行います。

JavaScriptのオブジェクトはプロトタイプをもとに作成されます。

インスタンス生成時にはプロトタイプが持つ最低限のテンプレートをもとに生成されます。

このプロトタイプオブジェクトは「prototype」プロパティからアクセスできます。

そして「prototype」を使ってメソッドを定義することもできます。

Person.prototype.getAge = function() {
 console.log(this.age);
}

関数宣言の外に記述することができます。

この方法でメソッドを定義した場合はメモリ消費量を削減することができます。

コード量が多い場合はこの方法でコーディングしたほうが軽量化ができるかもしれません。

「prototype」は少しややこしいですが、関数オブジェクトを使って継承の仕組みを実装しようとする場合は「prototype」を使います。

例えば以下の関数を使って説明します。

function Person() {}
function User() {}

中身が空の関数が2つ定義されています。

new演算子を使ってこのオブジェクトのインスタンスを「prototype」に作ることで継承ができます。

User.prototype = new Person();

これは「User」のprototypeに「Person」オブジェクトのインスタンスを代入しています。

こうすることで、「User」は「Person」オブジェクトを参照できるようになります。

例えば以下を見てください。

function Person() {}
function User() {}

Person.prototype.say = function() {
 return "やっほー!!";
}

User.prototype = new Person();

var taro = new User();

console.log(taro.say());
>やっほー!!

Userのインスタンスであるtaroでもsayメソッドを利用することができます。

これらがES2015より前でのオブジェクト指向プログラミングの方法です。

ここでthisの扱いについて注意点があります。

thisは関数ごとにthisの値を定義しています。

例えば以下の場合を見てみましょう。

function Person() {
 // Person()の「this」は自分のインスタンスとして定義される。
 this.age = 0;

 var sec = setInterval(function growUp() {
  // growUp()関数の「this」はグローバルオブジェクトとして定義される。
  this.age++;
  console.log(this.age);
  if (this.age > 2) {
   clearInterval(sec);
  }
 }, 1000);
}

var p = new Person();

このコードを実行すると

>NaN
>NaN
>NaN

1秒毎に「NaN」の表示を延々と繰り返します。

つまり、thisの値が違うためPerson直下に記述しているthis.ageと、growUp関数に記述しているthis.ageは別物になっています。

growUp関数を無名関数にしても結果は同じです。

これを回避する1つの方法は、thisを別の変数に代入して利用します。

function Person() {
 var that = this;
 that.age = 0;

 var sec = setInterval(function growUp() {
  that.age++;
  console.log(that.age);
  if (that.age > 2) {
   clearInterval(sec);
  }
 }, 1000);
}
var p = new Person();
> 1
> 2
> 3

これで同じageを参照し、1秒毎にインクリメントするようになります。

他の回避方法として、ES2015以降で利用できるようになったアロー関数で定義します。

function Person() {
 this.age = 0;

 var sec = setInterval(() => {
  this.age++;
  console.log(this.age);
  if (this.age > 2) {
   clearInterval(sec);
  }
 }, 1000);
}
var p = new Person();
> 1
> 2
> 3

こうするとthis.ageは同じものを参照するので、1秒毎にインクリメントするようになります。

後者のほうがコーディングはスマートかと思います。

次にES2015以降の方法を見てみましょう。

ES2015以降の方法

ES2015以降ではクラス宣言がサポートされています。

class Person {
 constructor(name, age) {
  this.name = name;
  this.age = age;
 }

 getName () {
  console.log(this.name);
 }
}

var a = new Person("あかり", 20);
var b = new Person("ももか", 18);
a.getName();
b.getName();
> "あかり"
> "ももか"

このほうが、他の言語でオブジェクト指向プログラミングを経験している人はわかりやすいのではないでしょうか。

ここでのthisの扱いについての注意点ですが、クラス宣言内でメンバ変数やメソッドを使うときは必ずthisを付けなければなりません。

thisは省略不可なので気をつけてください。

そしてクラスを継承するには「extends」を使います。

class User extends Person{
 constructor(name) {
  super(name);
  this.age = 0;
 }
}

var c = new User("かえで");
c.getName();
> "かえで"

継承もこのほうがわかりやすいですね。

ただし、JavaScriptのクラス宣言はあくまでプロトタイプベースのシンタックスシュガー(構文上の糖衣)にすぎません。

つまり、コーディングしやすくするためにクラス宣言が追加されただけで、仕組みはプロトタイプベースから変更されていないということです。

なのでプロトタイプベースの仕組みは理解しておく必要があります。

とはいえ他の言語のようにオブジェクト指向プログラミングを実践できるのでES2015から数年が経過した現在では多く利用されてきているのではないでしょうか。