一から勉強させてください( ̄ω ̄;)

最下級エンジニアが日々の学びをアウトプットしていくだけのブログです。

JavaScriptの継承について

先日ひさしぶりにJavaScript The Good Partsの「継承」の章を読み返したので、今回はそのアウトプットとして記事を書きたいと思います。


まず章の冒頭にこのようなことが書いてあります。


クラスを利用する言語では、オブジェクトはクラスのインスタンスであり、あるクラスは他のクラスを継承する事ができる。一方で、JavaScriptはプロトタイプ型の言語である。これはつまり、オブジェクトが他のオブジェクトから直接継承される事を意味する。


ふむ。これは重要ですね。

JavaScriptでもよくクラス型の言語を意識した書き方をされるときがありますが、これが却ってわかりにくくしているのではないかと。著者も「これはJavaScriptの設計ミスだ」とボロクソに書いてましたw


では、どう書けばいいのか…

本編に沿って、


1、疑似クラス型(これが非推奨なパターン)
2、プロトタイプ型
3、関数型


の順でそれぞれの継承パターンを見ていきたいと思います。


1、疑似クラス型

まず疑似クラス型のコードというのはよくあるコンストラクタ関数を定義して、newするやつですね。
サンプルコードを書いてみました。

var Animal = function(name){
	this.name = name;
};

Animal.prototype.get_name = function(){
	console.log(this.name);
}
Animal.prototype.breathe = function(){
	console.log("ふーふー");
};

var animal = new Animal("あにまる");
animal.get_name();//"あにまる"
animal.breathe(); //"ふーふー"

var Cat = function(name){
	this.name = name;
};

Cat.prototype = new Animal();
Cat.prototype.cry = function(){
	console.log("ニャーニャー");
};

var cat = new Cat("ねこ");
cat.get_name();//"ねこ"
cat.breathe(); //"ふーふー"
cat.cry(); //"ニャーニャー"


Animalを継承した疑似クラスのCatは、そのprototypeにAnimalのインスタンス(new Animal)をセットする事で作る事ができます。そして後はCatに好きなように拡張してやればOKです。

あ、ちなみに

Cat.prototype = Animal.prototype

とかやっちゃうとCat.prototypeを拡張したとき、Animal.prototypeも同時に拡張されちゃうので注意してくださいね。


これはよく見る書き方だと思いますし、理解もできるのですが、著者はこの書き方について次の2点から良くないと言っています。


・プライバシーが全くなく、すべてのプロパティがパブリックになってしまっている。

コンストラクタ関数を呼び出す際、もしnew演算子を付けるのを忘れてしまった場合、thisにグローバルオブジェクトがセットされてしまうので危険。(継承ではなく、ただのグローバル変数の上書きになってしまう)以下のような感じ↓

var Animal = function(name){
	this.name = name;
};

var animal = Animal("あにまる"); //new 付け忘れ!!
console.log(window.name); //"あにまる" グローバル変数nameができてしまう。


そして、「コンストラクタの頭文字を大文字にする等の慣習を取り入れるのも良いが、より良い方法はnewをそもそも使わない事だ」と述べています。

それが以降に書くプロトタイプ型や関数型になります。


2、プロトタイプ型

プロトタイプ型ではクラスを必要とせず、新しいオブジェクトが古いオブジェクトを継承していきます。
では、早速サンプルコードを。これ。

var animal = {
	name : "あにまる",

	get_name: function(){
		console.log(this.name);
	},

	breathe: function(){
		console.log("ふーふー");
	}
};

animal.get_name();//"あにまる"
animal.breathe(); //"ふーふー"


var cat = Object.create(animal);
cat.name = "ねこ";
cat.cry = function(){
	console.log("ニャーニャー");
};

cat.get_name();//"ねこ"
cat.breathe(); //"ふーふー"
cat.cry(); //"ニャーニャー"


ちなみにObject.createについて。
本の中では

if(typeof Object.create !== "function"){
	Object.create = function(o){
		var F = function(){};
		F.prototype = o;
		return new F();
	};
}

と定義して使ってましたが、Object.createはECMAScript5から標準で導入されているので、今回はなにも気にせず使いました。


こんな感じで古いオブジェクト(animal)を継承して新しいオブジェクト(cat)を作り、それを拡張することで1と同様のインスタンスが作成できました。(ベースとなったオブジェクトとの差分を記述して拡張することを差分継承というらしいです)

コンストラクタだとか、new演算子だとかは直接は使わなかったですね。たしかにシンプルだし、中途半端にクラスとか意識するよりもわかりやすいです。なんかJavaScriptっぽい感じがしますし。


でも著者はまだ攻めます。
これは「あるデータ構造を他のデータ構造から継承する際に便利だが、プライバシーがまったくない。プライベート変数やプライベートメソッドを利用することができないのだ。」と。これを解決するために「モジュールパターンを使え」と。


では次の関数型をみていきたいと思います。


3、関数型

今度はインスタンスのプライベート変数、プライベートメソッドも定義していきます。
早速サンプルコードを。こちら。

var animal = function(spec){
	var that = {};

	that.get_name = function(){
		console.log(spec.name);
	};

	that.breathe = function(){
		console.log("ふーふー");
	};
	
	return that;
};

var my_animal = animal({name: "あにまる"});
my_animal.get_name(); //"あにまる"
my_animal.breathe(); //"ふーふー"

var cat = function(spec){
	var that = animal(spec);

	that.cry = function(){
		console.log("ニャーニャー");
	};

	return that;
};

var my_cat = cat({name: "ねこ"});
my_cat.get_name();//"ねこ"
my_cat.breathe(); //"ふーふー"
my_cat.cry(); //"ニャーニャー"


こんな感じで2と同様、catはanimalを内部で呼び出すことによって、スムーズに差分継承できるようになってます。
nameもプライベートな状態になっており、get_nameから呼び出すことでのみアクセス可能になってますね。


本の中でも「関数型パターンは疑似クラス型よりも面倒が少なく、より良いカプセル化情報隠蔽を行うことができる」と書いてます。たしかに関数型は良い感じですね。


以上、本に沿って3つの継承パターンを見てきました。

著者的には2と3のパターン(とくに3?)をゴリ押ししてる感じでしたし、個人的にも変にクラスを意識しないほうが、よりプロトタイプ型のJavaScriptらしい記述ができると思いました。ま、状況に応じて使い分けが重要ってことですかね。


とくに3のモジュールパターンはJavaScriptの中でも特にクールな書き方だと思っているので、機会があればまた書きたいと思っています。


あ、最後に今回参考にさせていただいた本はこちら。名著です。


JavaScript: The Good Parts ―「良いパーツ」によるベストプラクティス

JavaScript: The Good Parts ―「良いパーツ」によるベストプラクティス


小さなことからコツコツと。