Прототипно-орієнтований JavaScript

Григорий Гребинюк
02 марта 2011
Григорий Гребинюк,
Программист компании OWOX

Досить часто в роботі використовую властивість «prototype» JavaScript об’єктів для зміни значень властивостей одразу у всіх примірників (instances) конкретного типу. Але прочитавши статтю «Разбираемся с prototype, __proto__, constructor и их цепочками в картинках», вирішив провести низку власних дослідів, щоб більш поглиблено розібратися яким саме чином примірники «класів» пов’язані з базовими об’єктами. І як каже один мій друг: «Наступний рівень самонавчання — навчання когось іншого». Тому пост сформую у вигляді лекції.


Перш за все, що ж таке «прототипне програмування»?

Згідно Wikipedia:

Прототипне програмування — стиль об’єктно-орієнтованого програмування, при якому відсутнє поняття класу, а повторне використання (успадкування) проводиться шляхом клонування існуючого примірника об’єкта — прототипу.

У випадку JavaScript: набування властивостей і методів батьківського типу відбувається через посилання на прототип конструктора. Саме час надати визначення властивостям constructor, prototype, __proto__.

  • constructor — властивість об’єкта прототипу, зберігає посилання на функцію, яка створила примірник.
  • prototype — властивість об’єкта конструктора, зберігає посилання на об’єкт, який має властивості і методи притаманні всім примірникам, створених цим конструкторoм.
  • __proto__ — властивість примірника, зберігає посилання на прототип конструктора, яким був створений.

Так як всі об’єкти в JavaScript походять від Object, то всі об’єкти успадковують властивості і методи від Object.prototype.


Розглянемо приклад

Створимо примірник Object:

Прототипно-орієнтований JavaScript - примірник Object

Ми викликали конструктор var obj = new Object() і отримали об’єкт, який має одну єдину властивість __proto__. Але для звернення доступні всі властивості і методи, які є у прототипі конструктора. JavaScript інтерпретатор спочатку шукає властивість чи метод, за яким було звернення, серед власних в obj. У випадку невдалого результату через посилання __proto__ переходить до прототипу і перевіряє його на наявність властивості чи метода, за яким було звернення. І так аж поки звернення не буде знайдене, або не буде досягнутий прототип, який не має посилання __proto__. В такому випадку отримаємо «undefined» (якщо зверталися до властивості), чи помилку «has no method» (якщо намагалися викликати метод).

Зверніть увагу, що конструктор Object є функцією і, як всі примірники створені конструктором Function, має посилання Function.prototype, той в свою чергу має в якості прототипа Object.prototype. Крім того, конструктор Function також є функцією, тому одночасно має посилання __proto__ на Function.prototype (як примірник створений «самим-собою» :) і властивість prototype, яка теж посилається на Function.prototype (щоб всі нащадки мали доступ до властивостей і методів цього прототипу).

Зі зв’язками об’єктів і принципом наслідування розібралися. Але на цьому вся краса реалізації прототипів в JavaScript лише починається. Всі три властивості можна динамічно перевизначити в залежності від потреб.


Почнемо з властивості __proto__

Припустимо ми створили власний тип Lifeform і додатково два типи Animal і Plant, які за логікою повинні успадковувати всі властивості і методи від типу Lifeform. Для цього посилання на батьківський прототип в прототипах типів Animal і Plant (Animal.prototype.__proto__ і Plant.prototype.__proto__) замінимо на прототип Lifeform (Lifeform.prototype)


function extend(child, supertype)
{
   child.prototype.__proto__ = supertype.prototype;
}

extend(Animal, Lifeform);
extend(Plant, Lifeform);

var anOnion = new Plant();

Таким чином примірник типу Plant — «цибулина» успадкує всі властивості і методи від Plant.prototype, Lifeform.prototype і Object.prorotype.

Варто також згадати що є можливість за бажанням (за потреби) заборонити розширювати властивості об’єкта за допомогою функції Object.preventExtensions(obj).


Зміна властивості prototype

За допомогою зміни властивості prototype у об’єкта конструктора, можна досягнути того, що об’єкти створені одним і тим самим конструктором будуть мати різні набори властивостей і методів. Через те, що в них властивість __proto__ посилається на різні об’єкти прототипів.


І нарешті зміна властивості constructor

Навіщо може виникнути потреба змінювати значення властивості constructor у об’єкта прототипа, чесно кажучи, не знаю. Але така можливість є для всіх типів (окрім примітивних Boolean, Number чи String).

Наступний приклад покаже, що не треба стовідсотково довіряти значенню властивості constructor, так як воно може бути змінено. Краще перевіряти чи є об’єкт примірником конкретного типу за допомогою конструкції: object instanceof Type.


function Type(){};
var types = [
 new Array, [],
 new Boolean, true,
 new Date,
 new Error,
 new Function, function(){},
 Math, 
 new Number, 1,
 new Object, {},
 new RegExp, /(?:)/,
 new String, "test"
];
for(var i = 0; i < types.length; i++){
 types[i].constructor = Type;
 types[i] = [types[i].constructor, types[i] instanceof Type, types[i].toString()];
};
alert(types.join("\n"));

Виведе:


function Type(){},false,
function Type(){},false,
function Type(){},false,false
function Boolean() { [native code] },false,true
function Type(){},false,Sun Dec 26 2010 22:35:47 GMT+0200 (Eastern Europe Standard Time)
function Type(){},false,Error
function Type(){},false,function anonymous() {

}
function Type(){},false,function (){}
function Type(){},false,[object Math]
function Type(){},false,0
function Number() { [native code] },false,1
function Type(){},false,[object Object]
function Type(){},false,[object Object]
function Type(){},false,/(?:)/
function Type(){},false,/(?:)/
function Type(){},false,
function String() { [native code] },false,test


Це і все що я хотів розповісти. Буду радий якщо комусь ця стаття допоможе.

Джерело: Блоґ звичайного програміста

Метки: javascript, Prototype

Комментарии

Добавить комментарий