Я знаю, що ця відповідь запізнюється на 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
на Java занадто багато s, навіть коли ваш код ідеально розбірливий. Я вважаю, що класичне успадкування зазвичай стає способом програмування, але, можливо, це лише 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, який має неявну статичну типізацію). Звідси це призводить до більш багатослівного коду.
Java відома для такої поведінки. Я виразно пам’ятаю, як Боб Ністром згадував такий анекдот у своєму блозі про Pratt Parsers :
Ви повинні любити Java, "будь ласка, підпишіть це в чотиризначні" рівень бюрократії тут.
Знову ж таки, я думаю, що це лише тому, що Java так сильно смокче.
Одним з вагомих аргументів є те, що не всі мови, які мають класичне успадкування, підтримують багатократне успадкування. Знову на думку приходить Java. Так, у Java є інтерфейси, але цього недостатньо. Іноді вам справді потрібно багаторазове успадкування.
Оскільки прототипічне успадкування допускає багаторазове успадкування, код, який вимагає багаторазового успадкування, є менш зайвим, якщо він написаний з використанням прототипового успадкування, а не мовою, яка має класичне успадкування, але не має багаторазового успадкування.
4. Прототипне спадкування є динамічним
Однією з найважливіших переваг успадкування прототипів є те, що ви можете додати нові властивості до прототипів після їх створення. Це дозволяє додати нові методи до прототипу, який буде автоматично доступний для всіх об'єктів, делегованих до цього прототипу.
У класичному успадкуванні це неможливо, оскільки після створення класу ви не можете його змінювати під час виконання. Це, мабуть, єдина найбільша перевага наслідування прототипів перед класичним успадкуванням, і воно мало бути на вершині. Однак мені подобається зберігати найкраще для кінця.
Висновок
Прототипне успадкування має значення. Важливо навчити програмістів JavaScript, чому слід відмовитися від конструкторського шаблону успадкування прототипів на користь прототипного шаблону успадкування прототипів.
Нам потрібно почати викладати JavaScript правильно, а це означає показувати новим програмістам, як писати код, використовуючи прототипний шаблон замість шаблону конструктора.
Не тільки буде простіше пояснити спадкування прототипів, використовуючи прототипний зразок, але це також зробить кращими програмістів.
Якщо вам сподобалась ця відповідь, тоді ви також повинні прочитати мій пост у блозі на тему " Чому питання про наслідування прототипів ". Повірте, ви не розчаруєтесь.