Если в названии JavaScript есть «Java», но это совершенно другой язык, это не единственное искажение, связанное с самым неправильно понимаемым языком программирования в мире. Действительно, всегда существовала проблема с тем, как большинство людей учили объектно-ориентированному программированию на JavaScript. Ресурсы для обучения были настолько плохи, что многие разработчики все еще упускают некоторые фундаментальные знания о JavaScript даже после многих лет практики.
На том основании, что это было бы более привычно, если бы мы пришли из фона Java, нам продали поддельную модель программирования на основе классов вместо того, чтобы принять элегантность и простоту прототипов. «Прототип» может показаться более сложным и незнакомым, чем класс, но эта концепция кладет конец дихотомии класса / экземпляра и имеет тенденцию делать объектное программирование более конкретным, устраняя ненужные абстракции.
Так почему же модель на основе классов преобладает, а прототипы почти не известны некоторым разработчикам JS?
История языка, родившегося под тенью Java
История начинается с рождения JavaScript, ранее называвшегося LiveScript, ранее называвшегося Mocha. В то время все было очень хаотично, и то, что стало JavaScript, было создано одним человеком, Бренданом Эйхом, за 10 дней. Тем не менее, Брендану удалось придумать несколько очень хороших функций, таких как функции как объекты первого класса и прототипное наследование. Но также есть какой-то странный, спешащий API, такой как Date, который имеет другое поведение, если вызывается с newoperator или нет

Об этом newoperator: он был добавлен как удобный способ создания объектов и может быть чем-то знаком Java-разработчикам, но оказывается излишне запутанным и запутанным.
Что не так с новым оператором?
Начнем с примера кода, который обычно можно найти в учебнике по объектно-ориентированному программированию ECMAScript 5:
function Car(model){
this.model = model;
this.speed = 0;
}
Car.prototype.wheels = 4;
Car.prototype.drive = function(){
this.speed = 120;
}
var car = new Car(“Ferrari”);
car.drive();
console.log(car.speed); // 120
Этот шаблон много лет использовался снова и снова в кодовых базах больших приложений. Его популярность возросла в 2000-х, вслед за Java и общей тенденцией ООП. Тем не менее, мы можем найти много предостережений:
Одна обязательная функция-конструктор
В программировании на основе классов конструктор необходим для преобразования модели в реальный объект, от класса к экземпляру. В программировании на основе прототипов модель является реальным объектом. Следовательно, функция-конструктор не должна быть обязательной. Нам просто нужен способ связать два объекта с помощью отношения isPrototypeOf. Конструктор может быть полезен в качестве ярлыка для запуска некоторых свойств и, возможно, для выполнения некоторых настроек, таких как прослушивание событий. Но в простых случаях объектные литералы могут работать без конструктора.
С другой стороны, более сложные случаи могут оправдать наличие нескольких конструкторов с разными интерфейсами. В Java существует несколько конструкторов, но в JavaScript шаблон оператора new требует наличия только одного конструктора.
Более того, ссылка, которую мы несем, относится не к объекту-прототипу, который мы хотим расширить, а к функции-конструктору, что очень сбивает с толку:
// if car is an instance of Car, then Car is my class ? car instanceof Car; // true // Nope ! Car is the constructor. car.constructor === Car; // true
У вас может не быть «constructor" property
Что касается этого свойства «конструктор», которое мы только что видели выше, что происходит, когда наш прототип уже имеет это свойство, например, для ссылки на конструктор автомобиля? Ну, просто молча ломаются вещи:
function Car(constructor, model){
this.model = model;
this.constructor = constructor;
this.speed = 0;
}
var golf = new Car("Volkswagen", "Golf");
golf.constructor === Car; // false !
Когда наследование стало таким запутанным?
Что, если мы хотим иметь подкатегорию автомобилей, которую обычно можно назвать «подклассом»? Это та часть, в которой большинство Java-разработчиков невежественны:
function Berline(constructor, model){
Car.apply(this, arguments); // equivalent of super();
}
Berline.prototype = new Car();
// the sub class has a property referring to an instance of the parent class ???
var volvo = new Berline("Volvo","S60");
Чтобы объяснить это, мы должны помнить, что прототипы - это «настоящие» объекты. Но Car - это не прототип, это конструктор дочерних прототипов. Не путайте и экземпляр, и его конструктор.
Object.getPrototypeOf(volvo); // Car {...}
Object.getPrototypeOf(volvo) === Car // false
Object.getPrototypeOf(Berline); // function Empty() {}
Все это действительно беспокоит и является частым источником ошибок. Мы вынуждены сохранять ссылку на конструкторы, но они бесполезны без newoperator.
Что это за хрень" ? И как я получил эти глобальные утечки?
При использовании new this больше не работает так же внутри тела функции конструктора. Он относится не к родительской области, как обычно, а к вновь созданному экземпляру. Это особенное поведение, которое опять же очень сбивает с толку новичков. Это также имеет серьезные последствия в виде ошибочного изменения родительского контекста, если мы забудем новый оператор для создания экземпляра:
var toyota = Car("Toyota","Yaris"); // oops
console.log(window.model); // Yaris ! We have a global leak
console.log(window.constructor); // Toyota !
// We have overwritten Window ! Big trouble ahead !
Зачем вообще нужно «новое»?
Любите эти старые добрые функции
Фабричные функции - это просто функции, возвращающие объекты, и предназначены для использования как конструкторы. Если вы уже кое-что знаете о функциональном программировании на JS, мне не нужно объяснять вам, насколько мощными и гибкими могут быть эти старые добрые функции. По правде говоря, нам не нужны особые функции или контекст вызова, чтобы создавать объекты так, как нам нравится.
Кроме того, стрелочные функции ES6 исправляют некоторые ошибки прошлого, выполняя некоторую очистку: у них нет связанного контекста (this разрешается в родительской области), и у них нет свойства «конструктор». Таким образом, фабричные стрелочные функции могут решить многие проблемы, о которых мы говорили выше, делая ваш код более явным и предсказуемым.
Object.create, чистый и простой
Сами по себе фабричные функции не решают задачу назначения прототипа. Мы можем динамически назначить новый прототип уже существующему объекту, используя Object.setPrototypeOf(obj, prototype) или напрямую переназначив нестандартное свойство __proto__. Динамическое назначение прототипа - еще одна интересная функция JavaScript, о которой мы еще не говорили, но для простоты и управляемости мы просто назначим прототип один раз с самого начала, используя Object.create.
Object.create - это метод ES5, единственная цель которого - создать объект из прототипа и в конечном итоге определить некоторые свойства одновременно. Давайте перепишем с ним наш код:
/* the base prototype of all objects is Object.prototype
Car prototype is an object
we assign to it the properties that all cars will have */
const Car = Object.create(Object.prototype);
Car.wheels = 4;
Car.drive = function(){
this.speed = 120;
};
// we create a car from the Car prototype
var golf = Object.create(Car);
golf.constructor = "Volkswagen";
golf.model = "Golf";
Object.getPrototypeOf(golf) === Car; // true ! Finally !
Наследование также становится намного проще:
const Berline = Object.create(Car); let peugeot = Object.create(Berline); console.log(peugeot.wheels); // 4
Новый новый?
Назначение всех свойств по одному после Object.create вызова может быть утомительным. Здесь могут помочь заводские функции. Если нам нужна функция-конструктор, мы можем просто объявить ее как фабричную функцию:
const makeCar = (constr, model) => Object.create(Car, {
constructor: { writable:true, configurable:true, value: constr },
model: { writable:true, configurable:true, value: model }
});
var volvo = makeCar("Volvo","S60");
Естественно, люди придумали свой собственный универсальный «новый» помощник. Вот что сделал Дуглас Крокфорд в 2011 году:

