WebEngine

だらだらと綴る技術系メモ

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

注意(2017.12.30追記)

JavaScriptの進化は早いです。少し前に調べものをしていたら、自分の知っているJSとは別物のような記述の仕方などが出てきて戸惑いました。
この記事では古いクラス実装の手法を紹介していますが、まだこの記事と同じようなスタイルで実装してあるプログラムの保守など、参照の機会はあると思うので少しは役に立つかもしれない、と考えています。 最新のクラス実装の仕方を知りたい方は、記事末尾まで飛んで下さい。軽くですが、最新の手法を載せておきます。

JavaScriptの特徴

JavaScriptは立派なオブジェクト指向言語です。ただし、Javaなんかのオブジェクト指向言語とは かなり異なる部分があり、オブジェクト指向を知らない初心者がJavaScriptから勉強しはじめると 混乱に陥ります。
まず、JavaScriptにはクラスがありません。かわりにプロトタイプと呼ばれるものが存在します。 ただし、このプロトタイプという言葉自体が混乱を招く結果にもなっていると僕は思います。なので、ここでは便宜上、 プロトタイプのことをクラス、あるいはオブジェクトと呼ぶこととします。

あと、以下のプログラムはGoogle chromefirefoxでしかテストしていません。念のため。

空のクラスを定義

まずTestクラスを実装してみます。

var Test = function() {
  // プロパティ、メソッドなどをこの中に記述
};


これがJavaScriptのクラスです。見てのとおり、厳密にはクラスではありません。関数にクラスとしての役割を与えることで クラスっぽい挙動をさせるのです。
ちなみにクラスなので変数の最初は大文字です。そうしないと混乱を招くので必ずそうしましょう。

インスタンス化は

var test = new Test();

と記述します。これは他言語と同じような感じですね。

プロパティとメソッドを実装

では上記のクラスにプロパティ、メソッドを書いてみましょう。

var Test = function(name, score) {

  // プロパティ(Javaだとフィールドにあたる)
  this.name = name;
  this.score = score;

  // メソッド
  this.getData = function() {
    return this.name + " : " + this.score;
  }

};


こんな感じになります。
メソッドに注目してください。これはメソッドというよりはプロパティに関数を突っ込んでいるだけ、とも 言えます。実際、JavaScriptではメソッドという概念があるとは言えず、値が関数であるプロパティが メソッドとしてみなされます。しかし、大体どこでもメソッドと言っている気がしますし、ここでもメソッドで通します。
あと、直観でわかる人もいると思いますがthisは自分自身(ここではTestクラス自身)です。(正確には、JavaScriptのthisは、 Javaなんかとちょっと違うのですが、ここでは割愛します)

本当に呼び出せるか実際に実行してみます。

var test = new Test("hoge-kun", 75);
console.log(test.getData());


hoge-kun : 75と表示されればOKです。

メソッドの後づけ定義

JavaScript一旦インスタンス化させたオブジェクトに対し、新しくメソッドを追加することができます。
言っている意味がわからない人も多いと思いますので具体例を出します。

var Test = function(name, score) {

  this.name = name;
  this.score = score;

  this.getData = function() {
    return this.name + " : " + this.score;
  }

};

var test = new Test("hoge-kun", 75);

// 名前だけ返すgetNameメソッドを追加
test.getName = function() {
  return this.name;
}

console.log(test.getName());


ちゃんとhoge-kunとだけかえってきましたか?
こんなことできていいのかよ、と他言語を使っていた人なら思うでしょう。
これがJavaScriptの柔軟性であり、同時にバグが混入しやすくなる原因でもあります。

たとえば、下記のコードを見てみましょう。

var Test = function(name, score) {

  this.name = name;
  this.score = score;

  this.getData = function() {
    return this.name + " : " + this.score;
  }

};

// hoge-kunとfuga-chanのTestオブジェクトをインスタンス化
var test_hoge = new Test("hoge-kun", 75);
var test_fuga = new Test("fuga-chan", 90);

// 変数test_hogeだけに、名前だけ返すgetNameメソッドを追加
test_hoge.getName = function() {
  return this.name;
}

console.log(test_hoge.getName());
console.log(test_fuga.getName());


TypeError: test_fuga.getName is not a functionといったような エラーが返ってきたはずです。そう、メソッドを追加したのは変数test_hogeだけであり、test_fugaには追加 されていないからです。
このようにJavaScriptでは、同じクラスのもとにインスタンス化されたオブジェクトであろうと、それぞれのインスタンスが持つメソッドは 同じとは限らないのです。

メソッドはプロトタイプで宣言しよう

先にコードを出します。

// -- Testクラス(Testオブジェクト) --

// プロパティ(フィールド)はTestクラスをつくったときに
var Test = function(name, score) {
  this.name = name;
  this.score = score;
};

// メソッドはprototypeプロパティへ入れる
Test.prototype = {
  getData : function() {
    return this.name + " : " + this.score;
  },

  getName : function() {
    return this.name;
  }

};
// -- Testクラスここまで(実際はここまででファイルを分ける) --

// -- ここからメイン処理 --
var test_hoge = new Test("hoge-kun", 75);
var test_fuga = new Test("fuga-chan", 90);

