Переклад не закінчено. Будь ласка, допоможіть перекласти цю статтю з англійської.

JavaScript - це об'єктна мова, що заснована на прототипах, а не на класах. У зв'язку з цим може бути менш очевидно, як саме JavaScript  дозволяє створювати ієрархії об'єктів із наслідуванням їх властивостей та значень. Цей розділ є спробою дещо прояснити цей механізм.

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

Мови з класовою та прототипною моделлю

Об'єктно-орієнтовані мови з класовою моделлю, такі як Java і C++, засновані на концепції двох окремих сутностей: класів та екземплярів.

  • Клас визначає всі властивості (включаючи поля та методи у Java, чи фукнції-члени у C++, як властивості), що хаарктеризують певний набір об'єктів. Класс - це абстрактна річ, на  відміну від будь-якого окремого об'єкту із набору, який він описує. Наприклад, клас Employee може представляти групу всіх робітників.
  • Екземпляр, з іншого боку, є реалізацією класу; тобто, один із його членів. Наприклад, Victoria може бути екземпляром класу Employee, представляючи окрему особу в якості працівника. Екземпляр має точно ті самі властивості, що і батьківський клас (не більше, не менше).

Мови з прототипною моделлю наслідування, такі як JavaScript, не відокремлюють ці сутності: у них просто є об'єкти. Мови з прототипною моделлю реалізовують поняття об'єкту-прототипу — об'єкту, який використовується як зразок, з якого вибираються початкові властивості для нового об'єкту. Будь-який об'єкт може вказати власні властивості, як в момент створення, так і під час виконання. На додачі, будь-який об'єкт можна асоціювати в якості прототипу з іншим об'єктом — таким чином перший об'єкт розділить свої властивості з другим.

Визначення класу

У мовах із класовою моделлю, класс задається у окремому визначенні класу. У цому визначенні можна вказати особливі методи, що називаються конструкторами, щоб створити екземпляри класу. Метод-конструктор може задати початкові значення властивостей екземпляру, і виконати якісь інші задачі прямо у момент створення. Для створення екземплярів застосовується оператор new у комбінації із методом-конструктором.

JavaScript слідує подібній моделі, проте не має відокремленого від конструктора визначення класу. Натомість, ви одразу задаєте функцію-конструктор, щоб створити об'єкти із відповідним початковим набором властивостей та значень. Будь-яка JavaScript-функція може використовуватись як конструктор. Для створення нового об'єкту так само використовується оператор new із фукнцією-конструктором.

Дочірні класи і наслідування

У мові з класовою моделлю наслідкування ієрархія класів створюється через визначення класу. У цьому визначенні можна окремо вказати, що новий клас являється дочірнім стосовно уже наявного класу. Дочірній клас наслідує всі властивості батьківського і може привнести нові, або ж змінити наслідувані. Наприклад, припустимо, що клас Employee включає в себе лише поля name та dept, і Manager - це дочірній клас Employee, що додає властивість reports. У цьому випадку, екземпляр класу Manager буде мати три властивості: name, dept, та reports.

JavaScript реалізовує наслідування дещо інакше. Він дозволяє пов'язувати об'єкт-прототип із будь-якою фукнцією-конструктором. Тобто ви можете точнісінько реалізувати приклад EmployeeManager, проте використовуючи дещо інші терміни. Спершу ви визначаєте конструктор Employee, задаючи властивості name та dept. Далі ви визначаєте фукнцію-конструктор Manager, що викликає конструктор Employee та задає властивість reports. Насамкінець, призначаєте новий об'єкт, отриманий з Employee.prototype в якості прототипу конструктора Manager. Надалі, при створенні екземпляра Manager він наслідує властивості name і dept з об'єкту Employee.

Додавання і видалення властивостей

Зазвичай у мовах із класовою моделлю наслідування класи створюються під час компіляції, а екземпляри класів - під час компіляції чи виконання програми. Не можна змінити кількість або тип властивостей класу після того, як його було визначено. Однак, у JavaScript можна додавати чи видаляти властивості будь-якого об'єкту безпосередньо під час виконання програми. Якщо додати нову властивість до об'єкту-прототипу певного переліку об'єктів, всі вони також отримають цю властивість.

