понедельник, 3 августа 2015 г.

Способы написания классов в JavaScript

Глоссарий

Object.assign(a, b) копирует все перечислимые (enumerable) свойства объекта b в объект a, а затем возвращает объект a
Object.create(proto) создает новый объект от указанного прототипа proto
Object.setPrototypeOf(obj, proto) меняет внутреннее свойство [[Prototype]] объекта obj на proto

Способ 1: Шаблон конструктор

function Animal(type){
  if (!(this instanceof Animal)){
    return new 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

Способ 2: Определение класса в ES2015 (ES6)

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");
  }
}

var sparkie = new Dog("Sparkie", "Border Collie");

Способ 3: Явное объявление прототипа, Object.create, фабричный метод

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 proto = Object.assign(Animal.create("dog"), Dog.prototype);
    var dog = Object.create(proto);
    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);
    }
  }
};

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, фабрика верхнего уровня, отложенный прототип

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 proto = Object.assign(Animal("dog"), Dog.prototype);
  var dog = Object.create(proto);
  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);
  }
};

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 против Способа 4

Существует довольно мало причин, для того чтобы использовать Способ 1 вместо Способа 4. Способ 1 требует либо использование ключевого слова new, либо добавление следующей проверки в конструкторе:
if(!(this instanceof Foo)){
  return new Foo(a, b, c);
}

В этом случае проще использовать Object.create с фабричным методом. Вы также не можете использовать функции Function#call или Function#apply с функциями-конструкторами, потому что они переопределяют контекст ключевого слова this. Проверка выше, может решить и эту проблему, но если вам нужно работать с неизвестным заранее количеством аргументов, вы должны использовать фабричный метод.

Способ 2 против Способа 3

Те же рассуждения о конструкторах и операторе new, что были упомянуты выше, применимы и в этом случае. Проверка с помощью instanceof необходима, если используется новый синтаксис class без использования оператора new или используются Function#call или Function#apply.

Синтаксис Способа 3 очень четко показывает, что именно происходит на самом деле. Он также позволяет легко использовать множественное наследование и стековое наследования. Так как оператор new нарушает принцип открытости/закрытости из-за несовместимости с apply или call, его следует избегать. Ключевое слово class скрывает прототипный характер наследования в JavaScript за маской системы классов.

Использование Object.create является более выразительным и ясным, чем использование связки new и this. Кроме того, прототип хранится в объекте, который может быть вне контекста самой фабрики, и таким образом может быть более легко изменен и расширен добавлением методов. Прям как классы в ES6.

Комментариев нет:

Отправить комментарий