POSTD PRODUCED BY NIJIBOX

POSTD PRODUCED BY NIJIBOX

ニジボックスが運営する
エンジニアに向けた
キュレーションメディア

Peter Jaszkowiak

本記事は、原著者の許諾のもとに翻訳・掲載しております。


JavaScriptはとても強力な言語です。強力がゆえ、実はプロトタイプをデザインしたり、オブジェクトのインスタンスを生成したりするのに何種類もやり方があります。それぞれの方法には長所も短所もあります。そこでJavaScript初心者の皆さんのために私がそのあたりを整理して説明したいと思います。今回の投稿は、以前私が書いた 「JavaScriptを分類するな」 の続編です。前回の投稿でたくさんのコメントをいただき、コードの例を出してほしいをいう反応をいただいたので、今回はそれらにお応えします。

JavaScriptはプロトタイプを使って継承

つまり、JavaScriptでは、オブジェクトは別のオブジェクトを継承することができます。 { } 波括弧を使って生成されるJavaScriptの基本的なオブジェクトは、唯一のプロトタイプとして Object.prototype をプロトタイプに持ちます。 Object.prototype はそれ自身もオブジェクトであり、 Object.prototype のメンバ全てが、全てのオブジェクトからアクセスが可能です。

[ ] 角括弧を使って生成された、基本的な配列は、 Object.prototypeArray.prototype などの数種類のプロトタイプを持ちます。つまり、 Object.prototypeArray.prototype の全てのメンバは、配列のメンバ同様にアクセス可能なのです。 .valueOftoString のように、重複するメンバは一番近いプロトタイプによってオーバーライドされます。今回の場合は 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

barkprint は全てのDogに適用されるプロトタイプのメソッドです。 namebreed のプロパティはコンストラクタによって設定された自身のプロパティです。通常は、全てのメソッドがプロトタイプ内に設定され、全てのプロパティはコンストラクタによって設定されます。

メソッド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#callFunction#apply を使うこともできません。上記のチェックをすれば、この問題が是正されますが、可変長の引数を用いたい場合は、ファクトリを使わなければいけません。

メソッド2 vs メソッド3

コンストラクタと new に関し、上記で適用したのと同じ理由がこの比較にも適用されます。 instanceof チェックは、 class シンタックスを new なしで、あるいは Function#applyFunction#call を用いて使う場合に不可欠です。

私の見解

プログラマは明瞭なコードを追求すべきです。メソッド3の明示的なシンタックスは、非常にはっきりと何が行われているのかを表しています。またそれは容易な複数継承と連結継承を可能にします。 new キーワードの使用は、 applycall と適合せず、開放/閉鎖原則を破ることになるので、避けましょう。 class キーワードは、プロトタイプを使うJavaScriptの継承の性質を、古典的なシステムに見せかけて隠しています。

“シンプルは器用に勝る”、だから、より”洗練されている”とされる古典的なシンタックスを使う、というのは、不必要な技術コストです。

Object.create は、bindされた this 変数と new よりもさらに直感的で明確です。また、プロトタイプはファクトリそのもののスコープ外にあるオブジェクトの中に格納され得るので、修正と改善がより簡単です。ちょうどES6クラスのように メソッドと定義 シンタックスを使うこともできます。

classキーワードは、おそらくJavaScriptの中で最も有害な特徴ではないか。私は、規格化の作業に携わった、才気に富み、過酷な努力をいとわない人々に多大なる敬意を払うが、たとえ才気に富む人々でも時には過ちを犯すものだ。 - Eric Elliott

不必要な、場合によっては破壊的な何かを追加し、言語の本来の性質に逆らうのは間違った動きです。
あなたが class の利用を選ぶなら、私は自分がそのコードのメンテナンスに携わらなくて済むように願います。私の考えでは、開発者はコンストラクタ、 classnew の利用を避け、より密接に言語の構造に沿った継承メソッドを使うべきです。

用語集

Object.assign(a, b) は、オブジェクト b の全てのenumerableなプロパティをオブジェクト a 上にコピーし、オブジェクト a を返します。
Object.create(proto) は、 proto をプロトタイプに持つ新規オブジェクトを生成します。
Object.setPrototypeOf(obj, proto)obj の内部 [[Prototype]] プロパティを、 proto に設定します。