Я знаю, что этот ответ опоздал на 3 года, но я действительно думаю, что текущие ответы не предоставляют достаточно информации о , как наследование прототипа лучше, чем классическое наследование .
Сначала давайте посмотримНаиболее распространенные аргументы Программисты JavaScript заявляют в защиту наследования прототипа (я беру эти аргументы из текущего пула ответов):
- Это просто.
- Это мощно.
- Это приводит к меньшему, менее избыточному коду.
- Он динамический и, следовательно, лучше для динамических языков.
Теперь все эти аргументы верны, но никто не удосужился объяснить, почему,Это все равно, что сказать ребенку, что изучение математики важно.Конечно, но ребенку все равно;и вы не можете сделать ребенка таким, как математика, сказав, что это важно.
Я думаю, что проблема с наследованием прототипов заключается в том, что это объясняется с точки зрения JavaScript.Я люблю JavaScript, но наследование прототипов в JavaScript неверно.В отличие от классического наследования, существует два образца наследования прототипа:
- Шаблон прототипа наследования прототипа.
- Шаблон конструктора наследования прототипа.
К сожалениюJavaScript использует шаблон конструктора прототипного наследования.Это потому, что когда был создан JavaScript, Брендан Айх (создатель JS) хотел, чтобы он выглядел как Java (с классическим наследованием):
И мы выдвигали его какмладший брат Java, так как дополнительный язык, такой как Visual Basic, был для C ++ в языковых семействах Microsoft в то время.
Это плохо, потому что когда люди используют конструкторы в JavaScript, они думают о конструкторах, наследуемых от другихконструкторы.Это не верно.В прототипе объекты наследования наследуются от других объектов.Конструкторы никогда не входят в картину.Это то, что смущает большинство людей.
Люди из таких языков, как Java, которые имеют классическое наследование, становятся еще более запутанными, потому что, хотя конструкторы выглядят как классы, они не ведут себя как классы.Как сказал Дуглас Крокфорд :
Эта косвенная информация была предназначена для того, чтобы сделать язык более знакомым для классически обученных программистов, но не смог этого сделать, как мы видим из очень низкого уровня.Мнение Java-программистов о JavaScript.Конструктор JavaScript не понравился классической толпе.Это также скрыло истинную прототипную природу JavaScript.В результате, очень мало программистов, которые знают, как эффективно использовать язык.
Вот он у вас.Прямо изо рта лошади.
Истинное наследование прототипа
Наследование прототипа - это все об объектах.Объекты наследуют свойства от других объектов.Это все, что нужно сделать.Существует два способа создания объектов с использованием наследования прототипов:
- Создание нового объекта.
- Клонирование существующего объекта и его расширение.
Примечание: JavaScript предлагает два способа клонирования объекта - делегирование и объединение .Впредь я буду использовать слово «клон», чтобы ссылаться исключительно на наследование через делегирование, а слово «копировать» - исключительно на наследование через конкатенацию.
Достаточно разговоров.Давайте посмотрим несколько примеров.Скажем, у меня есть круг радиуса 5
:
var circle = {
radius: 5
};
Мы можем вычислить площадь и окружность круга из его радиуса:
circle.area = function () {
var radius = this.radius;
return Math.PI * radius * radius;
};
circle.circumference = function () {
return 2 * Math.PI * this.radius;
};
Теперь я хочу создать еще одинкруг радиуса 10
.Один из способов сделать это:
var circle2 = {
radius: 10,
area: circle.area,
circumference: circle.circumference
};
Однако JavaScript предоставляет лучший способ - делегирование .Для этого используется функция Object.create
:
var circle2 = Object.create(circle);
circle2.radius = 10;
Это все. Вы только что сделали прототип наследования в JavaScript. Разве не так просто? Вы берете объект, клонируете его, меняете все, что вам нужно, и, эй, давай - у тебя есть новый объект.
Теперь вы можете спросить: «Как это просто? Каждый раз, когда я хочу создать новый круг, мне нужно клонировать circle
и вручную назначить ему радиус». Хорошо, решение состоит в том, чтобы использовать функцию для выполнения тяжелой работы за вас:
function createCircle(radius) {
var newCircle = Object.create(circle);
newCircle.radius = radius;
return newCircle;
}
var circle2 = createCircle(10);
Фактически вы можете объединить все это в один литерал объекта следующим образом:
var circle = {
radius: 5,
create: function (radius) {
var circle = Object.create(this);
circle.radius = radius;
return circle;
},
area: function () {
var radius = this.radius;
return Math.PI * radius * radius;
},
circumference: function () {
return 2 * Math.PI * this.radius;
}
};
var circle2 = circle.create(10);
Прототип наследования в JavaScript
Если вы заметили в вышеуказанной программе, функция create
создает клон circle
, присваивает ему новый radius
и затем возвращает его. Это именно то, что конструктор делает в JavaScript:
function Circle(radius) {
this.radius = radius;
}
Circle.prototype.area = function () {
var radius = this.radius;
return Math.PI * radius * radius;
};
Circle.prototype.circumference = function () {
return 2 * Math.PI * this.radius;
};
var circle = new Circle(5);
var circle2 = new Circle(10);
Шаблон конструктора в JavaScript является инвертированным прототипом. Вместо создания объекта вы создаете конструктор. Ключевое слово new
связывает указатель this
внутри конструктора с клоном prototype
конструктора.
Звучит запутанно? Это потому, что шаблон конструктора в JavaScript излишне усложняет вещи. Это то, что большинству программистов трудно понять.
Вместо того, чтобы думать об объектах, унаследованных от других объектов, они думают о конструкторах, унаследованных от других конструкторов, и затем полностью запутываются.
Существует целый ряд других причин, по которым следует избегать использования шаблона конструктора в JavaScript. Вы можете прочитать о них в моем блоге здесь: Конструкторы против прототипов
Так в чем же преимущества наследования прототипа по сравнению с классическим наследованием? Давайте снова рассмотрим наиболее распространенные аргументы и объясним , почему .
1. Прототип наследования прост
CMS утверждает в своем ответе:
На мой взгляд, основным преимуществом наследования прототипа является его простота.
Давайте рассмотрим, что мы только что сделали. Мы создали объект circle
с радиусом 5
. Затем мы его клонировали и дали клону радиус 10
.
Следовательно, для работы прототипного наследования нам нужны только две вещи:
- Способ создания нового объекта (например, литералов объекта).
- Способ расширения существующего объекта (например,
Object.create
).
В отличие от классического наследования гораздо сложнее. В классическом наследстве у вас есть:
- Классы.
- Объект.
- Интерфейсы.
- Абстрактные классы.
- Финал.
- Виртуальные базовые классы.
- Конструкторы.
- деструкторы.
Вы поняли идею. Дело в том, что наследование прототипов легче понять, легче реализовать и легче рассуждать.
Как сказал Стив Йегге в своем классическом сообщении в блоге " Портрет N00b ":
Метаданные - это любое описание или модель чего-то другого. Комментарии в вашем коде - это просто описание вычислений на естественном языке. Что делает метаданные метаданными, так это то, что они не являются строго необходимыми. Если у меня есть собака с какими-то племенными документами, и я теряю документы, у меня все еще есть вполне подходящая собака.
В том же смысле классы - это просто метаданные. Классы не являются строго обязательными для наследования. Однако некоторые люди (обычно n00bs) находят классы более удобными для работы. Это дает им ложное чувство безопасности.
Хорошо, мы также знаем, что статические типы - это просто метаданные. Это специализированный вид комментариев, предназначенный для двух типов читателей: программистов и компиляторов. Статические типы рассказывают историю о вычислениях, по-видимому, чтобы помочь обеим группам читателей понять цель программы. Но статические типы могут быть отброшены во время выполнения, потому что, в конце концов, это просто стилизованные комментарии. Они как родословная бумага: это может сделать неуверенного в себе человека более счастливым в отношении их собаки, но собаке, конечно, все равно.
Как я уже говорил ранее, занятия дают людям ложное чувство безопасности. Например, вы получаете слишком много NullPointerException
в Java, даже когда ваш код отлично читается. Я считаю, что классическое наследование обычно мешает программированию, но, возможно, это просто Java. У Python удивительная классическая система наследования.
2. Наследование прототипов является мощным
Большинство программистов, которые происходят из классического опыта, утверждают, что классическое наследование является более мощным, чем наследование прототипа, потому что оно имеет:
- Частные переменные.
- Множественное наследование.
Это утверждение неверно. Мы уже знаем, что JavaScript поддерживает закрытые переменные через замыкания , но как насчет множественного наследования? Объекты в JavaScript имеют только один прототип.
Правда в том, что наследование прототипов поддерживает наследование от нескольких прототипов. Прототип наследования просто означает, что один объект наследуется от другого объекта. На самом деле существует два способа реализации наследования прототипа :
- Делегирование или дифференциальное наследование
- Клонирование или сцепленное наследование
Да. JavaScript позволяет объектам делегировать только один объект. Однако это позволяет копировать свойства произвольного числа объектов. Например, _.extend
делает именно это.
Конечно, многие программисты не считают это истинным наследованием, потому что instanceof
и isPrototypeOf
говорят иначе. Однако это можно легко исправить, сохранив массив прототипов для каждого объекта, который наследуется от прототипа путем конкатенации:
function copyOf(object, prototype) {
var prototypes = object.prototypes;
var prototypeOf = Object.isPrototypeOf;
return prototypes.indexOf(prototype) >= 0 ||
prototypes.some(prototypeOf, prototype);
}
Следовательно, наследование прототипов так же сильно, как и классическое наследование. На самом деле это намного более мощно, чем классическое наследование, потому что в наследовании прототипов вы можете вручную выбрать, какие свойства копировать, а какие свойства опускать в разных прототипах.
В классическом наследовании невозможно (или, по крайней мере, очень сложно) выбрать, какие свойства вы хотите наследовать. Они используют виртуальные базовые классы и интерфейсы для решения алмазной проблемы .
Однако в JavaScript вы, скорее всего, никогда не услышите о проблеме с бриллиантами, поскольку вы можете точно контролировать, какие свойства вы хотите унаследовать и от каких прототипов.
3. Прототип наследования менее избыточен
Этот момент немного сложнее объяснить, потому что классическое наследование не обязательно приводит к более избыточному коду. Фактически наследование, классическое или прототипное, используется для уменьшения избыточности в коде.
Одним из аргументов может быть то, что большинство языков программирования с классическим наследованием статически типизированы и требуют от пользователя явного объявления типов (в отличие от Haskell, который имеет неявную статическую типизацию). Следовательно, это приводит к более подробному коду.
Java славится этим поведением. Я отчетливо помню, как Боб Нистром упомянул следующий анекдот в своем блоге о парсерах Pratt :
Вы должны любить уровень бюрократии Java "пожалуйста, подпишите его в четырех экземплярах".
Опять же, я думаю, что это только потому, что Java так много отстой.
Один действительный аргумент в том, что не все языки с классическим наследованием поддерживают множественное наследование. Снова на ум приходит Java. Да, в Java есть интерфейсы, но этого недостаточно. Иногда вам действительно нужно множественное наследование.
Поскольку наследование прототипа допускает множественное наследование, код, который требует множественного наследования, менее избыточен, если написан с использованием наследования прототипа, а не на языке, который имеет классическое наследование, но не множественное наследование.
4. Прототип наследования является динамическим
Одним из наиболее важных преимуществ наследования прототипов является то, что вы можете добавлять новые свойства в прототипы после их создания. Это позволяет вам добавлять новые методы в прототип, которые будут автоматически доступны всем объектам, которые делегируют этот прототип.
Это невозможно в классическом наследовании, поскольку после создания класса его нельзя изменить во время выполнения. Это, вероятно, самое большое преимущество наследования прототипа перед классическим наследованием, и оно должно было быть на вершине. Однако я люблю сохранять лучшее для конца.
Заключение
Имеет значение наследование прототипа. Важно обучить программистов на JavaScript тому, почему следует отказаться от шаблона конструктора наследования прототипа в пользу модели прототипа наследования прототипа.
Нам нужно начать учить JavaScript правильно, а это значит показать новым программистам, как писать код, используя шаблон-прототип вместо шаблона-конструктора.
Мало того, что будет проще объяснить наследование прототипа, используя шаблон прототипа, но это также улучшит программистов.
Если вам понравился этот ответ, то вам также следует прочитать мой пост в блоге " Why Prototypal Inheritance Matters " Поверьте мне, вы не будете разочарованы.