JavaScript - це об'єктна мова, що заснована на прототипах, а не на класах. У зв'язку з цим може бути менш очевидно, як саме JavaScript дозволяє створювати ієрархії об'єктів із наслідуванням їх властивостей та значень. Цей розділ є спробою дещо прояснити цей механізм.
Вміст цього розділу розрахований на те, що ви вже, принаймні, дещо знайомі з мовою JavaScript, і застосовували його функції для створення простих об'єктів.
Мови з класовою та прототипною моделлю
Об'єктно-орієнтовані мови з класовою моделлю, такі як Java і C++, засновані на концепції двох окремих сутностей: класів та екземплярів.
- Клас визначає всі властивості (включаючи поля та методи у Java, чи фукнції-члени у C++, як властивості), що характеризують певний набір об'єктів. Класс - це абстрактна річ, на відміну від будь-якого окремого об'єкту із набору, який він описує. Наприклад, клас
Employee
може представляти групу всіх робітників. - Екземпляр, з іншого боку, є реалізацією класу. Наприклад,
Victoria
може бути екземпляром класуEmployee
, представляючи окрему особу в якості працівника. Екземпляр має точно ті самі властивості, що і батьківський клас (не більше, не менше).
Мови з прототипною моделлю наслідування, такі як JavaScript, не розділяють ці сутності: у них просто є об'єкти. Такі мови реалізовують поняття об'єкту-прототипу — об'єкту, що використовується як зразок, з якого вибираються початкові властивості для нового об'єкту. Будь-який об'єкт може вказати власні властивості, як в момент створення, так і під час виконання. Ну і на додачу, будь-який об'єкт можна задати в якості прототипу з іншого об'єкту — таким чином перший об'єкт розділить свої властивості з другим.
Задання і визначення класу
У мовах із класовою моделлю, класс задається у окремому визначенні класу. У цому визначенні можна вказати особливі методи, що називаються конструкторами, щоб створити екземпляри класу. Метод-конструктор може задати початкові значення властивостей екземпляру, і виконати якісь інші задачі прямо у момент створення. Для створення екземплярів застосовується оператор new
у комбінації із методом-конструктором.
JavaScript слідує подібній моделі, проте не має відокремленого від конструктора визначення класу. Натомість, ви одразу задаєте функцію-конструктор, щоб створити об'єкти із відповідним початковим набором властивостей та значень. Будь-яка JavaScript-функція може використовуватись як конструктор. Для створення нового об'єкту так само використовується оператор new
із фукнцією-конструктором.
Зауважте, що ECMAScript 2015 вводить визначення класу:
Класи JavaScript, введені стандартом ECMAScript 2015, є лише синтаксичним цукром поверх уже наявного у JavaScript прототипного наслідування. Тобто ці класи не вводять у JavaScript нової моделі наслідуваня.
Дочірні класи і наслідування
У мові з класовою моделлю наслідкування ієрархія класів створюється через визначення класу. У цьому визначенні можна окремо вказати, що новий клас являється дочірнім стосовно уже наявного класу. Дочірній клас отримає всі властивості батьківського і може привнести нові (або ж змінити успадковані). Наприклад, припустимо, що клас Employee
включає в себе лише поля name
та dept
, і Manager
- це дочірній клас Employee
, що додає властивість reports
. У цьому випадку, екземпляр класу Manager
матиме три властивості: name
, dept
, та reports
.
JavaScript реалізовує наслідування дещо інакше. Він дозволяє пов'язувати об'єкт-прототип із будь-якою фукнцією-конструктором. Тобто ви можете точнісінько реалізувати приклад Employee
— Manager
, проте використовуючи дещо інші терміни. Спершу ви визначаєте конструктор Employee
, задаючи властивості name
та dept
. Далі ви визначаєте фукнцію-конструктор Manager
, що викликає конструктор Employee
та задає властивість reports
. Насамкінець, призначаєте новий об'єкт, отриманий з Employee.prototype
в якості прототипу конструктора Manager
. Надалі, при створенні екземпляра Manager
він наслідує властивості name
і dept
з об'єкту Employee
.
Додавання і видалення властивостей
Зазвичай у мовах із класовою моделлю наслідування класи створюються під час компіляції, а екземпляри класів - під час компіляції чи виконання програми. Після того, як його було визначено, не можна змінити кількість або тип його властивостей. Однак, у JavaScript можна додавати чи видаляти властивості будь-якого об'єкту безпосередньо під час виконання програми. Якщо додати нову властивість до об'єкту-прототипу певного набору об'єктів, вони всі також отримають цю властивість.
Підсумок відмінностей класової і прототипної моделей
Наступна таблиця надає короткий підсумок цих відмінностей. Решта розділу розкриває деталі застосування JavaScript-конструкторів і прототипів для створення ієрархії об'єктів, та порівнює це із тим, як би це робилось у Java.
Класова модель (Java) | Прототипна модель (JavaScript) |
---|---|
Клас та його екземпляр - окремі сутності. | Всі об'єкти можуть наслідувати інші об'єкти. |
Визначення класу описує його; екземпляри створюються методами-конструкторами. | Функції-конструктори і описують, і створюють набори об'єктів. |
Окремий об'єкт створюється оператором new . |
Так само. |
Ієрархія об'єктів формується при визначенні класів, шляхом задання нащадків для уже наявних класів. | Ієрархія об'єктів формується шляхом призначення об'єкту прототипом функції-конструктора. |
Властивості наслідуються згідно ланцюжка класів. | Властивості наслідуються згідно ланцюжка прототипів. |
Визначення класу задає всі властивості всіх екземплярів класу. Неможливо динамічно додавати властивості під час виконання програми. | Функція-конструктор чи прототип задають лише початковий набір властивостей. Можна додавати чи видаляти властивості як окремого об'єкту, так певного їх набору. |
Приклад із робітником "Employee"
Надалі у розділі ієрархія робітників, що показана на наступному зображенні.
Проста ієрархія об'єктів, сформована із наступних елементів:
Employee
має поляname
(із порожнім рядком в якості значення за замовчуванням) таdept
(у якого значення за замовчуванням — "general").Manager
заснований наEmployee
. Він додає властивістьreports
(за замовчуванням містить порожній масив для об'єктівEmployee
).WorkerBee
також заснований наEmployee
. Він додає властивістьprojects
(за замовчуванням містить порожній масив, призначений для рядків).SalesPerson
заснований наWorkerBee
. Він додає властивістьquota
(за замовчуванням — число 100). Він також перевизначае властивістьdept
, надаючи їй нове значенням "sales" (що означає, що всіSalesPerson
відносяться до одного відділу).Engineer
заснований наWorkerBee
. Він додає властивістьmachine
(значення за замовчуванням — порожній рядок) і перевизначає властивістьdept
, задаючи їй значення "engineering".
Створення ієрархії
Існує декілька способів задати відповідні функції-конструктори, щоб реалізувати ієрархію робітників. Який спосіб обрати — значною мірою залежить від того, які можливості ви хочете отримати від вашого додатку.
Цей розділ показує, як використовувати дуже прості (і відносно негнучкі) визначення, і таким чином демонструє, як отримати робочий механізм наслідування. У цих визначеннях не можна задати жодного значення при створенні об'єкту — він отримає властивості із значеннями за замовчуванням, які можна буде змінити пізніше.
У реальному додатку ви б, ймовірно, визначали конструктор, що дозволяє задавати значення в момент створення об'єкту (докладніше у Більш гнучкі конструктори). А наразі ці прості визначення покажуть, як загалом відбувається наслідування.
Наступні визначення Employee
у Java та JavaScript ідентичні. Єдина відмінність — у Java необхідно явно вказувати тип кожної властивості, на відміну від JavaScript (так як Java — мова із сильною типізацією, а JavaScript — із слабкою).
JavaScript
function Employee() {
this.name = '';
this.dept = 'general';
}
Java
public class Employee {
public String name = "";
public String dept = "general";
}
Визначення Manager
і WorkerBee
показують різницю у заданні батьківського об'єкту. У JavaScript ви додаєте екземпляр прототипу в якості значення поля prototype
функції-конструктора, а потім перевизначаєте prototype.constructor
, щоб це поле вказувало на функцію-конструктор. Ви можете це зробити в будь-якому місці після визначення конструктора. У Java надклас задається всередині визначення класу, і його ніяк не можна змінити зовні визначення класу.
JavaScript
function Manager() {
Employee.call(this);
this.reports = [];
}
Manager.prototype =
Object.create(Employee.prototype);
Manager.prototype.constructor = Manager;
function WorkerBee() {
Employee.call(this);
this.projects = [];
}
WorkerBee.prototype =
Object.create(Employee.prototype);
WorkerBee.prototype.constructor = WorkerBee;
Java
public class Manager extends Employee {
public Employee[] reports =
new Employee[0];
}
public class WorkerBee extends Employee {
public String[] projects = new String[0];
}
Визначення Engineer
та SalesPerson
створюють об'єкти, що наслідуються вже від WorkerBee
, а, отже, і від Employee
. Об'єкти цих типів мають властивості всіх об'єктів вище у ланцюжку наслідування. Надодачу, ці визначення перевизначають успадковані значення поля dept
, змінюючи їх відповідно до нового типу.
JavaScript
function SalesPerson() {
WorkerBee.call(this);
this.dept = 'sales';
this.quota = 100;
}
SalesPerson.prototype =
Object.create(WorkerBee.prototype);
SalesPerson.prototype.constructor = SalesPerson;
function Engineer() {
WorkerBee.call(this);
this.dept = 'engineering';
this.machine = '';
}
Engineer.prototype =
Object.create(WorkerBee.prototype);
Engineer.prototype.constructor = Engineer;
Java
public class SalesPerson extends WorkerBee {
public String dept = "sales";
public double quota = 100.0;
}
public class Engineer extends WorkerBee {
public String dept = "engineering";
public String machine = "";
}
Таким чином, ви можете створювати екземпляри об'єктів із уже заданими значеннями для своїх властивостей. Наступна схема ілюструє застосування цих JavaScript-визначень для створення нових об'єктів, і демонструє значення їх властивостей.
Зауважте, що: термін екземпляр має специфічний технічний зміст у мовах із класовою моделлю. У цих мовах екземпляр являється окремою реалізацією класу і корінним чином відрізняється від його визначення. У JavaScript, "екземпляр" не має такого особливого змісту, бо сам JavaScript не має такої значної відмінності класів від їх реалізацій. Однак, у контексті JavaScript, "екземпляр" може неформально позначати об'єкт, створений певною функцією-конструктором. Тому, згідно наступного прикладу, можна неформально стверджувати, що
є екземпляром класу jane
. Так само, хоча терміни предок, нащадок, дочірній і батьківський класи не мають формального смісту в JavaScript, їх можна застосовувати для позначення об'єктів, що знаходяться вище чи нижче у ланцюжку прототипів.Engineer
Створення об'єктів за допомогою простих визначень
Ієрархія об'єктів
Наступна ієрархія створена за допомогою коду у правій частині.
Окремі об'єкти = Jim, Sally, Mark, Fred, Jane, etc.
"Екземпляри", створені конструктором
var jim = new Employee;
// Дужки можна опустити, якщо
// конструктор не приймає аргументів.
// jim.name має значення ''
// jim.dept має значення 'general'
var sally = new Manager;
// sally.name має значення ''
// sally.dept має значення 'general'
// sally.reports має значення []
var mark = new WorkerBee;
// mark.name має значення ''
// mark.dept має значення 'general'
// mark.projects має значення []
var fred = new SalesPerson;
// fred.name має значення ''
// fred.dept має значення 'sales'
// fred.projects має значення []
// fred.quota має значення 100
var jane = new Engineer;
// jane.name має значення ''
// jane.dept має значення 'engineering'
// jane.projects має значення []
// jane.machine має значення ''
Властивості (поля) об'єкту
Ця секція описує, як об'єкти наслідують властивості інших об'єктів у ланцюжку прототипів, і що відбувається, якщо додати властивість під час виконання програми.
Наслідування властивостей
Припустимо, такою інструкцією ви створили екземпляр WorkerBee
— об'єкт mark
:
var mark = new WorkerBee;
Коли JavaScript бачить оператор new
, він створює загальний об'єкт і неявно встановлює WorkerBee.prototype
значенням внутрішньої властивості [[Prototype]], і передає цей новий об'єкт як значення this
до фукнції-конструктора WorkerBee
. Внутрішня властивість [[Prototype]] визначає ланцюжок прототипів для виводу значень полів. Коли ці властивості задані, JavaScript повертає новий об'єкт, а інструкція присвоєння задає його в якості значення змінної mark
.
Описаний процес явно не встановлює значення об'єкту mark
для властивостей (локальні значення), які mark
наслідує з ланцюжка прототипів. Коли ви запитуєте значення властивості, JavaScript в першу чергу перевіряє наявність цього значення у об'єкті, і повертає його, якщо знаходить. Якщо ж ні, JavaScript перевіряє весь ланцюжок прототипів (за допомогою властивості [[Prototype]]). Якщо об'єкт у ланцюжку має таку властивість - буде повернуто її значення (або повідомлення, що об'єкт не має такої властивості, якщо її все-таки не було знайдено). Таким чином, об'єкт mark
має наступні властивості і значення:
mark.name = '';
mark.dept = 'general';
mark.projects = [];
З конструктора Employee об'єкту mark
призначено локальні значення для властивостей name
та dept
, а з конструктора WorkerBee
— значення властивості projects
. Таким чином ми отримуємо наслідування властивостей і їх значень у JavaScript. Деякі тонкощі цього процесу додатково висвітлені у Іще раз про наслідування властивостей.
Так як ці конструктори не дозволяють задати специфічні для екземпляру значення, це являється загальним прикладом. В якості значень властивостей взяті значення за замовчуванням, що розповсюджуються на всі об'єкти, створені на основі WorkerBee
. Звісно, значення цих властивостей можна змінити. Наприклад, так можна встановити їх значення для об'єкту mark
:
mark.name = 'Doe, Mark';
mark.dept = 'admin';
mark.projects = ['navigator'];
Додавання властивостей
У JavaScript, можна додавати властивості до об'єкту безпосередно під час виконання. Ви не обмежені застосуванням лише властивостей, наданих конструктором. Щоб додати нову властивість до окремого об'єкту, просто призначте йому нове значення, як наведено далі:
mark.bonus = 3000;
Тепер об'єкт mark
містить властивість bonus
, проте більше ніхто із WorkerBee
її не має.
Якщо додати нову властивість до об'єкту-прототипу іншого конструктора, то ця властивість з'явиться у всіх об'єктів, що наслідують властивості від прототипу. Наприклад, ось так можна додати властивість specialty
всім робітникам:
Employee.prototype.specialty = 'none';
Як тільки JavaScript виконає інструкцію, об'єкт mark
матиме властивість specialty
із значенням "none"
. Наступна схема ілюструє ефект від додавання цієї властивості до прототипу Employee
і її перевизначення у прототипі Engineer
.
Додавання властивостей
Більш гнучкі конструктори
Наведені раніше функції-конструктори не дозволяють задавати значення під час створенні екземпляру. Як і у Java, можна надати конструктору аргументи для ініціалізації значень властивостей у об'єктів. Наступна схема показує один із способів це зробити.
Задання властивостей у конструкторі, варіант 1
Таблиця далі показує визначення цих об'єктів у Java і JavaScript.
JavaScript
Java
function Employee(name, dept) {
this.name = name || '';
this.dept = dept || 'general';
}
public class Employee {
public String name;
public String dept;
public Employee () {
this("", "general");
}
public Employee (String name) {
this(name, "general");
}
public Employee (String name, String dept) {
this.name = name;
this.dept = dept;
}
}
function WorkerBee(projs) {
this.projects = projs || [];
}
WorkerBee.prototype = new Employee;
public class WorkerBee extends Employee {
public String[] projects;
public WorkerBee () {
this(new String[0]);
}
public WorkerBee (String[] projs) {
projects = projs;
}
}
function Engineer(mach) {
this.dept = 'engineering';
this.machine = mach || '';
}
Engineer.prototype = new WorkerBee;
public class Engineer extends WorkerBee {
public String machine;
public Engineer () {
dept = "engineering";
machine = "";
}
public Engineer (String mach) {
dept = "engineering";
machine = mach;
}
}
Ці JavaScript-визначення застосовують особливу ідіому для задання значення за замовчуванням:
this.name = name || '';
Логічний оператор АБО у JavaScript (||
) обчислює перше значення. Якщо результат можна привести до true
, оператор повертає його, а інакше - значення другого аргументу. Таким чином, ця стрічка коду перевіряє, чи name
має якесь корисне значення для властивості name
. Якщо так — this.name
отримує її значення, а інакше значенням this.name
стає порожній рядок. У розділі ця ідіома застосовується для стислості; однак вона може бути неочевидною на перший погляд.
Зауважте, що: це може працювати не так, як очікується, якщо конструктор викликається із аргументами, що приводяться до
(число false
0
і порожній рядок (
). У цьому випадку буде обрано значення за замовчуванням.""
Таким чином можливо задати значення для властивостей на місці, безпосередньо під час створення екземпляру об'єкту. Наступною інструкцією ви можете створити новий екземпляр Engineer
:
var jane = new Engineer('belau');
Властивості Jane
тепер виглядають так:
jane.name == '';
jane.dept == 'engineering';
jane.projects == [];
jane.machine == 'belau';
Зауважте, що таким визначенням ви не можете задати первинне значення для наслідуваного поля name
. Для задання значення наслідуваним властивостям у JavaScript, необхідно додати трохи більше коду до фукнції-конструктора.
Поки що фукнція-конструктор створила загальний об'єкт, задала на місці властивості і значення нового об'єкту. Можна зробити, щоб конструктор додавав властивості безпосередньо викликаючи конструктор об'єкта, вищого у ієрархії прототипів. Власне, як зображено у наступній схемі.
Задання властивостей у конструкторі, варіант 2
Розгляньмо детальніше одне із цих визначень. Ось новий конструктор Engineer
:
function Engineer(name, projs, mach) {
this.base = WorkerBee;
this.base(name, 'engineering', projs);
this.machine = mach || '';
}
Припустимо, ви ось так створюєте новий об'єкт Engineer
:
var jane = new Engineer('Doe, Jane', ['navigator', 'javascript'], 'belau');
Тут JavaScript виконує наступні кроки:
- Оператор
new
створює загальний об'єкт і задаєEngineer.prototype
значенням його властивості__proto__
. - Оператор
new
передає новий об'єкт у конструкторEngineer
в якості значенняthis
. - Конструктор створює нову властивість
base
для цього об'єкту і присвоює їй значення конструктораWorkerBee
. Так конструкторWorkerBee
стає методом об'єктуEngineer
. Назва властивостіbase
не є чимось особливим. Ви можете використати будь-яке дозволене ім'я властивості, аbase
просто звучить змістовно для даного випадку. - Конструктор викликає метод
base
, передаючи в нього два своїх аргументи ("Doe, Jane"
і["navigator", "javascript"]
) і новий рядок"engineering"
. Явно вказаний"engineering"
у конструкторі означає, що всі об'єктиEngineer
матимуть одне значення для наслідуваної властивостіdept
, і це значення заміщує успадковане відEmployee
. - Так як метод
base
належитьEngineer
, всередині викликуbase
JavaScript прив'язує значенняthis
до об'єкту, створеного на першому етапі. Таким чином функціяWorkerBee
, в свою чергу, передає аргументи"Doe, Jane"
та"engineering"
до конструктораEmployee
. Після повернення з конструктораEmployee
функціяWorkerBee
задає останнім аргументом полеprojects
. - Після повернення з методу
base
конструкторEngineer
ініціалізовує властивістьmachine
об'єкту значенням"belau"
. - Після повернення з конструктору JavaScript присвоює новий об'єкт змінній
jane
.
Може скластися враження, що, викликавши конструтктор WorkerBee
зсередини конструктора Engineer
, ви вкажете наслідування відповідно для об'єктів Engineer
. Насправді виклик конструктора WorkerBee
гарантує, що об'єкт Engineer
матиме всі властивості, вказані у всіх викликаних конструкторах. Однак, якщо пізніше додати нові властивості до прототипів Employee
чи WorkerBee
, вони не будуть успадковані об'єктом Engineer
. Припустимо, у нас є наступні інструкції:
function Engineer(name, projs, mach) {
this.base = WorkerBee;
this.base(name, 'engineering', projs);
this.machine = mach || '';
}
var jane = new Engineer('Doe, Jane', ['navigator', 'javascript'], 'belau');
Employee.prototype.specialty = 'none';
Об'єкт jane
не наслідує властивість specialty
. Вам все ще необхідно явно вказувати прототип для гарантії динамічного наслідування. Натомість розглянемо такий набір інструкцій:
function Engineer(name, projs, mach) {
this.base = WorkerBee;
this.base(name, 'engineering', projs);
this.machine = mach || '';
}
Engineer.prototype = new WorkerBee;
var jane = new Engineer('Doe, Jane', ['navigator', 'javascript'], 'belau');
Employee.prototype.specialty = 'none';
Тепер значенням властивості specialty
об'єкту jane
являється "none".
Іншим способом наслідування являється використання методів call()
чи apply()
. Ось дві еквівалентні ділянки коду:
function Engineer(name, projs, mach) {
this.base = WorkerBee;
this.base(name, 'engineering', projs);
this.machine = mach || '';
}
function Engineer(name, projs, mach) {
WorkerBee.call(this, name, 'engineering', projs);
this.machine = mach || '';
}
Використання методу call()
дає більш чисту реалізацію, так як base
більше не потрібен.
Іще раз про наслідування властивостей
У попередніх частинах було описано, як конструктори і прототипи JavaScript дозволяють реалізовувати ієрархію та наслідування. Ця частина розглядає деякі тонкощі, які, можливо, не були очевидними у наведеному вище тексті.
Локальні і успадковані змінні
Коли ви викликаєте властивість, JavaScript виконує наступні кроки (як описано вище у главі):
- Перевіряє наявність цієї змінної прямо в об'єкті, і повертає її значення, якщо знаходить.
- Якщо її немає серед локальних змінних - перевіряє ланцюжок прототипів (використовуючи властивість
__proto__
). - Якщо прототип у ланцюжку має значення вказаної властивості — повертає це значення.
- Якщо вказана властивість не знайдена — значить, об'єкт її не має.
Результат виконання цих кроків залежить від того, як ви задаєте значення і об'єкти. Перший приклад мав такі визначення:
function Employee() {
this.name = '';
this.dept = 'general';
}
function WorkerBee() {
this.projects = [];
}
WorkerBee.prototype = new Employee;
Припустимо, враховуючи ці визначення, що наступною інструкцією ви створили екземпляр WorkerBee
у змінній amy
:
var amy = new WorkerBee;
Об'єкт amy
має одну локальну змінну — projects
. Значення властивостей name
та dept
насправді не належать amy
, і тому виводяться через поле її поле __proto__
. Отже, amy
має такі значення властивостей:
amy.name == '';
amy.dept == 'general';
amy.projects == [];
Тепер припустимо, що ви змінили значення властивості name
у прототипі, асоційованому з Employee
:
Employee.prototype.name = 'Unknown';
На перший погляд, можна очікувати, що нове значення пошириться на всі екземпляри Employee
. Однак, це не так.
При створенні будь-якого екземпляру об'єкту Employee
, цей екземпляр отримає локальне значення для властивості name
(порожній рядок). Тобто, коли ми задаємо прототип WorkerBee
шляхом створення нового об'єкту Employee
, WorkerBee.prototype
має локальне значення властивості name
. Отже, коли JavaScript шукає властивість name
об'єкту amy
(екземпляр WorkerBee
), він знаходить місцеве значення властивості у WorkerBee.prototype
. І тому він не намагається шукати далі в ланцюжку прототипів до Employee.prototype
.
Якщо ви хочете змінити значення властивості під час виконання, та ще й так, щоб воно поширилось на всіх нащадків об'єкту, слід визначати це значення не у конструкторі об'єкту, а у прототипі конструктора. Припустимо, наприклад, що ви змінили попередній варіант реалізації на такий:
function Employee() {
this.dept = 'general'; // Зауважте, що тут немає this.name (локальної змінної)
}
Employee.prototype.name = ''; // Одна копія
function WorkerBee() {
this.projects = [];
}
WorkerBee.prototype = new Employee;
var amy = new WorkerBee;
Employee.prototype.name = 'Unknown';
У цьому випадку, властивість amy
name
стане "Unknown".
Як ілюструють ці приклади, якщо вам потрібно і мати значення за замовчуванням для властивостей об'єктів, і можливість змінювати ці значення за замовчуванням під час виконання, слід задавати властивості у прототипі конструктора, а не у самому конструкторі.
Визначення відносин екземпляру
Пошук властивості у JavaScript виконується серед власних властивостей об'єкту, і, якщо властивість з даним іменем не була знайдена, пошук переходить всередину особливої властивості __proto__
. Це відбувається рекурсивно, і цей процес називається "пошук у ланцюжку прототипів".
Властивість __proto__
встановлюється у момент конструювання об'єкту; вказує вона на властивість prototype
конструктора. Тобто вираз new Foo()
створює об'єкт із__proto__ ==
. Відповідно, зміни у Foo.prototype
Foo.prototype
впливають на пошук властивості всіх об'єктів, створених через new Foo()
.
Кожен об'єкт має властивість __proto__
(за винятком Object
); кожна функція має властивість prototype
. Тобто об'єкти можуть відноситись один до одного згідно "прототипного наслідування". Перевірити наслідування можна, порівнюючи властивість __proto__
об'єкту із об'єктом prototype
функції. JavaScript надає скорочення: оператор instanceof
порівнює об'єкт і функцію, і повертає true, якщо об'єкт є нащадком прототипу функції. Наприклад:
var f = new Foo();
var isTrue = (f instanceof Foo);
Для більш докладного прикладу, припустимо, що у нас є набір визначень із показаних у Наслідування властивостей. Створімо об'єкт Engineer
:
var chris = new Engineer('Pigman, Chris', ['jsd'], 'fiji');
Наступні вирази стосовно цього об'єкту являються істинними:
chris.__proto__ == Engineer.prototype;
chris.__proto__.__proto__ == WorkerBee.prototype;
chris.__proto__.__proto__.__proto__ == Employee.prototype;
chris.__proto__.__proto__.__proto__.__proto__ == Object.prototype;
chris.__proto__.__proto__.__proto__.__proto__.__proto__ == null;
Враховуючи це, ми можемо ось так написати функцію instanceOf
:
function instanceOf(object, constructor) {
object = object.__proto__;
while (object != null) {
if (object == constructor.prototype)
return true;
if (typeof object == 'xml') {
return constructor.prototype == XML.prototype;
}
object = object.__proto__;
}
return false;
}
Наступні вирази із визначеною вище функцією instanceOf являються істинними:
instanceOf(chris, Engineer)
instanceOf(chris, WorkerBee)
instanceOf(chris, Employee)
instanceOf(chris, Object)
Проте такий приклад в результаті видасть false
:
instanceOf(chris, SalesPerson)
Глобальна інформація у конструкторах
Потрібно бути уважним при створенні конструкторів, якщо ви задаєте в них якісь глобальні дані. Наприклад, припустимо, нам потрібно автоматично задавати унікальний ID кожному новому робітнику. Ви можете об'явити Employee
так:
var idCounter = 1;
function Employee(name, dept) {
this.name = name || '';
this.dept = dept || 'general';
this.id = idCounter++;
}
Згідно цього визначення Employee
, під час створення нового екземпляру конструктор призначає йому наявний ID, і потім підвищує на 1 глобальний лічильник ID. Тобто, якщо виконати наступні інструкції, victoria.id
буде 1, а harry.id
— 2:
var victoria = new Employee('Pigbert, Victoria', 'pubs');
var harry = new Employee('Tschopik, Harry', 'sales');
На перший погляд все чудово. Однак, idCounter
змінюється кожного разу, коли створюється екземпляр Employee
, байдуже для чого. Якщо створити всю ієрархію Employee
, наведену у цьому розділі, конструктор Employee
буде викликано кожного разу при заданні прототипу. Припустимо, у нас є наступний код:
var idCounter = 1;
function Employee(name, dept) {
this.name = name || '';
this.dept = dept || 'general';
this.id = idCounter++;
}
function Manager(name, dept, reports) {...}
Manager.prototype = new Employee;
function WorkerBee(name, dept, projs) {...}
WorkerBee.prototype = new Employee;
function Engineer(name, projs, mach) {...}
Engineer.prototype = new WorkerBee;
function SalesPerson(name, projs, quota) {...}
SalesPerson.prototype = new WorkerBee;
var mac = new Engineer('Wood, Mac');
Далі припустимо, що визначення, що опущені тут мають властивість base
і викликають констуктор, що знаходиться вище у ланцюжку прототипів. У цьому випадку, у момент створення об'єкту mac
, mac.id
буде 5.
Залежно від застосування, таке додаткове збільшення лічильника може мати, чи не мати значення. Якщо для вас важливе справжнє значення лічильника, один із варіантів рішення матиме такий вигляд конструктора:
function Employee(name, dept) {
this.name = name || '';
this.dept = dept || 'general';
if (name)
this.id = idCounter++;
}
При створенні екземпляру Employee
для застосування в якості прототипу аргументи не задаються. Згідно цього визначення конструктора, коли ми не передаємо аргументи, конструктор не задає значення id
та не оновлює лічильник. Таким чином, щоб отримати Employee
із призначеним id
, необхідно задати ім'я робітника. У цьому випадку mac.id
буде дорівнювати 1.
Інший варіант — створювати копію об'єкту-прототипу Employee, щоб потім присвоювати її WorkerBee:
WorkerBee.prototype = Object.create(Employee.prototype);
// instead of WorkerBee.prototype = new Employee
Відсутність множинного успадкування
Деякі об'єктно-орієнтовані мови програмування дозволяють множинне успадкування. Це означає, що об'єкт може наслідувати поля і властивості від незалежних батьківських об'єктів. JavaScript не підтримує такий тип наслідування.
Наслідування значення властивості відбувається в момент виконання програми, коли JavaScript шукає значення властивості у ланцюжку прототипів об'єкту. Так як об'єкт має лише один прототип, JavaScript не може виконувати наслідування від більш ніж одного прототипного ланцюжка.
У JavaScript ми можемо мати функцію-конструктор, що викликає два чи більше інших конструкторів під час виконання. Це дає певну ілюзію множинного наслідування. Наприклад, припустимо, у нас є такі інструкції:
function Hobbyist(hobby) {
this.hobby = hobby || 'scuba';
}
function Engineer(name, projs, mach, hobby) {
this.base1 = WorkerBee;
this.base1(name, 'engineering', projs);
this.base2 = Hobbyist;
this.base2(hobby);
this.machine = mach || '';
}
Engineer.prototype = new WorkerBee;
var dennis = new Engineer('Doe, Dennis', ['collabra'], 'hugo');
Далі припустимо, що визначення WorkerBee
відповідає вказаному вище у розділі. У цьому випадку об'ект dennis
матиме такі значення властивостей:
dennis.name == 'Doe, Dennis';
dennis.dept == 'engineering';
dennis.projects == ['collabra'];
dennis.machine == 'hugo';
dennis.hobby == 'scuba';
Тобто dennis
отримує поле hobby
з конструктора Hobbyist
. Однак, якщо ми потім додамо нову властивість до прототипу конструктора Hobbyist
:
Hobbyist.prototype.equipment = ['mask', 'fins', 'regulator', 'bcd'];
Об'єкт dennis
не успадковує цю нову властивість.