Выложу карты на стол: лично я считаю, что классы переоценены, и по причинам, которые, я допускаю, могут быть отчасти эстетическими, мне они просто не нравятся. Я не единственный, кто так думает. Я очень предпочитаю более функциональный подход, и даже когда я хочу или должен смешивать данные и поведение, я предпочитаю другие альтернативы, ставшие возможными благодаря гибкости JavaScript, такие как объекты Крокфорда, фабричные функции или просто объекты. литералы, или паттерн выявления модуля, который вы можете использовать, даже не осознавая этого, просто экспортируя функции из одного файла и импортируя их в другой.

Но я обещаю, что это образовательная статья, а не рекламная статья о пламенной войне.

Есть два основных связанных, но разных аргумента в пользу того, чтобы избегать ключевого слова «класс», а также таких ключевых слов, как «этот» и «новый» в JS:

(1) Это ложь. Зачем вам писать код, полный лжи? Даже если вы достаточно хорошо себя изучите, чтобы избежать недоразумений и потенциальных ошибок, не должны ли мы стремиться писать код, который четко выражает наши намерения?

(2) Есть, как уже упоминалось, другие способы достижения того, чего вы пытаетесь достичь, используя более идиоматический JS, который использует преимущества того, как работает JS, а не пытается замаскировать и скрыть это. Эти альтернативы могут иметь преимущества, по крайней мере, в некоторых ситуациях. Даже если они этого не делают, их использование может помочь вам понять JS на более глубоком уровне и расширить свой кругозор, увидев новые способы решения проблем, вместо того, чтобы помочь вам продолжать не делать этого.

Но согласны вы с этими аргументами или нет… Пишите ли вы классы на JS все время и любите его, или стремитесь к чистому стилю FP и неохотно используете ключевое слово «новое» только тогда, когда вас вынуждает сторонняя библиотека , или что-то среднее между ними, вам следует понять, как на самом деле работают «классы» в JS, и почему это полностью отличается от классов в других языках ООП.

Чтобы использовать инструменты наиболее эффективно, нам нужно понимать, как они работают, независимо от того, как мы думаем, что они должны работать или как мы хотели бы, чтобы они работали. Поэтому, чтобы хорошо разбираться в JS, жизненно важно понимать наследование прототипов.

Это подводит нас к первой причине, по которой вы можете избегать использования классов ES6: они фальшивые. В 2021 году в JS нет классов, по крайней мере, НАСТОЯЩИХ классов. У него есть синтаксический сахар для точно такого же наследования прототипов, которое всегда было в JS.

// "Class" syntax
class Dog {
  voice = "Woof!";
  speak() {
    console.log(this.voice);
  }
}
// Constructor function
function Perro() {
  this.voz = "Guau!";
}
Perro.prototype.hablar = function () {
  console.log(this.voz);
};
// Usage
const fido = new Dog();
fido.speak();
const firulais = new Perro();
firulais.hablar();

В чем разница (кроме того, что второй на испанском)? Никто. Первое — это просто синтаксический сахар для второго.

Но разве первый не выглядит красивее? Ну, я думаю, может быть. Выглядит довольно субъективно. Легче в использовании? Действительно? На самом деле я не понимаю, как, если вы потратите, например, 5 минут на то, чтобы научиться использовать вторую форму. (Особенно со всеми изменениями синтаксиса классов JS, которые претерпели за эти годы.) Я подозреваю, что основная мотивация заключается в том, что первый выглядит более знакомым разработчикам Java, C# и т. д.

В чем проблема. Потому что это ложь. Независимо от того, что вы думаете о Java/C#, JavaScript, несмотря на название, не похож на них и никогда не может быть таким. Поэтому синтаксис создает впечатление, что он делает что-то, чего на самом деле не делает, по-видимому, по замыслу. Это синтаксическое мракобесие.

(На самом деле, на мой взгляд, это самая большая проблема с JS… Это втайне своего рода блестящий язык, по общему признанию, с множеством недостатков… Но самый большой недостаток в том, что он из кожи вон лезет, чтобы замаскироваться под посредственную подделку других языков. , Когда на самом деле это что-то, что, возможно, лучше… и, конечно, возможно, хуже… но, несомненно, совершенно другое.)

