JavaScript шаблоны наследования

от автора

Примечание переводчика: Тема наследования в JavaScript является одной из самых тяжелых для новичков. С добавлением нового синтаксиса с ключевым словом class, понимание наследования явно не стало проще, хотя кардинально нового ничего не появилось. В данной статье не затрагиваются нюансы реализации прототипного наследования в JavaScript, поэтому если у читателя возникли вопросы, то рекомендую прочитать следующие статьи: Основы и заблуждения насчет JavaScript и Понимание ООП в JavaScript [Часть 1]

По всем замечаниям, связанным с переводом, обращайтесь в личку.

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. Типичный конструктор может выглядеть следующим образом:

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 было зарезервировано в JavaScript с самого начала и вот наконец-то пришло время его использовать. Определения классов в JavaScript схоже с другими языками.

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, фабричный метод

Этот способ показывает, что на самом деле новый синтаксис с ключевым словом 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 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);     }   } }; 

Этот синтаксис удобен, потому что прототип объявляется явно. Понятно что определено в прототипе, а что определено в самом объекте. Метод 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, фабрика верхнего уровня, отложенный прототип

Этот способ является небольшим изменение способа 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 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);   } }; 

Этот способ интересен тем, что похож на первой способ, но не требует ключевого слова new и работает с оператором instanceOf. Использование такое же, как и в первом способе, но без использования ключевого слова new:

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.

Ключевое слово class, возможно будет наиболее пагубной чертой в JavaScript. Я испытываю огромное уважение к блестящим и очень трудолюбивым людям, которые были вовлечены в процесс написания стандарта, но даже блестящие люди иногда делают неправильные вещи. — Eric Elliott

Добавление чего-то ненужного и возможно пагубного, противоречащего самой природе языка является необдуманным и ошибочным.
Если вы решите использовать class, я искренне надеюсь, что мне никогда не придется работать с вашим кодом. На мой взгляд, разработчики должны избегать использования конструкторов, class и new, и использовать методы, которые более естественны парадигме и архитектуре языка.

Глоссарий

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

ссылка на оригинал статьи http://habrahabr.ru/post/263967/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *