Нужны ли в JavaScript классы?

от автора

JavaScript принято считать прототип-ориентированным языком программирования. Но, как ни странно, этим подходом практически никто не пользуется: большинство популярных JS-фреймворков явно или неявно оперируют классами.
В этой статье я хочу рассказать об альтернативном способе программирования на JavaScript, без использования классов и конструкторов — чистым прототип-ориентированным ООП и особенностях его реализации на ECMA Script 5.

ООП можно разделить на две группы: класс-ориентированное (классическое) и прототип-ориентированное. Классический подход отражает взгляд Аристотеля на мир, в котором всё описывается идеальными понятиями. Прототипное ООП ближе к философии Людвига Витгенштейна, которая не полагается на строгую категоризацию и классификацию всего и вся, а пытается представить понятия предметной области материальными и интуитивно понятными (насколько это возможно). Типичным аргументом в пользу прототипирования является то, что обычно намного проще сначала разобраться в конкретных примерах, а только потом, изучая и обобщая их, выделить некоторые абстрактные принципы и впоследствии их применять.

JavaScript, согласно этой классификации, находится где-то посередине: с одной стороны, в нем присутствуют прототипы, с другой — классы и оператор new, как средство создания новых объектов, что не свойственно прототип-ориентированному подходу.

Классы

В JavaScript нет классов, скажете вы. Я бы не стал так утверждать.
Под классами в JS я подразумеваю функции-конструкторы: функции, вызываемой при создании экземпляра (выполнении оператора new), со ссылкой на прототип — объект, содержащий свойства (данные) и методы (функции) класса.

Как известно, в ЕСМА Script 6 возможно таки введут ключевое слово class:

   class Duck{         constructor(name){             this.name = name;         },         quack(){             return this.name +" Duck: Quack-quack!";         }     }          /// Наследование      class TalkingDuck extends Duck{         constructor(name){             super(name);         },         quack(){             return super.quack() + " My name is " + this.name;         }     }          /// Инстанцирование      var donald = new TalkingDuck("Donald"); 

Но по сути, ничего существенного (например модификаторов public, private) данное нововведение не принесет. Это нечто иное, как синтаксический сахар для подобной конструкции:

    var Duck = function(name){     	this.name = name; 	};      Duck.prototype.quack = function(){         return this.name +" Duck: Quack-quack!";     };          /// Наследование  	var TalkingDuck = function(name){ 		Duck.call(this, name); 	}  	TalkingDuck.prototype = Object.create(Duck.prototype); 	TalkingDuck.prototype.constructor = TalkingDuck;  	TalkingDuck.prototype.quack = function(){         return TalkingDuck.prototype.quack.call(this) + " My name is " + this.name;     };          /// Инстанцирование      var donald = new TalkingDuck("Donald"); 

Следовательно, классы в текущей версии JS уже есть, только нет удобной синтаксической конструкции для их создания.
В конце-концов, давайте определимся, что же такое класс. Вот определение (из википедии):

Класс — разновидность абстрактного типа данных в ООП, характеризуемый способом своего построения. Суть отличия классов от других абстрактных типов данных состоит в том, что при задании типа данных класс определяет одновременно и интерфейс, и реализацию для всех своих экземпляров, а вызов метода-конструктора обязателен.

Следуя этому определению, функция-конструктор является классом:
Функция-конструктор это абстрактный тип данных? — Да.
Функция-конструктор (вместе с свойствами из прототипа) определяет одновременно и интерфейс, и реализацию? — Да.
Вызов конструктора при создании экземпляра обязателен? — Да.

Прототипы

Прототип отличается от класса тем, что:

  1. Это уже готовый к использованию объект, не нуждающийся в инстанцировании. Он может иметь собственное состояние (state). Можно сказать что прототип является классом и экземпляром объединенными в одну сущность, грубо говоря, Singleton’ом.
  2. Вызов конструктора при создании объекта (клонировании прототипа) не обязателен.

Суть прототипного ООП сама по себе очень простая. Даже проще чем классического. Сложности в JS возникают из-за попытки сделать его похожим на то, как это реализовано в Java: в Java создание новых объектов производится с помощью оператора new, применяемого к классу. В JS — аналогично. Но, т.к. JS вроде как прототипный язык, и классов в нем не должно быть по определению, было введено понятие функция-конструктор. Беда в том, что синтаксиса для нормального описания связки конструктор-прототип в JavaScript’e нет. В итоге имеем море библиотек, исправляющих это досадное упущение.
В прототип-ориентированном подходе нет оператора new, а создание новых объектов производится путем клонирования уже существующих.

Наследование

Итак, суть прототипного (делегирующего) наследования состоит в том, что один объект может ссылаться на другой, что делает его прототипом. Если при обращении к свойству/методу оно не будет найдено в самом объекте, поиск продолжится в прототипе, а далее в прототипе прототипа и т.д.

    var $duck = {         name: "",         quack: function(){             return this.name +" Duck: Quack-quack!";         }     };     var donald = {         __proto__: $duck,         name: "Donald"     };     var daffy = {         __proto__: $duck,         name: "Daffy"     };          console.log( donald.quack() ); // Donald Duck: Quack-quack!     console.log( daffy.quack()  ); // Daffy Duck: Quack-quack!     console.log( $duck.isPrototypeOf(donald) ); // true 