Так что я имею в виду, что это не «настоящий» класс? Что ж, если вы знаете свой ООП 101, вы знаете, что класс — это «тип». Это как план. Вы не можете управлять чертежом автомобиля; вы можете использовать чертеж только для того, чтобы построить настоящую машину, которой затем сможете управлять. Точно так же, чтобы использовать класс (если это не статический класс), вам сначала нужно создать экземпляр этого класса, тем самым создав фактический объект, также известный как экземпляр.

Так как же в JS достигается фальшивое классоподобное поведение? С прототипным наследованием. Так как же работает прототипное наследование? Через цепочку прототипов.

И это ключевой момент: классическое наследование и прототипное наследование — принципиально разные модели наследования. Короче говоря, более или менее классическое наследование предполагает создание копий на основе шаблона; прототипное наследование, с другой стороны, позволяет совместно использовать код путем связывания объектов.

Следовательно, стиль ООП в JS можно описать аббревиатурой OLOO — объекты, связанные с другими объектами. Вы можете возразить, как это делают Кайл Симпсон и другие, что поэтому он более объектно-ориентирован, чем классические языки ООП (например, Java и MS Java, я имею в виду C#), которые могут быть более точно описывается как классово-ориентированное программирование (COP?). Но награды за самую объектно-ориентированную программу нет и быть не должно. Любите вы это или ненавидите, это просто другая парадигма.

Каждый объект содержит ссылку на объект, который является прототипом этого объекта. Когда мы вызываем fido.speak(), JS-движок проверяет объект «fido» на наличие свойства, называемого «speak». Он не находит его, поэтому получает прототип объекта и ищет свойство, называемое «говорить», на этом объекте. И он находит его, поэтому использует его.

Что, если бы у прототипа не было «говорения»? Он получит прототип прототипа и попытается найти там слово. И не нашел бы его там. Таким образом, он будет проверять прототип прототипа прототипа… Упс, у прототипа прототипа нет прототипа, поэтому значение говорить не определено. (Обратите внимание, что в JS, как ни странно, undefined — это значение.) Что происходит, когда вы пытаетесь вызвать undefined? Ну, undefined нельзя вызывать, поэтому вы получаете TypeError.

fido.bark(); // Uncaught TypeError: fido.bark is not a function
console.log(fido); // Dog {voice: 'Woof!'}
console.log(fido.__proto__); // {constructor: ƒ, speak: ƒ}
console.log(fido.__proto__.__proto__); // {constructor: ƒ, 
  // __defineGetter__: ƒ, __defineSetter__: ƒ,
  // hasOwnProperty: ƒ, __lookupGetter__: ƒ, …}
console.log(fido.__proto__.__proto__.__proto__); // null

Что это за .__proto__? свойство? Чем оно отличается от свойства .prototype («точка-прототип»), к которому мы присоединяем функции для создания «методов класса» с синтаксисом до ES6? И какое отношение любой из них имеет к [[Prototype]]вещи, которую вы можете увидеть в инструментах разработки? Хорошие вопросы! Честно говоря, я до сих пор разбираюсь сам. Я вернусь к вам.

Важным моментом сейчас является то, что мы можем видеть, что каждый объект имеет ссылку на другой, совершенно нормальный неспециальный объект, который является его прототипом. Просто обычная ссылка на объект, содержащаяся в более или менее обычном (но неперечислимом, т.е. Object.keys, циклах for-in и т. д., это не будет отображаться) свойстве.

fido — это экземпляр Dog, который мы создали с помощью new. Мы видим, что единственным собственным свойством фидо является voice, но в прототипе фидо есть метод speak, который мы можем вызвать для выхода из системы голоса фидо. И под методом я подразумеваю: функцию, ссылка на которую хранится в свойстве объекта. Помните, в JS функции — это первый класс, а классы — это даже не эконом-класс: их на самом деле не существует.

Так что же такое прототип прототипа? Что ж, мы видим, что среди прочего у него есть метод с именем hasOwnProperty… Хм… Звучит знакомо? Это Объект. И мы видим, что прототип Object (т. е. прототип прототипа фидо) равен… null. Мы достигли конца цепочки прототипов (или вершины иерархии наследования).

Если вы перейдете по ссылке, вы увидите, что там написано: Почти все объекты в JavaScript являются экземплярами Object. Я бы сказал, что эта терминология вводит в заблуждение.

Экземпляры подразумевают абстрактные типы, экземпляром которых может быть нечто, например: Вы делаете то, что меня раздражает — это общее понятие; Вчера вы оставили грязную одежду на полу — тому пример. Ваш питомец Фидо — экземпляр платоновского идеала Собаки.

В JS нет понятия типы или чертежи в этом смысле. Есть только объекты (экземпляры). Надеюсь, я уже ясно дал понять: объект и экземпляр — это синонимы. Это верно в целом, но в JS вдвойне, потому что им не с чем противопоставить; ничто для того, чтобы они были экземплярами of. Так что с этого момента я буду просто возражать.

Еще одна придирка к формулировке: часть «почти все» также в основном верна, но… Вы можете легко создать объект, который не наследуется от Object, если хотите, и вы можете изменить прототип любого объекта в любое время. Потому что, как мы установили, прототип объекта — это просто другой объект, связанный ссылкой, содержащейся в свойстве «дочернего» объекта.

Почему это имеет значение? Ну, вот один вывод:

Object.prototype.sit = () => console.log("Good boy.");
fido.sit(); // "Good boy."
Dog.prototype.sit = () => console.log("Woof woof, I'm already sitting.");
fido.sit(); // "Woof woof, I'm already sitting."

Если вы усвоили классическую модель наследования и думаете о типах, а не о конкретных экземплярах, это может удивить, а может быть, и обеспокоить. Если вы понимаете прототипное наследование, оно должно быть совершенно очевидным и ожидаемым.

Это позволяет вам делать подобные вещи, хотя многие считают это плохой практикой:

Array.prototype.double = function () {
  return this.map((x) => x * 2);
};
console.log([1, 2, 3].double()); // [2, 4, 6]

Несмотря на все разговоры о том, что классическое ООП якобы моделирует реальный мир… (Что я считаю (А) неверным, (Б) бесполезным, даже если бы это было правдой, поскольку наша цель — создавать программы, которые производят желаемое поведение, а не обязательно что-нибудь моделировать…)

Прототипное наследование, по-видимому, имеет немного больше общего с буквальным биологическим наследованием, чем с классическим наследованием. На самом деле виды на самом деле не существуют, кроме как… своего рода статистическая абстракция… более или менее…. На самом деле, конкретно существуют только отдельные организмы. Они не возникают, когда платоническая сущность вида инстанцируется… Они создаются, когда индивидуальный организм размножается… т. е. создает новый, индивидуальный организм.

И когда млекопитающее не знает, как добывать пищу (у него нет метода catchFood в качестве собственного свойства), оно «делегирует» задачу своему родителю, который, надеюсь, знает.

Опять же, я не думаю, что целью языков программирования является «моделирование реальности», так что это не аргумент в пользу того, что прототипное наследование обязательно лучше. Это просто метафора, помогающая понять, как это работает.

И эта метафора несостоятельна, потому что воспроизводство подразумевает своего рода копирование с модификацией. Настоящее наследование на основе классов предполагает копирование, но когда вы создаете новый объект из прототипа, он не является копией. Общее поведение достигается за счет связывания через свойство, содержащее ссылку.

Если я повторяюсь, то это потому, что это не может быть достаточно подчеркнуто: в JS есть только отдельные объекты. Наследование прототипов — это механизм повторного использования кода, который работает путем связывания объектов в цепочку прототипов, т. е. OLOO — объекты, связанные с другими объектами.

Итак, это основная концепция. Я обнаружил, что мне еще многое предстоит узнать о деталях того, как это работает. Как только разберусь, возможно, объясню в новом посте.

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