Існує дві моделі реалізації класів та екземплярів у JavaScript: спосіб прототипування та спосіб закриття. Обидва мають переваги та недоліки, і існує велика кількість розширених варіацій. Багато програмістів і бібліотек мають різні підходи та функції корисних функцій для обробки класів, щоб надрукувати деякі найменші частини мови.
Результат полягає в тому, що в змішаній компанії у вас буде мішаний метаклас, і всі поводяться трохи інакше. Що ще гірше, більшість матеріалів підручника JavaScript жахливі і служать певним компромісом для покриття всіх баз, залишаючи вас дуже розгубленими. (Можливо, автор також заплутався. Об'єктна модель JavaScript сильно відрізняється від більшості мов програмування, а в багатьох місцях прямолінійна погано розроблена.)
Почнемо з прототипу . Це найрізноманітніший JavaScript, який ви можете отримати: є мінімум накладного коду, а instanceof буде працювати з екземплярами такого типу об’єктів.
function Shape(x, y) {
this.x= x;
this.y= y;
}
Ми можемо додати методи до створеного екземпляра new Shape
, записавши їх у prototype
пошук цієї функції конструктора:
Shape.prototype.toString= function() {
return 'Shape at '+this.x+', '+this.y;
};
Тепер, щоб підкласирувати його, настільки, наскільки ви можете назвати те, що JavaScript робить субкласифікацією. Ми робимо це, повністю замінивши це дивне магічне prototype
властивість:
function Circle(x, y, r) {
Shape.call(this, x, y); // invoke the base class's constructor function to take co-ords
this.r= r;
}
Circle.prototype= new Shape();
перед додаванням методів до нього:
Circle.prototype.toString= function() {
return 'Circular '+Shape.prototype.toString.call(this)+' with radius '+this.r;
}
Цей приклад спрацює, і ви побачите код, як він в багатьох навчальних посібниках. Але людина, new Shape()
це некрасиво: ми створюємо базовий клас, навіть не маючи створити жодної фактичної форми. Трапляється працювати в цьому простому випадку , оскільки JavaScript настільки неаккуратен: вона дозволяє нульові аргументи, які передаються в, в якому випадку x
і y
стати undefined
і призначені на прототип this.x
і this.y
. Якби функція конструктора робила щось складніше, вона падала б на обличчя.
Отже, що нам потрібно зробити - це знайти спосіб створити об’єкт-прототип, який містить методи та інших членів, яких ми хочемо на рівні класу, без виклику конструкторської функції базового класу. Для цього нам доведеться почати писати допоміжний код. Це найпростіший підхід, про який я знаю:
function subclassOf(base) {
_subclassOf.prototype= base.prototype;
return new _subclassOf();
}
function _subclassOf() {};
Це передає учасникам базового класу в його прототипі нову функцію конструктора, яка нічого не робить, потім використовує цей конструктор. Тепер ми можемо просто написати:
function Circle(x, y, r) {
Shape.call(this, x, y);
this.r= r;
}
Circle.prototype= subclassOf(Shape);
замість new Shape()
неправильності. Зараз у нас є прийнятний набір примітивів для побудованих класів.
Є кілька вдосконалень та розширень, які ми можемо розглянути в цій моделі. Наприклад, ось синтаксична-цукрова версія:
Function.prototype.subclass= function(base) {
var c= Function.prototype.subclass.nonconstructor;
c.prototype= base.prototype;
this.prototype= new c();
};
Function.prototype.subclass.nonconstructor= function() {};
...
function Circle(x, y, r) {
Shape.call(this, x, y);
this.r= r;
}
Circle.subclass(Shape);
Будь-яка версія має недолік, що конструкторська функція не може бути успадкована, як це є у багатьох мовах. Тож навіть якщо ваш підклас нічого не додає до процесу побудови, він повинен пам’ятати, щоб викликати конструктор бази з будь-якими аргументами, які хотіла база. Це можна злегка автоматизувати за допомогою apply
, але все-таки доведеться виписати:
function Point() {
Shape.apply(this, arguments);
}
Point.subclass(Shape);
Тож загальне розширення полягає в тому, щоб вирвати ініціалізацію в свою функцію, а не сам конструктор. Потім ця функція може просто успадкувати від бази:
function Shape() { this._init.apply(this, arguments); }
Shape.prototype._init= function(x, y) {
this.x= x;
this.y= y;
};
function Point() { this._init.apply(this, arguments); }
Point.subclass(Shape);
// no need to write new initialiser for Point!
Зараз у нас тільки одна і та ж плитка для функціонування конструктора для кожного класу. Можливо, ми можемо перенести це у свою функцію помічника, тому нам не доведеться продовжувати друкувати, наприклад, замість того Function.prototype.subclass
, щоб повертати його і дозволяти Функції базового класу виплювати підкласи:
Function.prototype.makeSubclass= function() {
function Class() {
if ('_init' in this)
this._init.apply(this, arguments);
}
Function.prototype.makeSubclass.nonconstructor.prototype= this.prototype;
Class.prototype= new Function.prototype.makeSubclass.nonconstructor();
return Class;
};
Function.prototype.makeSubclass.nonconstructor= function() {};
...
Shape= Object.makeSubclass();
Shape.prototype._init= function(x, y) {
this.x= x;
this.y= y;
};
Point= Shape.makeSubclass();
Circle= Shape.makeSubclass();
Circle.prototype._init= function(x, y, r) {
Shape.prototype._init.call(this, x, y);
this.r= r;
};
... яка починає трохи більше нагадувати інші мови, хоча і з дещо незграбним синтаксисом. При бажанні можна посипати декілька додаткових функцій. Можливо, ви хочете makeSubclass
взяти і запам'ятати ім'я класу та надати за замовчуванням toString
його використання. Можливо, ви хочете змусити конструктор виявити, коли його випадково викликали без new
оператора (що в іншому випадку часто призводить до дуже дратівливої налагодження):
Function.prototype.makeSubclass= function() {
function Class() {
if (!(this instanceof Class))
throw('Constructor called without "new"');
...
Можливо, ви хочете передати всіх нових членів і makeSubclass
додати їх до прототипу, щоб заощадити, вам потрібно писати Class.prototype...
досить багато. Багато систем класів роблять це, наприклад:
Circle= Shape.makeSubclass({
_init: function(x, y, z) {
Shape.prototype._init.call(this, x, y);
this.r= r;
},
...
});
У об’єктній системі існує багато потенційних можливостей, які можна вважати бажаними, і ніхто не погоджується на одну конкретну формулу.
Застібка , потім. Це дозволяє уникнути проблем спадкування на основі прототипу JavaScript, взагалі не використовуючи спадкування. Замість цього:
function Shape(x, y) {
var that= this;
this.x= x;
this.y= y;
this.toString= function() {
return 'Shape at '+that.x+', '+that.y;
};
}
function Circle(x, y, r) {
var that= this;
Shape.call(this, x, y);
this.r= r;
var _baseToString= this.toString;
this.toString= function() {
return 'Circular '+_baseToString(that)+' with radius '+that.r;
};
};
var mycircle= new Circle();
Тепер кожен окремий екземпляр Shape
матиме власну копію toString
методу (та будь-яких інших методів чи інших членів класу, які ми додаємо).
Погано, що кожен екземпляр має свою копію кожного члена класу - це менш ефективний. Якщо ви маєте справу з великою кількістю підкласових примірників, прототипічне успадкування може вам служити краще. Виклик методу базового класу трохи дратує, як ви бачите: ми повинні пам’ятати, яким був метод до того, як конструктор підкласу його переробив, або він загубиться.
[Крім того, оскільки тут немає спадщини, instanceof
оператор не буде працювати; вам доведеться надати власний механізм нюхування класів, якщо він вам потрібен. У той час як ви могли обробляти об'єкти прототипу аналогічно, як і при успадкуванні прототипу, це трохи хитро, і насправді не варто лише instanceof
працювати.]
Хороша річ у кожному екземплярі, який має свій метод, - це те, що метод може бути потім прив’язаний до конкретного екземпляра, який йому належить. Це корисно, оскільки дивний спосіб прив'язки JavaScript this
у викликах методу, який має підсумок, що якщо ви відстороните метод від його власника:
var ts= mycircle.toString;
alert(ts());
то this
всередині методу не буде екземпляра Circle, як очікувалося (він насправді буде глобальним window
об'єктом, викликаючи широке горе налагодження). Насправді це зазвичай відбувається , коли метод приймається і присвоюється setTimeout
, onclick
або EventListener
в цілому.
З використанням прототипу ви повинні включити закриття для кожного такого завдання:
setTimeout(function() {
mycircle.move(1, 1);
}, 1000);
або, в майбутньому (або зараз, якщо ви зламаєте Function.prototype), ви також можете це зробити за допомогою function.bind()
:
setTimeout(mycircle.move.bind(mycircle, 1, 1), 1000);
якщо ваші екземпляри зроблені способом закриття, прив'язка здійснюється безкоштовно закриттям через змінну екземпляра (як правило, викликається that
або self
, хоча особисто я б радив проти останнього, оскільки self
в JavaScript вже є інше, інше значення). Ви не отримуєте аргументи 1, 1
у наведеному вище фрагменті безкоштовно, тому вам все одно знадобиться ще одне закриття або, bind()
якщо вам потрібно це зробити.
Існує також багато варіантів методу закриття. Ви можете відмовитися this
повністю пропустити , створивши новий that
і повернути його замість new
оператора:
function Shape(x, y) {
var that= {};
that.x= x;
that.y= y;
that.toString= function() {
return 'Shape at '+that.x+', '+that.y;
};
return that;
}
function Circle(x, y, r) {
var that= Shape(x, y);
that.r= r;
var _baseToString= that.toString;
that.toString= function() {
return 'Circular '+_baseToString(that)+' with radius '+r;
};
return that;
};
var mycircle= Circle(); // you can include `new` if you want but it won't do anything
Який шлях "належний"? І те й інше. Що найкраще? Це залежить від вашої ситуації. FWIW я схильний до складання прототипів для наслідування справжнього JavaScript, коли я роблю сильно OO, та закриття для простих ефектів сторінки, що викидається.
Але обидва способи є досить протиінтуїтивними для більшості програмістів. Обидва мають багато потенційних безладних варіацій. Ви зустрінете обидві (а також безліч схем між і загалом зламаними), якщо будете використовувати код / бібліотеки інших людей. Немає жодної загальновизнаної відповіді. Ласкаво просимо у чудовий світ об’єктів JavaScript.
[Це частина 94 статті, чому JavaScript не є моєю улюбленою мовою програмування.]