Підсумок відмінностей

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

Порівняння мови із класовою моделлю наслідування (Java) і прототипною (JavaScript)
Класова модель (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. Який спосіб обрати — значною мірою залежить від того, які можливості ви хочете отримати від вашого додатку.

Цей розділ показує, як використовувати дуже прості (і відносно негнучкі) визначення, і таким чином демонструє, як отримати робочий механізм наслідування. У цих визначеннях не можна задати жодного значення при створенні об'єкту — він отримає властивості із значеннями за замовчуванням, які можна буде змінити пізніше.

У реальному додатку ви б, ймовірно, визначали конструктор, що дозволяє задавати значення в момент створення об'єкту (докладніше тут More flexible constructors). А наразі ці прості визначення покажуть, як власне відбувається наслідування.

Наступні визначення 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 є екземпляром класу Engineer. Так само, хоча терміни предок, нащадок, дочірній і батьківський класи не мають формального смісту в JavaScript, їх можна застосовувати для позначення об'єктів, що знаходяться вище чи нижче у ланцюжку прототипів.

Створення об'єктів за допомогою простих визначень

Ієрархія об'єктів

Наступна ієрархія створена за допомогою коду у правій частині.

Окремі об'єкти = 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 = [];

Об'єкт mark призначив місцеві значення для властивостей name та dept з конструктора Employee, а значення властивості projects — з конструктора WorkerBee. Таким чином ми отримуємо наслідування властивостей і їх значень у JavaScript. Деякі тонкощі цього процесу додатково висвітлені у Property inheritance revisited.

Так як ці конструктори не дозволяють задати специфічні для екземпляру значення, це являється загальним прикладом. Значення властивостей - за замовчуванням, що розповсюджуються на всі об'єкти, створені на основі 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 тут виконує наступні кроки:

  1. Оператор new створює загальний об'єкт і задає Engineer.prototype значенням його властивості __proto__ .
  2. Оператор new передає новий об'єкт у конструктор Engineer в якості значення this.
  3. Конструктор створює нову властивість base для цього об'єкту і присвоює їй значення конструктора WorkerBee. Так конструктор WorkerBee стає методом об'єкту Engineer. Назва властивості base не є чимось особливим. Ви можете використати будь-яке дозволене ім'я поля; а base просто підходить за змістом.
  4. Конструктор викликає метод base, передаючи в нього два своїх аргументи ("Doe, Jane" і ["navigator", "javascript"]) і новий рядок "engineering". Явно вказаний "engineering" у конструкторі означає, що всі об'єкти Engineer матимуть одне значення для наслідуваної властивості dept, і це значення заміняє наслідуване від Employee.
  5. Так як метод base належить Engineer, всередині виклику base JavaScript прив'язує значення this до об'єкту, створеного на першому етапі. Таким чином функція WorkerBee, в свою чергу, передає аргументи "Doe, Jane" та "engineering" до конструктора Employee. Після повернення з конструктора Employee функція WorkerBee задає останнім аргументом поле projects.
  6. Після повернення з методу base конструктор Engineer ініціалізовує властивість machine об'єкту значенням "belau".
  7. Після повернення з конструктору 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 виконує наступні кроки (як описано вище у главі):

  1. Перевіряє наявність цієї змінної прямо в об'єкті, і повертає її значення, якщо знаходить.
  2. Якщо її немає серед локальних змінних - перевіряє ланцюжок прототипів (використовуючи властивість __proto__).
  3. Якщо прототип у ланцюжку має значення вказаної властивості — повертає це значення.
  4. Якщо вказана властивість не знайдена — значить, об'єкт її не має.

Результат виконання цих кроків залежить від того, як ви задаєте значення і об'єкти.  Перший приклад мав такі визначення:

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".

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

Визначення відносин екземпляру

Property lookup in JavaScript looks within an object's own properties and, if the property name is not found, it looks within the special object property __proto__. This continues recursively; the process is called "lookup in the prototype chain".

The special property __proto__ is set when an object is constructed; it is set to the value of the constructor's prototype property. So the expression new Foo() creates an object with __proto__ == Foo.prototype. Consequently, changes to the properties of Foo.prototype alters the property lookup for all objects that were created by new Foo().

Every object has a __proto__ object property (except Object); every function has a prototype object property. So objects can be related by 'prototype inheritance' to other objects. You can test for inheritance by comparing an object's __proto__ to a function's prototype object. JavaScript provides a shortcut: the instanceof operator tests an object against a function and returns true if the object inherits from the function prototype. For example,

var f = new Foo();
var isTrue = (f instanceof Foo);

For a more detailed example, suppose you have the same set of definitions shown in Inheriting properties. Create an Engineer object as follows:

var chris = new Engineer('Pigman, Chris', ['jsd'], 'fiji');

With this object, the following statements are all true:

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;

Given this, you could write an instanceOf function as follows:

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;
}
Note: The implementation above checks the type of the object against "xml" in order to work around a quirk of how XML objects are represented in recent versions of JavaScript. See bug 634150 if you want the nitty-gritty details.

Using the instanceOf function defined above, these expressions are true:

instanceOf(chris, Engineer)
instanceOf(chris, WorkerBee)
instanceOf(chris, Employee)
instanceOf(chris, Object)

But the following expression is false:

instanceOf(chris, SalesPerson)

Global information in constructors

When you create constructors, you need to be careful if you set global information in the constructor. For example, assume that you want a unique ID to be automatically assigned to each new employee. You could use the following definition for Employee:

var idCounter = 1;

function Employee(name, dept) {
   this.name = name || '';
   this.dept = dept || 'general';
   this.id = idCounter++;
}

With this definition, when you create a new Employee, the constructor assigns it the next ID in sequence and then increments the global ID counter. So, if your next statement is the following, victoria.id is 1 and harry.id is 2:

var victoria = new Employee('Pigbert, Victoria', 'pubs');
var harry = new Employee('Tschopik, Harry', 'sales');

At first glance that seems fine. However, idCounter gets incremented every time an Employee object is created, for whatever purpose. If you create the entire Employee hierarchy shown in this chapter, the Employee constructor is called every time you set up a prototype. Suppose you have the following code:

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');

Further assume that the definitions omitted here have the base property and call the constructor above them in the prototype chain. In this case, by the time the mac object is created, mac.id is 5.

Depending on the application, it may or may not matter that the counter has been incremented these extra times. If you care about the exact value of this counter, one possible solution involves instead using the following constructor:

function Employee(name, dept) {
   this.name = name || '';
   this.dept = dept || 'general';
   if (name)
      this.id = idCounter++;
}

When you create an instance of Employee to use as a prototype, you do not supply arguments to the constructor. Using this definition of the constructor, when you do not supply arguments, the constructor does not assign a value to the id and does not update the counter. Therefore, for an Employee to get an assigned id, you must specify a name for the employee. In this example, mac.id would be 1.

Alternatively, you can create a copy of Employee's prototype object to assign to WorkerBee:

WorkerBee.prototype = Object.create(Employee.prototype);
// instead of WorkerBee.prototype = new Employee

No multiple inheritance

Some object-oriented languages allow multiple inheritance. That is, an object can inherit the properties and values from unrelated parent objects. JavaScript does not support multiple inheritance.

Inheritance of property values occurs at run time by JavaScript searching the prototype chain of an object to find a value. Because an object has a single associated prototype, JavaScript cannot dynamically inherit from more than one prototype chain.

In JavaScript, you can have a constructor function call more than one other constructor function within it. This gives the illusion of multiple inheritance. For example, consider the following statements:

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');

Further assume that the definition of WorkerBee is as used earlier in this chapter. In this case, the dennis object has these properties:

dennis.name == 'Doe, Dennis';
dennis.dept == 'engineering';
dennis.projects == ['collabra'];
dennis.machine == 'hugo';
dennis.hobby == 'scuba';

So dennis does get the hobby property from the Hobbyist constructor. However, assume you then add a property to the Hobbyist constructor's prototype:

Hobbyist.prototype.equipment = ['mask', 'fins', 'regulator', 'bcd'];

The dennis object does not inherit this new property.

Мітки документа й учасники

Зробили внесок у цю сторінку: AdriandeCita, PavloEleva
Востаннє оновлена: AdriandeCita,