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


271

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



Ответы:


560

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  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) находят классы более удобными для работы. Это дает им ложное чувство безопасности.

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

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

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

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


33
Хотя я понимаю, откуда вы родом, и я согласен с тем, что наследование прототипов очень полезно, я думаю, что, сделав предположение «наследование прототипов лучше классического наследования», вы уже обречены на провал. Чтобы дать вам некоторое представление, моя библиотека jTypes - это классическая библиотека наследования для JavaScript. Будучи парнем, который нашел время, чтобы сделать это, я все еще буду сидеть здесь и говорить, что наследование прототипов - это круто и очень полезно. Но это всего лишь один инструмент среди многих, которые есть у программиста. Есть еще много недостатков для наследования прототипов.
Редлайн

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

9
Что ж, это ваше мнение, но я продолжу не соглашаться, и я думаю, что растущая популярность таких вещей, как CoffeeScript и TypeScript, показывает, что существует большое сообщество разработчиков, которые хотели бы использовать эту функциональность, когда это необходимо. Как вы сказали, ES6 добавляет синтаксический сахар, но все еще не предлагает обширность jTypes. Кстати, я не ответственен за ваше отрицательное мнение. Хотя я не согласен с вами, я не чувствую, что это плохой ответ. Вы были очень тщательны.
красная линия

25
Вы часто используете слово клон , что просто неправильно. Object.createсоздает новый объект с указанным прототипом. Ваш выбор слов создает впечатление, что прототип клонируется.
Павел Хорал

7
@Aadit: нет нужды быть таким оборонительным. Ваш ответ очень подробный и заслуживает голосов. Я не предполагал, что «связанный» должен быть заменой «клона», но он более уместно описывает связь между объектом и прототипом, от которого он наследует, утверждаете ли вы свое собственное определение термина «клон» " или не. Измени это или не меняй, это полностью твой выбор.
Энди Э,

42

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

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

  1. Он лучше подходит для динамических языков, потому что наследование столь же динамично, как и среда, в которой оно находится. (Применимость к JavaScript здесь должна быть очевидна.) Это позволяет вам быстро выполнять действия на лету, как настройка классов без огромного количества кода инфраструктуры. ,
  2. Проще реализовать схему объекта-прототипа, чем классические схемы дихотомии класса / объекта.
  3. Это устраняет необходимость в сложных острых гранях вокруг объектной модели, таких как «метаклассы» (мне никогда не нравился метакласс… извините!) Или «собственные значения» или тому подобное.

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

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

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


1
Эй, смотрите, краткий ответ, который не фанат. Очень хотелось бы, чтобы это был лучший ответ на вопрос.
Siliconrockstar

Сегодня у нас есть динамические компиляторы Just-in-time, которые могут компилировать код во время выполнения кода, создавая разные фрагменты кода для каждого раздела. JavaScript на самом деле быстрее, чем Ruby или Python, которые используют классические классы из-за этого, даже если вы используете прототипы, потому что проделана большая работа по его оптимизации.
aoeu256

28

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

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

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

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

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

var obj = Object.create(baseInstance);

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


11
«Ваш код меньше, менее избыточен ...», почему? Я посмотрел на ссылку в Википедии для «дифференциального наследования», и нет ничего, подтверждающего эти утверждения. Почему классическое наследование приведет к большему, более избыточному коду?
Ноэль Абрахамс

4
Точно, я согласен с Ноэлем. Наследование прототипа - это просто один из способов выполнить работу, но это не делает его правильным. Различные инструменты будут работать по-разному для разных задач. Наследование прототипа имеет свое место. Это чрезвычайно мощная и простая концепция. При этом отсутствие поддержки истинной инкапсуляции и полиморфизма ставит JavaScript в невыгодное положение. Эти методологии существуют намного дольше, чем JavaScript, и они основательны в своих принципах. Так что думать, что прототип «лучше» - это просто неправильный менталитет.
Редлайн

1
Вы можете моделировать наследование на основе классов, используя наследование на основе прототипов, но не наоборот. Это может быть хорошим аргументом. Также я считаю инкапсуляцию скорее соглашением, чем языковой функцией (обычно вы можете нарушить инкапсуляцию с помощью отражения). Что касается полиморфизма - все, что вы получаете, - это не писать простые условия «если» при проверке аргументов метода (и немного скорости, если целевой метод разрешен во время компиляции). Здесь нет реальных недостатков JavaScript.
Павел Хорал

Прототипы потрясающие, ИМО. Я думаю о создании функционального языка, такого как Haskell ... но вместо создания абстракций я буду основывать все на прототипах. Вместо обобщения суммы и факториала, чтобы «сложить», вы должны создать прототип функции суммы и заменить + на * и 0 на 1, чтобы сделать продукт. Я объясню «Monads» как прототипы, которые заменяют «then» из обещаний / обратных вызовов, а flatMap является синонимом «then». Я думаю, что прототипы могут помочь принести функциональное программирование в массы.
aoeu256

11

Там действительно не так много, чтобы выбрать между этими двумя методами. Основная идея заключается в том, что когда движку 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"

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


2
Глядя на другие ответы и ресурсы по этой теме, ваш ответ может показаться следующим: «Наследование прототипа - это подмножество синтаксического сахара, добавленного в JavaScript, чтобы позволить появление классического наследования». Похоже, что OP спрашивает о преимуществах наследования прототипов в JS по сравнению с классическим наследованием в других языках, а не о сравнении методов инстанцирования в JavaScript.
Мрачный фейдер

2

Веб-разработка: прототип наследования против классического наследования

http://chamnapchhorn.blogspot.com/2009/05/prototypal-inheritance-vs-classical.html

Классическое наследование против прототипа - переполнение стека

Классический Vs прототип наследования


20
Я думаю, что лучше суммировать содержимое ссылок, чем вставлять ссылку (что я сам делал раньше), если только это не другая SO-ссылка. Это связано с тем, что ссылки / сайты отключаются, и вы теряете ответ на вопрос, что потенциально влияет на результаты поиска.
Джеймс Вестгейт

1-я ссылка не отвечает на вопрос, почему наследование прототипа? Это просто описывает это.
viebel
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.