Я знаю, что этот ответ опоздал на 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
s в 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, который имеет неявную статическую типизацию). Следовательно, это приводит к более подробному коду.
Ява печально известна этим поведением. Я отчетливо помню, как Боб Нистром упомянул следующий анекдот в своем блоге о парсерах Pratt :
Вы должны любить уровень бюрократии Java «пожалуйста, подпишите его в четырех экземплярах».
Опять же, я думаю, что это только потому, что Java так много отстой.
Одним из веских аргументов является то, что не все языки с классическим наследованием поддерживают множественное наследование. Снова на ум приходит Java. Да, в Java есть интерфейсы, но этого недостаточно. Иногда вам действительно нужно множественное наследование.
Поскольку наследование прототипа допускает множественное наследование, код, который требует множественного наследования, менее избыточен, если написан с использованием наследования прототипа, а не на языке, который имеет классическое наследование, но не множественное наследование.
4. Прототип наследования является динамическим
Одним из наиболее важных преимуществ наследования прототипов является то, что вы можете добавлять новые свойства в прототипы после их создания. Это позволяет вам добавлять новые методы в прототип, которые будут автоматически доступны для всех объектов, которые делегируют этот прототип.
Это невозможно в классическом наследовании, потому что после создания класса вы не сможете изменить его во время выполнения. Это, вероятно, самое большое преимущество наследования прототипа перед классическим наследованием, и оно должно было быть на вершине. Однако мне нравится сохранять лучшее для конца.
Вывод
Прототип наследования имеет значение. Важно обучить программистов на JavaScript тому, почему следует отказаться от шаблона конструктора наследования прототипа в пользу модели прототипа наследования прототипа.
Нам нужно начать учить JavaScript правильно, а это значит показать новым программистам, как писать код, используя шаблон-прототип вместо шаблона-конструктора.
Мало того, что будет проще объяснить наследование прототипа, используя шаблон прототипа, но это также сделает лучших программистов.
Если вам понравился этот ответ, то вам также следует прочитать мой пост в блоге " Почему вопросы наследования прототипов ". Поверьте мне, вы не будете разочарованы.