daffy и donald используют один общий метод quack(), который предоставляет им прототип $duck. С прототипной точки зрения donald и daffy являются клонами объекта $duck, а с класс-ориентированной — “экземплярами класса” $duck.
Eсли же добавить/изменить некоторые методы непосредственно в объекте donald (или daffy), тогда его можно будет считать еще и “наследником класса” $duck.

Не забываем, что свойство __proto__ не стандартизировано, и использовать его можно только для дебага. Официально манипулировать свойством __proto__ возможно методами Object.create и Object.getPrototypeOf, появившимися в ECMAScript 5:

    var donald = Object.create($duck, {         name: {value: "Donald"}     });     var daffy = Object.create($duck, {         name: {value: "Daffy"}     }); 

Инициализация

В отличии от класс-ориентированного подхода, наличие конструктора и его вызов при создании объекта на базе прототипа (клонировании) не обязателен.
Как же тогда инициализировать свойства объекта?
Простые, не калькулируемые значения по умолчанию для свойств можно сразу присвоить прототипу:

var proto = {     name: "Unnamed" }; 

А если нужно использовать калькулируемые значения, то вместе с ECMA Script 5 нам на помощь приходит:

Ленивая (отложенная) инициализация

Ленивая инициализация это техника, позволяющая инициализировать свойство при первом к нему обращении:

var obj = {     get lazy(){         console.log("Инициализация свойства lazy...");         // Вычисляем значение:         var value = "Лениво инициализированное свойство " + this.name;                  // Переопределяем свойство, для того чтобы при следующем         // обращении к нему, оно не вычислялось заново:         Object.defineProperty(this, 'lazy', {             value: value,              writable: true, enumerable: true         });         console.log("Инициализация окончена.");         return value;     },     // оставляем возможность инициализировать свойство      // самостоятельно, в обход функции-инициализатора      // (если это не будет влиять на согласованность объекта):           set lazy(value){         console.log("Установка свойства lazy...");         Object.defineProperty(this, 'lazy', {             value: value,              writable: true, enumerable: true         });     },     name: "БезИмени" }; console.log( obj.lazy ); // Инициализация свойства lazy... // Лениво инициализированное свойство БезИмени  console.log( obj.lazy );// Инициализатор не запускается снова // Лениво инициализированное свойство БезИмени  obj.lazy = "Переопределено";// Сеттер не запускается, т.к. свойство уже инициализировано console.log( obj.lazy ); // Переопределено 

К плюсам этой техники можно отнести:

  • Разбиение конструктора на более мелкие методы-аксессоры “автоматически”, как предотвращение появлению длинных конструкторов (см. длинный метод).
  • Прирост в производительности, т.к. не используемые свойства инициализироваться не будут.

Сравнительная таблица

Прототип Класс (ECMA Script 5) Класс (ECMA Script 6)
Описание типа данных («класса»)
var $duck = {   name: "Unnamed",   get firstWords(){     var value = this.quack();     Object.defineProperty(       this, 'firstWords',       {value: value}     );     return value;   },   quack: function(){     return this.name       +" Duck: Quack-quack!";   } }; 
var Duck = function(name){   this.name = name||"Unnamed";   this.firstWords = this.quack(); }; Duck.prototype.quack = function(){   return this.name     +" Duck: Quack-quack!"; }; 
class Duck{   constructor(name){     this.name = name||"Unnamed";     this.firstWords = this.quack();   },   quack(){     return this.name       +" Duck: Quack-quack!";   } } 
Наследование
var $talkingDuck = Object.create($duck);  $talkingDuck.quack = function(){   return $duck.quack.call(this)     + " My name is "     + this.name; }; 
var TalkingDuck = function(name){   Duck.call(this, name); }  TalkingDuck.prototype = Object.create(Duck.prototype);   TalkingDuck.prototype.constructor = TalkingDuck; TalkingDuck.prototype.quack = function(){   return TalkingDuck.prototype.quack.call(this)     + " My name is "      + this.name; }; 
class TalkingDuck extends Duck{   constructor(name){     super(name);   },   quack(){     return super.quack()       + " My name is "        + this.name;   } } 
Создание объектов-экземпляров
var donald = Object.create($talkingDuck); donald.name = "Donald"; 
var donald = new TalkingDuck("Donald"); 
var donald = new TalkingDuck("Donald"); 

Список использованной литературы:
Dr. Axel Rauschmayer — Myth: JavaScript needs classes
Antero Taivalsaari — Classes vs. prototypes: some philosophical and historical observations [PDF]
Mike Anderson — Advantages of prototype-based OOP over class-based OOP

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


Комментарии

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

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