Из-за огромного количества библиотек, инструментов и всевозможных вещей, которые облегчают вашу разработку, многие программисты начинают создавать приложения, не имея глубокого понимания того, как что-то работает под капотом. JavaScript - пример этого точного поведения. Хотя это один из самых сложных и наиболее распространенных языков, многих разработчиков привлекает использование инструментов более высокого уровня и абстрагирование «плохих частей» языка.
Вы по-прежнему сможете создавать потрясающие приложения, но погружение в водоворот JavaScript может быть для вас весьма полезным. Понимание «странных частей» - это то, что отличает среднестатистического программиста от старшего разработчика, и хотя экосистема JS постоянно меняется, основные принципы - это те, на которых построены все остальные инструменты. Понимание этого дает вам более широкое представление и меняет ваш взгляд на процесс разработки.
Наследование в JavaScript - это то, что обсуждается давно. Я начал изучать JS, пройдя учебный курс, который был направлен на то, чтобы дать нам практические знания, которые нам понадобятся для решения наших повседневных задач по программированию. Нам рассказали, что в плохом языке JavaScript отсутствуют объектно-ориентированные шаблоны и как мы должны ломать себе голову, чтобы они работали. Преподаватели быстро рассмотрели прототип и то, как мы можем имитировать поведение класса с его помощью, не вдаваясь в подробности того, как он работает (потому что они этого тоже не понимали).
Так что у меня осталось ощущение, что JS - это ущербный язык, потому что ему не хватает возможностей других языков для создания классов из коробки, поэтому нам пришлось проделывать всевозможные безумные махинации, чтобы он соответствовал тому, что мы считаем правильным. Затем я начал думать, что если это так сложно в обучении, ведет к нелогичному поведению и возникает множество препятствий, может быть, прототипы - неправильный подход. Прочитав книгу Кайла Симпсона о прототипе, я пришел к выводу, что многие люди винят JavaScript в собственной неспособности изучать новые концепции. В JS есть свои особенности, и я расскажу, как работает прототипное наследование и в чем заключаются его проблемы.
Что такое прототип?
Почти все объекты в JavaScript имеют свойство prototype. Используя его и, в частности, цепочку прототипов, мы можем имитировать наследование. Прототип - это ссылка на другой объект, и он используется всякий раз, когда JS не может найти свойство, которое вы ищете для текущего объекта. Проще говоря, всякий раз, когда вы вызываете свойство объекта, а его не существует, JavaScript переходит к объекту-прототипу и ищет его там. Если он его найдет, он будет его использовать, если нет, он перейдет к свойству этого объекта и посмотрит там. Это может всплыть до Object.prototype перед возвратом undefined. В этом суть цепочки прототипов и поведения, лежащего в основе наследования JavaScript.
Приведенный выше код может показаться сложным, когда вы впервые встречаетесь с прототипами, давайте разберемся с ним. Мы начнем со строки 20 - с ключевым словом new мы создаем новый объект, используя функцию конструктора Dog в строке 9. Это дает нам объект со свойством name и функцией makeSound, связанной с его прототипом. . Когда мы вызываем makeSound, он выполняется в контексте текущего объекта (собаки), и мы получаем правильный результат.
Когда мы вызываем sleep (), его явно не существует в Dog, поэтому он идет вверх по цепочке прототипов до Animal. Он находит это там и называет. В последней строке мы вызываем функцию missing (), которая нигде не определена. Он будет проходить весь путь вверх по цепочке прототипов до Object.prototype и, поскольку не найдет его там, выдаст ошибку.
Как видите, мы нигде не использовали слово «класс», мы не определили класс, расширяющий базовый, или что-то в этом роде. Это больше похоже на делегирование, чем на наследование. Каждый объект имеет это свойство прототипа, которое указывает на другой объект, которому следует делегировать ответственность в случае, если свойство, которое мы ищем, не найдено в текущем. В этом нет ничего особенного, объект просто делегирует ответственность своему начальнику, когда он не может справиться с задачей.
Затенение
Если мы еще раз посмотрим через призму наследования, мы поймем, что нам часто приходится переопределять свойства и методы. В прототипном наследовании это называется затенением.
Что мы здесь делаем, так это создаем свойство с тем же именем в прототипе Dog, поэтому, когда мы его вызываем, оно найдет его там и остановит всплытие цепочки прототипов. Как видите, мы нигде не используем ключевое слово override, мы просто объявляем свойство для объекта Dog, поэтому JavaScript не будет искать его в цепочке прототипов.
Проблемы с наследованием
Если вы читали мои предыдущие статьи с таким же названием, здесь я говорю вам, что JavaScript всегда имеет свои особенности, и их неправильное понимание может вызвать у вас большую головную боль. В какой-то момент вам, возможно, придется использовать затенение для свойства, но затем внутри него вызовите «родительскую» функцию с тем же именем. Хотя на большинстве других языков вы можете просто использовать super (), здесь все немного сложнее. Если вы воспользуетесь этим подходом и вызовете this.sleep () внутри затененной функции, вы в конечном итоге вызовете ту же функцию, и результатом будет рекурсия. Поэтому мы должны думать о другом способе решения проблемы.
Dog.prototype.sleep = function() { Animal.prototype.sleep(); }
Сначала это звучит как хороший подход, и это почти так, но если вы выполните его, вы увидите, что не получите ожидаемого результата, потому что контекстная привязка неверна.
Dog.prototype.sleep = function() { Animal.prototype.sleep.call(this); }
Однако, выполняя его таким образом, мы вызываем функцию на Animal, используя контекстную привязку текущей функции. Другими словами, мы вызываем функцию сна Animal, используя текущие данные Dog, я не могу объяснить это намного проще.
Концепция наследования
Для меня одной из самых сложных вещей, которые нужно было понять, было значение ключевого слова prototype. Я пытался мысленно установить связь между словом и процессом наследования, пока не понял, что на самом деле они не имеют никакой связи. Мы просто злоупотребляем делегированием поведения прототипа, чтобы имитировать наследование, хотя на самом деле у нас его нет. Чтобы дать вам еще один пример того, чем наша концепция иерархий на основе классов отличается от прототипов JS, я представляю вам свойство constructor.
Это свойство Object.prototype, которое возвращает функцию-конструктор, создавшую объект, для которого мы вызываем это. Не название функции, а сама функция. Как вы думаете, что произойдет, если мы войдем в консоль в свойстве конструктора переменной dog? Вы, вероятно, ожидаете, что он вернет функцию Dog, но вы будете удивлены.
console.log(dog.constructor); // [Function: Animal]
Чего ждать? Это возвращает функцию Animal, потому что она присоединяется к свойству Dog при создании объекта. Это еще раз показывает, что эти методы и свойства не были реализованы с учетом объектно-ориентированного проектирования.
Классы ES6
Что касается ES6, можно сказать, что проблема иерархии наследования в JavaScript решена. Мы получили ключевое слово class вместе со всеми другими полезностями, которые идут с ним. Теперь мы можем определять классы, расширять их, использовать конструкторы и ключевое слово super для доступа к родительским методам без уродства приведенного выше кода.
Это выглядит намного лучше, если вы уже знакомы с классами. Он удалил ненужное повторение ключевого слова prototype, использование Object.create () и темную магию, которую мы должны были сделать, чтобы вызвать «родительскую» функцию. У вас также есть правильный синтаксис, и вам намного легче понять, что происходит. Код ES6 и приведенный выше код прототипа более или менее выполняют одно и то же, но код нового стандарта легче понять.
Теперь вам нужно иметь в виду, что этот синтаксис (по большей части) всего лишь усовершенствованная версия прототипа. Это действительно упрощает процесс разработки, облегчая понимание людьми, имеющими опыт работы на других языках. Но нужно учитывать то, что по той же причине - классы ES6 являются синтаксическим сахаром над прототипом - все еще есть некоторые проблемы.
Поскольку подход прототипа больше похож на делегирование, чем на реальное наследование, «родительские» объекты передаются как ссылка. В других языках при создании объекта вы получаете копию его функциональности, включая родительские классы. Следовательно, в JS дочерний объект может обращаться к «родительским» функциям, даже если они созданы после дочернего объекта. Это связано с тем, что прототип - это просто ссылка, а не конкретный объект. Это горячая линия, которую объекты используют, чтобы найти выход, когда им не хватает определенного свойства.
Заключение
До появления «настоящих» классов в ES6 вся концепция наследования была испорчена и (обычно) преподавалась неверным образом. Прототипы - это скорее инструмент делегирования, и они на самом деле не ведут себя как классы. Теперь, используя современный подход, мы можем в значительной степени создавать обычные иерархии классов и использовать для них понятный синтаксис, но остерегайтесь возможных проблем, которые могут возникнуть. Этот синтаксис - лишь красивый фасад того, что происходит внутри.
Если вас интересует дополнительный контент, связанный с JS, вы можете подписаться на мою рассылку новостей здесь или просмотреть другие статьи из той же серии: