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


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) вважають заняття більш зручними для роботи. Це дає їм помилкове відчуття безпеки.

Ну, ми також знаємо, що статичні типи - це лише метадані. Вони - це спеціалізований вид коментарів, орієнтований на два типи читачів: програмісти та компілятори. Статичні типи розповідають про обчислення, імовірно, щоб допомогти обом читацьким групам зрозуміти наміри програми. Але статичні типи можуть бути викинуті під час виконання, тому що зрештою вони просто стилізовані коментарі. Вони схожі на родовідну документацію: це може зробити певний небезпечний тип особистості щасливішим щодо своєї собаки, але собака, безумовно, не хвилює.

Як я вже говорив раніше, заняття дають людям помилкове почуття безпеки. Наприклад, у вас NullPointerExceptionна Java занадто багато s, навіть коли ваш код ідеально розбірливий. Я вважаю, що класичне успадкування зазвичай стає способом програмування, але, можливо, це лише 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, який має неявну статичну типізацію). Звідси це призводить до більш багатослівного коду.

Java відома для такої поведінки. Я виразно пам’ятаю, як Боб Ністром згадував такий анекдот у своєму блозі про Pratt Parsers :

Ви повинні любити 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

Сьогодні у нас є динамічні щомісячні компілятори, які можуть компілювати код, поки код працює, створюючи різні фрагменти коду для кожного розділу. JavaScript насправді швидше, ніж Ruby або Python, які використовують класичні класи через це, навіть якщо ви використовуєте прототипи, оскільки було зроблено багато роботи над його оптимізацією.
aoeu256

28

Основна перевага наслідування прототипів IMO полягає в його простоті.

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

Не потрібно робити класифікацію , ваш код менший, менш зайвий, об'єкти успадковуються від інших, більш загальних об'єктів.

Якщо ви думаєте прототипно , то незабаром помітите, що вам не потрібні заняття ...

Прототипне успадкування буде набагато популярніше найближчим часом, специфікація ECMAScript 5th Edition запровадила Object.createметод, який дозволяє створити новий екземпляр об'єкта, який успадковує від іншого справжньо простим способом:

var obj = Object.create(baseInstance);

Ця нова версія стандарту впроваджується всіма постачальниками браузерів, і я думаю, ми почнемо бачити більш чисте прототипне успадкування ...


11
"Ваш код менший, менш зайвий ...", чому? Я переглянув посилання на вікіпедію щодо "диференціального успадкування", і немає нічого, що підтримує ці твердження. Чому класичне успадкування призведе до більшого, надмірного коду?
Ноель Абрахамс

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

1
Ви можете імітувати спадкування на основі класів, використовуючи спадкування на основі прототипу, але не навпаки. Це може бути хорошим аргументом. Також я бачу інкапсуляцію швидше як умову, ніж особливість мови (зазвичай ви можете порушити інкапсуляцію за допомогою рефлексії). Щодо поліморфізму - все, що ви отримуєте, - не потрібно писати прості умови "якщо" під час перевірки аргументів методу (і трохи швидкості, якщо цільовий метод вирішено під час компіляції). Тут немає жодних реальних недоліків JavaScript.
Павло Гораль

Прототипи приголомшливі, ІМО. Я думаю створити функціональну мову, як Haskell ... але замість створення абстракцій я буду базувати все на прототипах. Замість того, щоб узагальнювати суму, а факторію "складати", ви повинні прототип функції суми і замінити + на * і 0 на 1, щоб скласти добуток. Я поясню "Monads" як прототипи, які замінюють "тоді" на обіцянки / зворотні виклики, а flatMap є синонімом "тоді". Я думаю, що прототипи можуть допомогти наблизити функціональне програмування до маси.
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, щоб дозволити появу класичного успадкування". ОП, схоже, запитує про переваги спадкоємства прототипів у JS перед класичним успадкуванням іншими мовами, а не порівняння методів інстанції в JavaScript.
Fader Darkly

2

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

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

Класичне наслідування прототипів Vs - Переповнення стека

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


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

1-е посилання не відповідає на питання, чому успадковувати прототипи? Це просто описує.
viebel
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.