console.log(test_hoge.getName());
console.log(test_fuga.getName());


今回はhoge-kun fuga-chanがともに返ってきます。これがJavaScriptオブジェクト指向の完成形・・・というわけでは ないのですが、とりあえずそうしておいてください。少なくとも僕はこう書いています。
この急に出てきた 「prototypeってなに?」 「なんでクラスのなかにメソッド入れてしまわないの?」 と思われる方 もいると思いますが、これを使うといろいろ利点があります。その内の一つが

メモリ消費を抑えることができる

です。

インスタンスは生成するたびに、それぞれのインスタンスのためのメモリを消費します。メソッドをプロトタイプ宣言で追加する前のコードでは、 メソッドをインスタンス化のたびにコピーしていたわけです。

f:id:web-engine:20161023190700p:plain

これらのメソッド群をprototypeプロパティで追加すると、オブジェクトをインスタンス化したとき、インスタンスはそれぞれ個別のメソッドを 持つわけではなく、もととなったprototypeオブジェクトを参照することとなります。
図で説明するとこんな感じですね。

f:id:web-engine:20161023190721p:plain

毎回メソッドをコピーするわけではなく同じアドレスを見に行っているだけなので、結果としてメモリ節約になるわけです。

ちなみに、上記コードでメソッドをprototypeプロパティへ格納していますが、この手法はオブジェクトのリテラル表現を使っています。

基本形は

var Test = function() { // プロパティ };
Test.prototype.getData() { // 処理 };
Test.prototype.getName() { // 処理 };


のように書きます。しかし、これでは修正箇所が多くなるので、以下のように書くと良いでしょう。

var Test = function() { // プロパティ };
Test.prototype = {
   getData : function() {
    // 処理
  },

  getName : function() {
    // 処理
  }
  
};


読みやすくもなって一石二鳥です。

カプセル化は面倒くさい

PHPの場合

<?php
class Test {

  private $name;
  private $score;

  function __construct($name, $score) {
    $this->name = $name;
    $this->score = $score;
  }

  // $nameは、このメソッドを通しでしか参照できない
  public function getName() {
    return $this->name;
  }

  // Testクラス内でしか呼び出せないメソッドの処理
  private function secretMethod() {
    // 処理
  }

}


のようにしてメソッドなどを自分自身のクラス内でしか呼び出せないようにします。$name$scoreも パブリックメソッドを通してでしか参照できないようにできます。クラスを設計する際は、通常このようにして 安全性を向上させます。これが俗に言うカプセル化です。

JavaScriptではデフォルトで、すべてのメンバがパブリックになっています。そして、JavaScriptでも カプセル化を施すことは可能です。
可能なんですが、ちょっと面倒くさいというか、コードが非常に冗長的になるというか・・・。

どうしても仕事で使っていてプライベートメンバにして「危険を取り除きたいんだ!」という方は、ほかの人が良い 方法を書いてくれているはずなので検索してみてください。(投げやり)


静的プロパティ・メソッドの定義

静的プロパティ、静的メソッドとは、インスタンス化せずともオブジェクトから直接呼び出せるプロパティ、メソッド。 グローバル変数、関数を減らすために使用されることが多いです。
以下のように実装します。

/*
 -- 構文 --
 オブジェクト名.プロパティ名 = 値;
 オブジェクト名.メソッド名 = function() { 処理 };
*/

// クラス定義
var StaticTest = function() {};

// 静的プロパティ定義
StaticTest.VERSION = "2.0";

// 静的メソッド定義
StaticTest.returnName = function () {
  return "STATIC_TEST!";
};

// -- メイン処理 --
console.log(StaticTest.VERSION);
console.log(StaticTest.returnName());

var static_test = new StaticTest();
console.log(static_test.returnName()); // エラー


インスタンス経由で静的メソッドを呼び出そうとするとエラーになるはずです。

上記のエラーからわかるように、静的プロパティ及びメソッドを定義する際は、プロトタイプオブジェクトには追加できません。 プロトタイプオブジェクトはインスタンスから直接参照されることを前提としたものだからです。

2017.12.30 追記

ECMAScript 6 からはよりほかの言語に近い形でクラス定義を行えるようになっていました。 結構前からこのように記述できたみたいですね。リサーチ不足でした。

今後はこちらの書き方を見る機会が増えるかもしれません。

<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>ほかの言語に近い感じで書けるようになった</title>
<script>
class Student {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    info() {
        console.log(`${this.name} : ${this.age}`);
    }
}
const tanaka = new Student('Taro Tanaka', 15);
tanaka.info();
</script>
</head>
<body>
</body>
</html>

結果

Taro Tanaka : 15

constは変数を変更できないようにします。変数tanakaには、新しく数値とかを代入できなくするわけです。(代入するとエラーになる)
今は、変更しないものはconstで定義してしまうのが主流みたいです。変更する必要がある場合はletを使います。

for (var i = 0; i < 10; i++) {
}
console.log(i);  // 数値が出力されてしまう

varを使っていた場合は漏れちゃってました。

for (let i = 0; i < 10; i++) {
}
console.log(i);  // エラーが出力される

letを使用した場合には、厳密にスコープ内でおさまってくれます。多言語を使っている人からすれば、まあ、こっちが普通ですから 安心して書けます。