В эпоху, появившуюся после ES6, я выбрал гораздо более простую функцию:
// my custom factory function to replace the new operator
New = (proto, props) => Object.assign(Object.create(proto), props)
const Berline = New(Car, { seats: 4 })
const Mercedes = New(Berline, { constructor: "Mercedes" })
Конечно, эти помощники не являются обязательными, и вы можете свободно использовать и объявлять все виды фабричных функций и вспомогательных служебных программ в соответствии с вашими потребностями. Это просто пример, демонстрирующий, насколько легко получить такой же уровень удобства без использования оператора new.
Замена instanceof
Вы могли заметить, что оператор instanceof не работает должным образом при обращении к прототипу вместо конструктора. Ну можешь выбросить с new. Нам не нужен еще один оператор: мы можем заменить его на метод isPrototypeOf, объявленный в Object.prototype.
golf instanceof Car // TypeError: Expecting a function in instanceof check, but got #<Object> ; // use this instead: Car.isPrototypeOf(golf) // true
Это также поможет избавиться от образа мышления «класс / экземпляр». У каждого объекта есть прототип, и каждый объект может быть прототипом другого объекта.
А как насчет «класса»?
Оператор class - ложь. Добавление в спецификацию ES6 было очень спорным в сообществе JS. Вопреки тому, что это, кажется, указывает, в JavaScript нет классов, и этот оператор является просто синтаксическим сахаром над прототипным шаблоном. Он предназначен для использования с оператором new, поэтому имеет те же проблемы. Некоторые проблемы были частично решены, например, пропуск new при создании экземпляра класса вызывает исключение. Но в целом он не предлагает ничего особенного по сравнению с шаблонами на основе Object.create.
Зачем создавать еще один оператор, если мы только что доказали, что можем избавиться от двух? И зачем его называть class, если не для того, чтобы еще больше запутать и дезинформировать новичков? Для меня это нерешенный вопрос, но похоже, что JavaScript никогда полностью не отделится от Java.
Проблема отладки и названий функций
Один аргумент для конструкторов и new состоит в том, что, поскольку функции имеют определенный name, это имя можно использовать для целей отладки, показывая имя родительского прототипа при записи объектов в консоль.
Это действительно может быть полезно, но свойство hidden name - лишь одно из средств идентификации объекта. Если вы не хотите загрязнять пространство свойств, скажем, свойством __name__, тогда ES6 Symbol - хороший кандидат для добавления такого рода метаданных к объекту.
Затем нам просто нужно указать разработчикам использовать это настраиваемое свойство для форматирования наших объектов в консоли. Для этой цели Chrome предлагает customDevtoolFormatters API. Это позволило нам отформатировать наши объекты так:

Я сделал доказательство концепции этого Name метаданных + настраиваемого форматирования здесь:
https://github.com/sylvainpolletvillard/ named-objects
Примечания: эта закономерность теоретизировалась и объяснялась многими другими людьми до меня, особенно Кайлом Симпсоном в его книге Вы не знаете JS, которую я настоятельно рекомендую вам прочитать.