Преимущества прототипного наследования над классическим?

Итак, я окончательно перестал тащить ноги все эти годы и решил научиться JavaScript "правильно". Одним из наиболее важных элементов дизайна языков является реализация наследования. Имея опыт работы в Ruby, я был очень рад видеть закрытия и динамическую типизацию; но для жизни меня не может понять, какие преимущества должны быть получены из экземпляров объекта, используя другие экземпляры для наследования.

Ответы

Ответ 1

Я знаю, что этот ответ опоздал на 3 года, но я действительно думаю, что текущие ответы не дают достаточно информации о том, насколько наследование прототипа лучше, чем классическое наследование.

Сначала давайте рассмотрим наиболее распространенные аргументы, которые программисты JavaScript заявляют в защиту наследования прототипа (я беру эти аргументы из текущего пула ответов):

  1. Это просто.
  2. Это мощный.
  3. Это приводит к меньшему, менее избыточному коду.
  4. Это динамическое и, следовательно, лучше для динамических языков.

Теперь все эти аргументы верны, но никто не удосужился объяснить, почему. Это как сказать ребенку, что изучение математики важно. Конечно, но ребенку все равно; и вы не можете сделать ребенка, как математика, сказав, что это важно.

Я думаю, что проблема с наследованием прототипов заключается в том, что это объясняется с точки зрения JavaScript. Я люблю JavaScript, но наследование прототипов в JavaScript неверно. В отличие от классического наследования, существует два типа наследования прототипа:

  1. Модель прототипа наследования прототипа.
  2. Шаблон конструктора прототипного наследования.

К сожалению, JavaScript использует шаблон конструктора прототипного наследования. Это потому, что когда был создан JavaScript, Брендан Эйх (создатель JS) хотел, чтобы он выглядел как Java (который имеет классическое наследование):

И мы толкали его как младшего брата к Java, так как дополнительный язык, такой как Visual Basic, был C++ в семействах языков Microsoft в то время.

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

Люди из таких языков, как Java, которые имеют классическое наследование, становятся еще более запутанными, потому что, хотя конструкторы выглядят как классы, они не ведут себя как классы. Как сказал Дуглас Крокфорд:

Эта косвенность была предназначена для того, чтобы сделать язык более знакомым для классически обученных программистов, но не смог этого сделать, как мы можем видеть из очень низкого мнения о том, что Java-программисты имеют JavaScript. Шаблон конструктора JavaScripts не понравился классической толпе. Это также заслоняет истинную прототипную природу JavaScripts. В результате, очень мало программистов, которые знают, как эффективно использовать язык.

Там у вас есть это. Прямо изо рта лошади.

Истинное наследование прототипов

Прототипное наследование - это все об объектах. Объекты наследуют свойства от других объектов. Это все, что нужно сделать. Существует два способа создания объектов с использованием наследования прототипов:

  1. Создайте новый объект.
  2. Клонировать существующий объект и расширить его.

Примечание: 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.

Следовательно, нам нужны только две вещи, чтобы заставить наследование прототипа работать:

  1. Способ создания нового объекта (например, литералы объекта).
  2. Способ расширить существующий объект (например, Object.create).

В отличие от классического наследования гораздо сложнее. В классическом наследовании у вас есть:

  1. Классы.
  2. Объект.
  3. Интерфейсы.
  4. Абстрактные Занятия.
  5. Финальные Занятия.
  6. Виртуальные базовые классы.
  7. Конструкторы.
  8. Деструкторов.

Вы поняли идею. Дело в том, что наследование прототипов легче понять, легче реализовать и легче рассуждать.

Как пишет Стив Йегге в своем классическом посте " Портрет N00b ":

Метаданные - это любое описание или модель чего-то другого. Комментарии в вашем коде - это просто описание вычислений на естественном языке. Что делает метаданные метаданными, так это то, что они не являются строго необходимыми. Если у меня есть собака с какими-то племенными документами, и я теряю документы, у меня все еще есть вполне подходящая собака.

В этом же смысле классы - это просто метаданные. Классы не являются строго обязательными для наследования. Однако некоторые люди (обычно n00bs) находят классы более удобными для работы. Это дает им ложное чувство безопасности.

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

Как я уже говорил ранее, занятия дают людям ложное чувство безопасности. Например, вы получаете слишком много NullPointerException в Java, даже если ваш код отлично читается. Я считаю, что классическое наследование обычно мешает программированию, но, возможно, это просто Java. У Python удивительная классическая система наследования.

2. Прототип наследования является мощным

Большинство программистов, которые имеют классический опыт, утверждают, что классическое наследование является более мощным, чем наследование прототипов, потому что оно имеет:

  1. Частные переменные.
  2. Множественное наследование.

Это утверждение является ложным. Мы уже знаем, что JavaScript поддерживает закрытые переменные через замыкания, но как насчет множественного наследования? Объекты в JavaScript имеют только один прототип.

