Ответ 1
Я знаю, что этот ответ опоздал на 3 года, но я действительно думаю, что текущие ответы не дают достаточно информации о том, насколько наследование прототипа лучше, чем классическое наследование.
Сначала давайте рассмотрим наиболее распространенные аргументы, которые программисты JavaScript заявляют в защиту наследования прототипа (я беру эти аргументы из текущего пула ответов):
- Это просто.
- Это мощный.
- Это приводит к меньшему, менее избыточному коду.
- Это динамическое и, следовательно, лучше для динамических языков.
Теперь все эти аргументы верны, но никто не удосужился объяснить, почему. Это как сказать ребенку, что изучение математики важно. Конечно, но ребенку все равно; и вы не можете сделать ребенка, как математика, сказав, что это важно.
Я думаю, что проблема с наследованием прототипов заключается в том, что это объясняется с точки зрения JavaScript. Я люблю JavaScript, но наследование прототипов в JavaScript неверно. В отличие от классического наследования, существует два типа наследования прототипа:
- Модель прототипа наследования прототипа.
- Шаблон конструктора прототипного наследования.
К сожалению, JavaScript использует шаблон конструктора прототипного наследования. Это потому, что когда был создан JavaScript, Брендан Эйх (создатель JS) хотел, чтобы он выглядел как Java (который имеет классическое наследование):
И мы толкали его как младшего брата к Java, так как дополнительный язык, такой как Visual Basic, был C++ в семействах языков Microsoft в то время.
Это плохо, потому что когда люди используют конструкторы в JavaScript, они думают о конструкторах, наследуемых от других конструкторов. Это не верно. В прототипе объекты наследования наследуются от других объектов. Конструкторы никогда не входят в картину. Это то, что смущает большинство людей.
Люди из таких языков, как Java, которые имеют классическое наследование, становятся еще более запутанными, потому что, хотя конструкторы выглядят как классы, они не ведут себя как классы. Как сказал Дуглас Крокфорд:
Эта косвенность была предназначена для того, чтобы сделать язык более знакомым для классически обученных программистов, но не смог этого сделать, как мы можем видеть из очень низкого мнения о том, что Java-программисты имеют JavaScript. Шаблон конструктора JavaScripts не понравился классической толпе. Это также заслоняет истинную прототипную природу JavaScripts. В результате, очень мало программистов, которые знают, как эффективно использовать язык.
Там у вас есть это. Прямо изо рта лошади.
Истинное наследование прототипов
Прототипное наследование - это все об объектах. Объекты наследуют свойства от других объектов. Это все, что нужно сделать. Существует два способа создания объектов с использованием наследования прототипов:
- Создайте новый объект.
- Клонировать существующий объект и расширить его.
Примечание: 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, который имеет неявную статическую типизацию). Следовательно, это приводит к более подробному коду.
Ява печально известна этим поведением. Я отчетливо помню, как Боб Нистром упомянул следующий анекдот в своем блоге о парсерах Pratt:
Вы должны любить Java "пожалуйста, подпишите его в четырехкратном" уровне бюрократии здесь.
Опять же, я думаю, что только потому, что Java сосет так много.
Одним из веских аргументов является то, что не все языки с классическим наследованием поддерживают множественное наследование. Снова на ум приходит Java. Да, у Java есть интерфейсы, но этого недостаточно. Иногда вам действительно нужно множественное наследование.
Поскольку наследование прототипа допускает множественное наследование, код, который требует множественного наследования, менее избыточен, если написан с использованием наследования прототипа, а не на языке, который имеет классическое наследование, но не множественное наследование.
4. Прототип наследования является динамическим
Одним из наиболее важных преимуществ наследования прототипов является то, что вы можете добавлять новые свойства в прототипы после их создания. Это позволяет вам добавлять новые методы в прототип, которые будут автоматически доступны всем объектам, которые делегируют этот прототип.
Это невозможно в классическом наследовании, потому что после создания класса вы не сможете изменить его во время выполнения. Это, вероятно, самое большое преимущество наследования прототипа перед классическим наследованием, и оно должно было быть на вершине. Однако мне нравится сохранять лучшее для конца.
Заключение
Прототип наследования имеет значение. Важно обучить программистов на JavaScript тому, почему следует отказаться от конструкторского шаблона наследования прототипа в пользу прототипного шаблона наследования прототипа.
Нам нужно начать учить JavaScript правильно, а это значит показать начинающим программистам, как писать код, используя шаблон-прототип вместо шаблона-конструктора.
Мало того, что будет легче объяснить наследование прототипа, используя шаблон прототипа, но это также сделает лучших программистов.
Если вам понравился этот ответ, то вам также следует прочитать мой пост в блоге " Почему вопросы наследования прототипов ". Поверьте мне, вы не будете разочарованы.