Поставим перед собой несколько целей:
- Наследование должно быть полноценным, т.е. конструкция
object instanceof Class
должна работать [^] - Реализовать простую возможность вызова конструктора [^] или любого метода
базового класса [^] - Простота в использовании, стройность и лёгкость кода [^]
- По возможности, облегчить задачу современным IDE (в частности Spket) для корректного функционирования code complete по созданным объектам [^]
// Пишем конструктор базового класса /** * Некий абстрактный человек, имеющий имя и * умеющий выполнять некоторые повседневные действия * @constructor Person * @param name {String} Имя человека */ var Person = function (name) { // Инициализируем public свойство name this.name = name; // Локальные переменные определённые внутри конструктора // можно использовать, при необходимости, как private члены класса // но только через специальные public методы (getter и/или setter), // определённые здесь же - в конструкторе // // var somePrivateVariable = {}; // this._getSomePrivateVariable = function() { // return somePrivateVariable; // } } // Описываем члены базового класса внутри wrapper-функции // и сохраняем ссылку на неё в public static свойство $class // для дальнейшего использования // ПРИМЕЧАНИЕ: // Естественно, wrapper-функция может быть и анонимной. // В данном примере имя (совпадающее с именем класса) используется // только для того, чтобы получить более дружественный code complete в IDE /** * @class Person */ Person.$class = function Person() { // Локальные переменные определённые в этой области // будут что-то вроде private static членов класса var $className = 'Person'; // Описываем public свойства класса /** * Имя человека * @type {String} */ this.name = ''; // Описываем public методы класса /** * Возвращает список ежедневных действий человека * @returns {String} */ this.performEverydayActions = function () { return '- Спит, ест, развлекается [as a ' + $className + ']'; } /** * Возвращает имя человека * @returns {String} */ this.getName = function () { return this.name; } this.toString = function () { return this.name; } } // "Компилируем" базовый класс, создавая прототип из wrapper-функции Person.prototype = new Person.$class();Далее, создадим производный класс:
// Пишем конструктор производного класса /** * Некий абстрактный работник, имеющий имя (как и любой человек) и * умеющий выполнять какую-либо работу * @constructor Employee * @extends Person * @param name {String} Имя работника * @param job {String} Функция работника (или работа, которую он умеет выполнять) */ var Employee = function (name, job) { // Вызываем конструктор базового класса Person // (через ссылку $super, которую установим ниже), // чтобы проинициализировать имя при создании // объекта производного класса (Employee) arguments.callee.$super.call(this, name); // Инициализируем public свойство job this.job = job; } // Сохраняем ссылку на базовый класс для дальнейшего использования Employee.$super = Person; // Описываем члены производного класса внутри wrapper-функции // и сохраняем ссылку на неё для дальнейшего использования /** * @class Employee * @extends Person * @param $super {Object} Прототип базового класса */ Employee.$class = function Employee($super) { // Внутрь wrapper-функция производного класса будем передавать // в качестве агрумента ссылку на прототип базового класса. // Это существенно облегчит обращение к методам базового класса (см. ниже) var $className = 'Employee'; // Описываем public свойства класса /** * Функция работника * @type {String} */ this.job = ''; // Описываем public методы класса /** * Возвращает функцию работника * @returns {String} */ this.getJob = function () { return this.job; } // Переопределяем метод базового класса /** * Возвращает список ежедневных действий человека, включая его рабочие функции * @returns {String} */ this.performEverydayActions = function () { // Вызываем метод базового класса return $super.performEverydayActions.call(this) + '\n- ' + this.job + ' [as an ' + $className + ']'; } } // Устанавливаем наследование производного класса // от базового класса через прототипы Employee.$class.prototype = Employee.$super.prototype; // "Компилируем" класс, создавая прототип из wrapper-функции // и передавая в качестве аргумента прототип базового класса Employee.prototype = new Employee.$class(Employee.$super.prototype);Теперь, создадим класс производный от производного (именно на этом шаге многие попытки реализовать классовое наследование терпели неудачу):
// Пишем конструктор ещё одного производного класса /** * Программист, который может писать код * @constructor Developer * @extends Employee * @param {String} name Имя */ var Developer = function (name) { // Вызываем конструктор базового класса (Employee) arguments.callee.$super.call(this, name, 'Пишет код'); } // Т.к. класс Developer не имеет собственных членов, то wrapper - пустая функция /** * @class Developer * @extends Employee */ Developer.$class = function () {}; // Сохраняем ссылку на базовый класс Developer.$super = Employee; // Устанавливаем наследование Developer.$class.prototype = Developer.$super.prototype; // "Компилируем" класс Developer.prototype = new Developer.$class(Developer.$super.prototype);Ещё немного усложним - создадим третий производный класс:
// Пишем конструктор последнего производного класса /** * Ведущий инженер-программист, который ещё и командой разработчиков управляет * @constructor LeadDeveloper * @extends Developer * @param {String} name Имя * @param {Developer[]} team Команда программистов */ var LeadDeveloper = function(name, team) { // Вызываем конструктор базового класса (Developer) arguments.callee.$super.call(this, name); // Инициализируем public свойство team this.team = team; } // Сохраняем ссылку на базовый класс LeadDeveloper.$super = Developer; // Описываем члены класса /** * @class LeadDeveloper * @extends Developer * @param $super {Object} Прототип базового класса */ LeadDeveloper.$class = function LeadDeveloper($super) { var $className = 'LeadDeveloper'; // Описываем public свойства класса /** * Команда программистов, которой руководит данный Lead * @type {Developer[]} */ this.team = []; // Описываем public методы класса /** * Возвращает подчиненных данного Lead-а * @returns {Developer[]} */ this.getTeam = function () { return this.team; } // Переопределяем метод базового класса /** * Возвращает список ежедневных действий человека, включая его рабочие функции * @returns {String} */ this.performEverydayActions = function () { // Вызываем метод базового класса return $super.performEverydayActions.call(this) + '\n- Управляет командой: [as a ' + $className + ']\n ' + this.team.join('\n '); } } // Устанавливаем наследование LeadDeveloper.$class.prototype = LeadDeveloper.$super.prototype; // "Компилируем" класс LeadDeveloper.prototype = new LeadDeveloper.$class(LeadDeveloper.$super.prototype);Теперь проверим, что получилось:
var ivanov = new Person('Иванов Василий'); alert(ivanov.getName() + ':\n' + ivanov.performEverydayActions()); // Иванов Василий: // - Спит, ест, развлекается [as a Person] var petrova = new Employee('Петрова Мария','Доит коров'); alert(petrova.getName() + ':\n' + petrova.performEverydayActions()); // Петрова Мария: // - Спит, ест, развлекается [as a Person] // - Доит коров [as an Employee] var sidorov = new Developer('Сидоров Алексей'); var pupkin = new Developer('Пупкин Иван'); var shishkin = new Developer('Шишкин Пётр'); alert(sidorov.getName() + ':\n' + sidorov.performEverydayActions()); // Сидоров Алексей: // - Спит, ест, развлекается [as a Person] // - Пишет код [as an Employee] var myshkin = new LeadDeveloper('Мышкин Фёдор', [sidorov, pupkin, shishkin]); alert(myshkin.getName() + ':\n' + myshkin.performEverydayActions()); // Мышкин Фёдор: // - Спит, ест, развлекается [as a Person] // - Пишет код [as an Employee] // - Управляет командой: [as a LeadDeveloper] // Сидоров Алексей // Пупкин Иван // Шишкин Пётр alert(myshkin instanceof Person); // true alert(myshkin instanceof Employee); // true alert(myshkin instanceof Developer); // true alert(myshkin instanceof LeadDeveloper); // true[Весь предыдущий код одним файлом: classes-start.js]
Всё отлично работает. Code complete в Spket без проблем выдаёт список свойств и методов объекта myshkin, причем по каждому из них указан класс, который их содержит:
Осталось сделать красивую "обёртку":
Function.prototype.$extends = function ($super) { this.$super = $super; return this; } Function.prototype.$class = function ($class) { if ($class == null) { $class = function () {}; } this.$class = $class; if (this.$super != null) this.$class.prototype = this.$super.prototype; this.prototype = new $class(this.$super!=null ? this.$super.prototype : null); return this; }И "упаковать" (для примера, только два последних класса, чтобы не повторять опять гору кода):
var Developer = function (name) { arguments.callee.$super.call(this, name, 'Пишет код'); }.$extends( Employee ).$class(); var LeadDeveloper = function(name, team) { arguments.callee.$super.call(this, name); this.team = team; }.$extends( Developer ).$class( function ($super) { var $className = 'LeadDeveloper'; this.team = []; this.getTeam = function () { return this.team; } this.performEverydayActions = function () { return $super.performEverydayActions.call(this) + '\n- Управляет командой: [as a ' + $className + ']\n ' + this.team.join('\n '); } });[Финальная реализация одним файлом: classes-final.js]
К сожалению, данная конструкция уже не по зубам IDE и code complete в финальной варианте всё-таки работать не будет. Однако, есть некоторые хитрости, которые позволят извлечь максимум пользы из всего вышеизложенного на различных фазах девелопмента.
Но об этом как-нибудь в другой раз...
Ссылки по теме:
- Details of the Object Model
- Inheritance
- Classical Inheritance in JavaScript
- Prototypal Inheritance in JavaScript
- ООП в Java Script: Наследование классов
Если в Function.prototype.class() передавать не анонимную функцию, а объявленную, eclipse будет показывать свойства и методы данного класса. Конечно, это не code complete, но все же.
ОтветитьУдалитьТакже можно расширить прототип и добавить Function.prototype.$implements(), чтобы добавлять методы из других объектов.
Function.prototype.$implements = function()
{
this.$interfaces = arguments;
return this;
};
Function.prototype.$class = function(base)
{
if ('function' != typeof base) {
base = function(){};
}
if ('function' == typeof this.$parent) {
var parent = this.$parent;
base.prototype = parent.prototype;
}
this.prototype = new base(parent ? parent.prototype : null);
if (this.$interfaces) {
for (var i = this.$interfaces.length - 1; i > -1; -- i) {
for (var field in this.$interfaces[i]) {
if (undefined === this.prototype[field]) {
this.prototype[field] = this.$interfaces[i][field];
}
}
}
delete this.$interfaces;
}
return this;
};
Andrew, спасибо за комментарий!
ОтветитьУдалитьЧестно говоря, я не сторонник "вслепую" копировать свойства одного объекта другому. Это может существенно ухудшить читабельность и понятность кода, ну и привести к ошибкам.
Я бы, при необходимости, просто вручную прописывал нужные свойства (точнее методы) где-нибудь в конструкторе {
...
this.externalMethod = someObject.internalMethod;
...
}
Хотя, я допускаю, что метод $implements() можно понадобиться, хотя бы в качестве хэлпера.
Если же это попытка имитации "множественного наследования", то тогда, для полноты картины :), можно добавить ещё функцию Object.prototype.$instanceof(class), которая будет пробегаться по массиву this.$interfaces. Строку delete this.$interfaces; в вашем коде, конечно, надо будет убрать для этого.
В общем, я тоже над этой темой задумывался, но практической пользы пока не нашёл.
В общем-то польза может быть. Например, у вас есть набор объектов, некоторые из которых должны реализовать стандартные методы, предположим некие геттеры.
ОтветитьУдалитьВ этом случае мы или вводим дополнительный класс для наследования и наследуем от него, или расшираем прототипы при помощи вот такого искуственного "множественного наследования" :)