Правда в том, что наследование прототипов поддерживает наследование от нескольких прототипов. Прототип наследования просто означает, что один объект наследуется от другого объекта. Существует два способа реализации наследования прототипов:

  1. Делегирование или дифференциальное наследование
  2. Клонирование или конкатенационное наследование

Да, 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 правильно, а это значит показать начинающим программистам, как писать код, используя шаблон-прототип вместо шаблона-конструктора.

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

Если вам понравился этот ответ, то вам также следует прочитать мой пост в блоге " Почему вопросы наследования прототипов ". Поверьте мне, вы не будете разочарованы.

Ответ 2

Позвольте мне ответить на вопрос inline.

Наследование прототипа имеет следующие достоинства:

  • Он лучше подходит для динамических языков, потому что наследование так же динамично, как и среда, в которой он находится. (Применимость к JavaScript должна быть очевидной здесь.) Это позволяет вам быстро делать вещи на лету, например, настраивать классы без огромных сумм кода инфраструктуры.
  • Проще реализовать схему объектов прототипирования, чем классические схемы дихотомии класса/объекта.
  • Это устраняет необходимость в сложных острых краях вокруг объектной модели, например, "метаклассы" (мне никогда не нравился метакласс, который мне нравился... извините!) или "собственные значения" или тому подобное.

Однако он имеет следующие недостатки:

  • Тип проверки языка прототипа не является невозможным, но это очень, очень сложно. Большинство "проверки типов" прототипических языков - это чистые проверки времени "утиная печать" во время исполнения. Это не подходит для всех сред.
  • Аналогично сложно делать такие вещи, как оптимизация отправки метода статическим (или, зачастую, даже динамическим!) анализом. Это может (я подчеркиваю: может) очень неэффективно очень легко.
  • Точно так же создание объекта может (и обычно) намного медленнее на языке прототипирования, чем может быть в более обычной схеме дихотомии класса/объекта.

Я думаю, вы можете читать между строками выше и придумать соответствующие преимущества и недостатки традиционных схем классов/объектов. Есть, конечно, больше в каждой области, поэтому я оставлю остальных до других людей, отвечающих.

Ответ 3

ИМО - главное преимущество прототипного наследования - его простота.

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

Вам не нужно делать классификацию, ваш код меньше, менее избыточен, объекты наследуются от других, более общих объектов.

Если вы думаете прототипом, вы скоро заметите, что вам не нужны классы...

Прототипное наследование будет гораздо более популярным в ближайшем будущем, спецификация ECMAScript 5th Edition внесла Object.create, который позволяет создать новый экземпляр объекта, который наследует от другого по-настоящему простой способ:

var obj = Object.create(baseInstance);

Эта новая версия стандарта реализуется всеми поставщиками браузеров, и я думаю, что мы начнем видеть более чистое прототипное наследование...

Ответ 4

Существует не так много выбора между двумя методами. Основная идея заключается в том, что когда движку JavaScript предоставляется свойство объекта для чтения, он сначала проверяет экземпляр, и если это свойство отсутствует, он проверяет цепочку прототипов. Вот пример, который показывает разницу между прототипом и классикой:

прототипичный

var single = { status: "Single" },
    princeWilliam = Object.create(single),
    cliffRichard = Object.create(single);

console.log(Object.keys(princeWilliam).length); // 0
console.log(Object.keys(cliffRichard).length); // 0

// Marriage event occurs
princeWilliam.status = "Married";

console.log(Object.keys(princeWilliam).length); // 1 (New instance property)
console.log(Object.keys(cliffRichard).length); // 0 (Still refers to prototype)

Классический метод экземпляра (неэффективен, потому что каждый экземпляр сохраняет его собственное свойство)

function Single() {
    this.status = "Single";
}

var princeWilliam = new Single(),
    cliffRichard = new Single();

console.log(Object.keys(princeWilliam).length); // 1
console.log(Object.keys(cliffRichard).length); // 1

Эффективный классический

function Single() {
}

Single.prototype.status = "Single";

var princeWilliam = new Single(),
    cliffRichard = new Single();

princeWilliam.status = "Married";

console.log(Object.keys(princeWilliam).length); // 1
console.log(Object.keys(cliffRichard).length); // 0
console.log(cliffRichard.status); // "Single"

Как вы можете видеть, поскольку можно манипулировать прототипом "классов", объявленных в классическом стиле, нет никакой пользы для использования прототипного наследования. Это подмножество классического метода.

Ответ 6

В Javascript, с прототипом модального, вы не можете делать instanceOf. С классической моделью вы можете. Я собираюсь поставить ссылку для вас. www.objectplayground.com Лучшее видео, которое говорит о прототипе Javascript.

При поступлении ECMA 6 Javascript будет поддерживать новый синтаксис класса. Итак, идет ИСТИННАЯ классическая модель.

Будущее придерживается классической модели.