JavaScriptはとても強力な言語です。強力がゆえ、実はプロトタイプをデザインしたり、オブジェクトのインスタンスを生成したりするのに何種類もやり方があります。それぞれの方法には長所も短所もあります。そこでJavaScript初心者の皆さんのために私がそのあたりを整理して説明したいと思います。今回の投稿は、以前私が書いた「JavaScriptを分類するな」の続編です。前回の投稿でたくさんのコメントをいただき、コードの例を出してほしいをいう反応をいただいたので、今回はそれらにお応えします。
JavaScriptはプロトタイプを使って継承
つまり、JavaScriptでは、オブジェクトは別のオブジェクトを継承することができます。{ }
波括弧を使って生成されるJavaScriptの基本的なオブジェクトは、唯一のプロトタイプとしてObject.prototype
をプロトタイプに持ちます。Object.prototype
はそれ自身もオブジェクトであり、Object.prototype
のメンバ全てが、全てのオブジェクトからアクセスが可能です。
[ ]
角括弧を使って生成された、基本的な配列は、Object.prototype
やArray.prototype
などの数種類のプロトタイプを持ちます。つまり、Object.prototype
とArray.prototype
の全てのメンバは、配列のメンバ同様にアクセス可能なのです。.valueOf
やtoString
のように、重複するメンバは一番近いプロトタイプによってオーバーライドされます。今回の場合はArray.prototype
です。
プロトタイプの定義とオブジェクトのインスタンス化
メソッド1:コンストラクタパターン
JavaScriptは、コンストラクタ関数という特殊な関数を持っています。これは他の言語のコンストラクタと似た働きをします。コンストラクタは強制的にnew
キーワードをつけて呼び出され、コンストラクタ関数によって生成されたオブジェクトにthis キーワードをbindします。典型的なコンストラクタは以下です。
function Animal(type){ this.type = type; } Animal.isAnimal = function(obj, type){ if(!Animal.prototype.isPrototypeOf(obj)){ return false; } return type ? obj.type === type : true; }; function Dog(name, breed){ Animal.call(this, "dog"); this.name = name; this.breed = breed; } Object.setPrototypeOf(Dog.prototype, Animal.prototype); Dog.prototype.bark = function(){ console.log("ruff, ruff"); }; Dog.prototype.print = function(){ console.log("The dog " + this.name + " is a " + this.breed); }; Dog.isDog = function(obj){ return Animal.isAnimal(obj, "dog"); };
このコンストラクタは、別の言語のインスタンス化のやり方に似ています。
var sparkie = new Dog("Sparkie", "Border Collie"); sparkie.name; // "Sparkie" sparkie.breed; // "Border Collie" sparkie.bark(); // console: "ruff, ruff" sparkie.print(); // console: "The dog Sparkie is a Border Collie" Dog.isDog(sparkie); // true
bark
とprint
は全てのDogに適用されるプロトタイプのメソッドです。name
とbreed
のプロパティはコンストラクタによって設定された自身のプロパティです。通常は、全てのメソッドがプロトタイプ内に設定され、全てのプロパティはコンストラクタによって設定されます。
メソッド2:ES2015 (ES6) のclassの定義
class
は、初期のころからずっと、将来的に使用されるJavaScriptの予約済みキーワードという位置づけでしたが、今では実際に使われるようになりました。JavaScriptにおけるclassの定義は、他の言語での定義と大変よく似ています。
class Animal { constructor(type){ this.type = type; } static isAnimal(obj, type){ if(!Animal.prototype.isPrototypeOf(obj)){ return false; } return type ? obj.type === type : true; } } class Dog extends Animal { constructor(name, breed){ super("dog"); this.name = name; this.breed = breed; } bark(){ console.log("ruff, ruff"); } print(){ console.log("The dog " + this.name + " is a " + this.breed); } static isDog(obj){ return Animal.isAnimal(obj, "dog"); } }
このシンタックスは多くの人が好んで使います。理由は、このシンタックスがコンストラクタ、スタティック、プロトタイプメソッド宣言を、1つの優れたブロックの中に兼ね備えているからです。使い方はコンストラクタメソッドと全く同じです。
var sparkie = new Dog("Sparkie", "Border Collie");
メソッド3:明示的なプロトタイプの宣言、Object.create、ファクトリメソッド
このメソッドでは、class
シンタックスの仕組みの背後にあるプロトタイプ継承を表示しています。また、new
キーワードを省くことが可能です。
var Animal = { create(type){ var animal = Object.create(Animal.prototype); animal.type = type; return animal; }, isAnimal(obj, type){ if(!Animal.prototype.isPrototypeOf(obj)){ return false; } return type ? obj.type === type : true; }, prototype: {} }; var Dog = { create(name, breed){ var dog = Object.create(Dog.prototype); Object.assign(dog, Animal.create("dog")); dog.name = name; dog.breed = breed; return dog; }, isDog(obj){ return Animal.isAnimal(obj, "dog"); }, prototype: { bark(){ console.log("ruff, ruff"); }, print(){ console.log("The dog " + this.name + " is a " + this.breed); } } }; Object.setPrototypeOf(Dog.prototype, Animal.prototype);
このシンタックスは大変優れていて、プロトタイプがとても明示的に定義されます。どちらがプロトタイプのメンバで、どちらがオブジェクトのメンバかがはっきりと分かります。また、Object.create
の優れている点は、ある特定のプロトタイプからオブジェクトを生成できるところです。どちらの場合も、.isPrototypeOf
によるチェックは可能です。使い方は異なりますが、大きな違いではありません。
var sparkie = Dog.create("Sparkie", "Border Collie"); sparkie.name; // "Sparkie" sparkie.breed; // "Border Collie" sparkie.bark(); // console: "ruff, ruff" sparkie.print(); // console: "The dog Sparkie is a Border Collie" Dog.isDog(sparkie); // true
メソッド4:Object.create、top-levelファクトリ、プロトタイプポスト宣言
これはメソッド3をわずかに変えたメソッドです。このメソッドでは、ファクトリはクラス、対クラスはファクトリメソッドを含むオブジェクトとなっています。コンストラクタの例(メソッド1)に似ていますが、代わりにファクトリとObject.create
を使います。
function Animal(type){ var animal = Object.create(Animal.prototype); animal.type = type; return animal; } Animal.isAnimal = function(obj, type){ if(!Animal.prototype.isPrototypeOf(obj)){ return false; } return type ? obj.type === type : true; }; Animal.prototype = {}; function Dog(name, breed){ var dog = Object.create(Dog.prototype); Object.assign(dog, Animal("dog")); dog.name = name; dog.breed = breed; return dog; } Dog.isDog = function(obj){ return Animal.isAnimal(obj, "dog"); }; Dog.prototype = { bark(){ console.log("ruff, ruff"); }, print(){ console.log("The dog " + this.name + " is a " + this.breed); } }; Object.setPrototypeOf(Dog.prototype, Animal.prototype);
このメソッドの優れている点は、使い方はメソッド1と似ているのに、new
キーワードを要求せず、instanceof.
を使うところです。new
を用いないで、メソッド1と同じやり方をすれば使えます。
var sparkie = Dog("Sparkie", "Border Collie"); sparkie.name; // "Sparkie" sparkie.breed; // "Border Collie" sparkie.bark(); // console: "ruff, ruff" sparkie.print(); // console: "The dog Sparkie is a Border Collie" Dog.isDog(sparkie); // true
比較
メソッド1 vs メソッド4
メソッド4ではなくメソッド1を使う理由はほとんどありません。メソッド1では,
コード内でnew
を使うか、コンストラクタで以下のようにチェックしなければいけません。
if(!(this instanceof Foo)){ return new Foo(a, b, c); }
この場合も、ファクトリ内でObject.create
を使うだけでいいかもしれません。また、this
がめちゃくちゃになってしまうので、コンストラクタ関数上でFunction#call
やFunction#apply
を使うこともできません。上記のチェックをすれば、この問題が是正されますが、可変長の引数を用いたい場合は、ファクトリを使わなければいけません。
メソッド2 vs メソッド3
コンストラクタとnew
に関し、上記で適用したのと同じ理由がこの比較にも適用されます。instanceof
チェックは、class
シンタックスをnew
なしで、あるいはFunction#apply
とFunction#call
を用いて使う場合に不可欠です。
私の見解
プログラマは明瞭なコードを追求すべきです。メソッド3の明示的なシンタックスは、非常にはっきりと何が行われているのかを表しています。またそれは容易な複数継承と連結継承を可能にします。new
キーワードの使用は、apply
やcall
と適合せず、開放/閉鎖原則を破ることになるので、避けましょう。class
キーワードは、プロトタイプを使うJavaScriptの継承の性質を、古典的なシステムに見せかけて隠しています。
“シンプルは器用に勝る”、だから、より”洗練されている”とされる古典的なシンタックスを使う、というのは、不必要な技術コストです。
Object.create
は、bindされたthis
変数とnew
よりもさらに直感的で明確です。また、プロトタイプはファクトリそのもののスコープ外にあるオブジェクトの中に格納され得るので、修正と改善がより簡単です。ちょうどES6クラスのようにメソッドと定義シンタックスを使うこともできます。
classキーワードは、おそらくJavaScriptの中で最も有害な特徴ではないか。私は、規格化の作業に携わった、才気に富み、過酷な努力をいとわない人々に多大なる敬意を払うが、たとえ才気に富む人々でも時には過ちを犯すものだ。 - Eric Elliott
不必要な、場合によっては破壊的な何かを追加し、言語の本来の性質に逆らうのは間違った動きです。
あなたがclass
の利用を選ぶなら、私は自分がそのコードのメンテナンスに携わらなくて済むように願います。私の考えでは、開発者はコンストラクタ、class
、new
の利用を避け、より密接に言語の構造に沿った継承メソッドを使うべきです。
用語集
Object.assign(a, b)
は、オブジェクトb
の全てのenumerableなプロパティをオブジェクトa
上にコピーし、オブジェクトa
を返します。
Object.create(proto)
は、proto
をプロトタイプに持つ新規オブジェクトを生成します。
Object.setPrototypeOf(obj, proto)
はobj
の内部[[Prototype]]
プロパティを、proto
に設定します。