Создание объектов с помощью функций конструктора довольно просто в JavaScript. Но как только вы хотите заняться наследованием, все становится сложнее. Этот пост исследует, как наследование работает в традиционном JavaScript. Затем он представляет четыре API-интерфейса, которые упрощают работу, не добавляя слишком большого объема в язык: ECMAScript 5, YUI, Prototype.js и простое наследование Джона Резига.
Как выглядит API, который помогает нам с наследованием, но в противном случае изменяет JavaScript как можно меньше? Мы используем имя «фабрика объектов» для конструкции, которая позволяет нам создавать объекты. Вот необходимые части API:
- Единая конструкция для создания объекта. Наличие единой конструкции означает меньше беспорядка. И наоборот, определение функции конструктора в традиционном JavaScript — это всегда двухэтапный процесс: сначала функция, затем прототип.
- Вызовите объектную фабрику через нового оператора. В результате код, использующий API, выглядит знакомым и, следовательно, его легче понять.
- Определите, что является общим для объектов, а что нет. Методы обычно попадают в первую категорию, свойства данных обычно попадают во вторую. Эти определения — это то, что представляет собой создание объекта.
- Наследование. Сложно с традиционным JavaScript, поэтому мы хотим, чтобы API помог нам.
- Добавление методов к существующим объектным фабрикам. Добавление методов к свойству prototype функции конструктора является обычной практикой в JavaScript и должно поддерживаться API.
- Легкий вызов переопределенных методов. Многословно в традиционном JavaScript.
- Поддержка экземпляров тестов. Должен быть способ проверить, является ли объект экземпляром данной фабрики объектов.
В следующих разделах мы сначала рассмотрим, как осуществляется наследование в традиционном JavaScript, а затем перейдем к четырем API, которые соответствуют вышеупомянутым требованиям: ECMAScript 5, YUI, Prototype.js и Simple Inheritance Джона Резига. Существуют более мощные API-интерфейсы, но они слишком сильно отличаются от собственно JavaScript для целей этого поста, где мы хотим, чтобы все было легко.
Традиционный JavaScript
Когда дело доходит до создания экземпляров в JavaScript, нужно различать специфичные для экземпляра вещи (свойства данных) и вещи, которые являются общими для всех экземпляров (методов). Учитывая функцию-
конструктор C, которая создает объекты, C.prototype — это то место, куда идут общие вещи, а сама функция C добавляет специфичные для экземпляра свойства. Это четкое разделение ответственности.
function Person(name) {
this.name = name;
}
Person.prototype.describe = function() {
return "Person called "+this.name;
}
function Worker(name, title) {
Person.call(this, name);
this.title = title;
}
Worker.prototype = Object.create(Person.prototype);
Worker.prototype.describe = function() {
return Person.prototype.describe.call(this)+" ("+this.title+")";
}
Взаимодействие:
> var john = new Person("John");
> john.describe()
Person called John
> var jane = new Worker("Jane", "CTO");
> jane.describe()
Person called Jane (CTO)
> jane instanceof Worker
true
> jane instanceof Person
true
Работник является вспомогательным конструктором Person. Для его создания нам необходимо решить следующие задачи:
- Worker.prototype должен быть объектом, прототип которого — Person.prototype. Мы решаем эту проблему с помощью объекта ECMAScript 5 Object.create () (который будет объяснен в следующем разделе). Часто используемым более традиционным решением является назначение Worker.prototype = new Person (), но затем Worker.prototype имеет переменные экземпляра Person, которые являются избыточными.
- Экземпляр Worker должен иметь переменные экземпляра Person, в дополнение к его собственным. Мы решаем это, позволяя конструктору Worker вызывать конструктор Person, но без оператора new. Таким образом, новый экземпляр не создается, но Person по-прежнему добавляет свои переменные экземпляра по мере необходимости.
- Вызов супер-методов. Для этого нам нужно напрямую обратиться к супер-методу (который является частью супер-прототипа).
- Включение instanceof: object instanceof ConstructorFunction работает, проверяя, находится ли ConstructorFunction.prototype в цепочке прототипов объекта. Таким образом, вместо jane instanceof Person мы могли бы написать Person.prototype.isPrototypeOf (jane). isPrototypeOf () является методом ECMAScript 5.
Обратите внимание, что у нас нет общего способа ссылки на супер-конструктор, мы всегда упоминаем Person напрямую.
ECMAScript 5
Чтобы понять подход ECMAScript, нам сначала нужно посмотреть, как ECMAScript обрабатывает свойства.
Характеристики недвижимости. Свойство в ECMAScript может иметь следующие характеристики:
- Есть три вида свойств:
- Имя свойство данных является нормальным свойством в объекте , где он связывает имя со значением.
- Имя аксессор свойство является свойством, доступ к которым осуществляются через геттер и способ инкубационного. Значение свойства может быть вычислено и, следовательно, нигде не сохранено.
- Внутреннее свойство управляется двигателем JavaScript и не могут быть доступны на всех через JavaScript, или только косвенно. Например, прототип объекта недоступен для записи во многих механизмах JavaScript и доступен только через Object.getPrototypeOf ().
- Атрибуты свойства: есть три логических флага, которые определяют, как работает свойство.
- доступный для записи: если true, значение свойства может быть изменено
- enumerable: если false, свойство скрыто в некоторых контекстах
- настраиваемый: если true, свойство может быть удалено, его атрибуты могут быть изменены, и оно может быть изменено со свойства данных на свойство доступа (или наоборот)
- Собственные и унаследованные: Собственные свойства непосредственно содержатся в этом, унаследованные свойства доступны через цепочку прототипов.
Дескрипторы свойств. ECMAScript использует
дескрипторы свойств для указания свойств, которые будут добавлены. Такой дескриптор может иметь следующие свойства:
- значение: назначить значение свойства данных
- get, set: назначить метод получения и установки свойства метода доступа.
- записываемый, перечисляемый, настраиваемый: установите соответствующие атрибуты свойства. По умолчанию все атрибуты имеют значение true.
Например, следующий код добавляет свойство только для чтения к объекту.
Object.defineProperty(obj, "prop", { writable: false, value: "abc" });
Установка прототипа объекта. Единственный способ назначить прототип объекта в ECMAScript — создать новый через Object.create (). Эта функция имеет следующую подпись.
Object.create(prototype, [propertyDescriptors]);
Второй аргумент является необязательным. Если опущен, создается пустой объект. Прототип объекта также называется
родительским объектом. Объект также называется
дочерним по прототипу.
Создание экземпляров с заводскими функциями. Мы используем не конструкторы для создания экземпляров, а обычные функции (не новые и не такие). Эти функции можно вызывать фабричными функциями и использовать Object.create () для создания объектов, имеющих общий прототип.
Наследование.Наследование должно заботиться о двух вещах: прототип и экземпляр. Супер-прототип расширяется путем добавления суб-прототипа. То есть супер-прототип становится родителем суб-прототипа. Таким образом, супер-прототип должен быть доступен с суб-фабрики. Обеспечение присутствия как супер-свойств, так и суб-свойств в экземпляре является более сложным. Наивной мыслью может быть сделать дескрипторы супер-свойств доступными для суб-фабрики, но тогда мы не можем легко инициализировать значения через параметры. Таким образом, мы моделируем традиционный подход, давая фабричной функции необязательный параметр self, к которому она должна добавлять свойства. self соответствует этому в традиционном JavaScript. Если self не определено, создается новый экземпляр супер-прототипа. Подфункция использует self для передачи экземпляра субпрототипа.
var PersonProto = {
describe: function() {
return "Person called "+this.name;
}
};
function createPerson(name, self) {
self = self || Object.create(PersonProto);
self.name = name;
return self;
}
// The sub-prototype extends the super-prototype
var WorkerProto = Object.create(PersonProto, {
describe: {
value: function() {
return PersonProto.describe.call(this)+" ("+this.title+")";
}
}
});
function createWorker(name, title) {
var self = createPerson(name, Object.create(WorkerProto));
self.title = title;
return self;
}
Взаимодействие:
> var john = createPerson("John");
> john.describe()
Person called John
> var jane = createWorker("Jane", "CTO");
> jane.describe()
Person called Jane (CTO)
> WorkerProto.isPrototypeOf(jane)
true
> PersonProto.isPrototypeOf(jane)
true
Мы не можем использовать instanceof. Если мы попробуем следующее.
jane instanceof WorkerProto
Мы получаем следующее исключение (в большинстве браузеров).
TypeError: Результат выражения ‘WorkerProto’ [[object object]] не является допустимым аргументом для instanceof.
Таким образом, мы используем isPrototypeOf ().
YUI
YUI().use("oop", function(Y) {
function Person(name) {
this.name = name;
}
Person.prototype.describe = function() {
return "Person called "+this.name;
}
function Worker(name, title) {
Worker.superclass.constructor.call(this, name);
this.title = title;
}
Y.extend(Worker, Person); // before adding methods
Worker.prototype.describe = function() {
return Worker.superclass.describe.call(this)+" ("+this.title+")";
}
var john = new Person("John");
console.log(john.describe());
var jane = new Worker("Jane", "CTO");
console.log(jane.describe());
console.log(jane instanceof Worker);
console.log(jane instanceof Person);
});
Консольный вывод:
Person called John
Person called Jane (CTO)
true
true
Комментарии:
- Этот API является абсолютным минимумом, необходимым для внедрения наследования в традиционный JavaScript.
- Он обрабатывает связывание прототипов и устанавливает свойство суперкласса функции конструктора, чтобы можно было обращаться к супер-конструктору в общем, без жестко заданного имени.
- Node.js имеет функцию sys.extends, которая похожа.
Дальнейшее чтение:
Prototype.js
var Person = Class.create({
initialize: function(name) {
this.name = name;
},
describe: function() {
return "Person called "+this.name;
}
});
var Worker = Class.create(Person, {
initialize: function($super, name, title) {
$super(name);
this.title = title;
},
describe: function($super) {
return $super()+" ("+this.title+")";
}
});
var john = new Person("John");
console.log(john.describe());
var jane = new Worker("Jane", "CTO");
console.log(jane.describe());
console.log(jane instanceof Worker);
console.log(jane instanceof Person);
Консольный вывод:
Person called John
Person called Jane (CTO)
true
true
Пояснения:
- Если у метода есть первый аргумент с именем $ super, Prototype передает метод super. Мне нравится четкость и простота этого подхода (аналогично методу переопределения в Java с помощью аннотации @Override). Однако нужно быть осторожным с минификацией, потому что она часто переименовывает аргументы функции для экономии места.
- Инициализация происходит в методе initialize (). Это разделяет конструкцию объекта и инициализацию объекта. Цепочка конструктора становится цепочкой метода с использованием стандартного механизма $ super.
- Не то чтобы вышеприведенный код не сильно отличался от традиционного JavaScript (который был одним из требований).
Дальнейшее чтение:
Простое наследство Джона Резига
Мотивация решения Ресига заключается в следующем. Свойство prototype функции конструктора может быть назначено как литерал объекта. Этот литерал объекта является наиболее близким к определению класса, которое в настоящее время имеет JavaScript. Все, что не хватает, это код инициализации из функции конструктора. Таким образом, API работает следующим образом: определите класс, передав прототип как литерал объекта. Сделайте код инициализации частью этого литерала, поместив его в метод init ().
var Person = Class.extend({
init: function(name) {
this.name = name;
},
describe: function() {
return "Person called "+this.name;
}
});
var Worker = Person.extend({
init: function(name, title) {
this._super(name);
this.title = title;
},
describe: function() {
return this._super()+" ("+this.title+")";
}
});
Взаимодействие:
> var john = new Person("John");
> john.describe()
Person called John
> var jane = new Worker("Jane", "CTO");
> jane.describe()
Person called Jane (CTO)
> jane instanceof Worker
true
> jane instanceof Person
true
Этот API мне нравится больше всего (снаружи). Вызывая метод, он делает его супер-метод доступным как this._super. Хотя я думаю, что параметр $ super в Prototype красивее, решение Resig не имеет проблем минимизации и может быть более эффективным (без привязки к этому, без добавления параметра). Таким образом, это лучший выбор.
- Вы не можете иметь объект, который может быть вызван через new (это могут делать только функции) и быть прототипом (с методами) для экземпляров. Это позор, потому что он мешает классам наследовать методы класса от своих суперклассов. Основным примером является метод extend, который необходимо добавить в каждый новый класс.
- Доступ к переопределенным методам: API временно добавляет к этому супер-метод под именем _super. Это автоматически позаботится о том, чтобы придать супер-методу правильное значение. Если рекурсивные вызовы методов работают, _super должен быть сохранен перед вызовом метода, а затем восстановлен. API позаботится об этом прозрачно.
Ресурсы:
- Джон Резиг — Простое наследование JavaScript . Объясняет, как используется API и как он был реализован.
- class.js , моя реализация API Simple Inheritance, которая, возможно, немного проще для понимания, чем код Ресига.
- Предупреждение: в коде используется API ECMAScript 5, что означает, что вам понадобится шайба в старых браузерах.
Понимание кода Ресига:
- Свойство prototype функции конструктора должно содержать объект, прототип которого является свойством prototype супер-конструктора. Это достигается с помощью кода, позволяя супер-конструктору производить как в экземпляре, но в специальном режиме, когда он не добавляет переменные экземпляра или иным образом инициализирует экземпляр. Моя реализация использует Object.create () для этой цели, устраняя необходимость в специальном режиме.
- Глобальный класс создается внутри функции, не связанной с методом, путем присвоения this.Class. Таким образом, он использует причуду JavaScript, где это глобальный объект в не-методах. Это не разрешено в строгом режиме .
- Регулярное выражение xyz проверяет, можно ли искать исходный код функции по имени свойства _super. Потенциальными препятствиями являются механизмы минификации и JavaScript, которые не возвращают исходный код функции, когда она приводится к строке.
- Также несовместим со строгим режимом : доступ к arguments.callee.
дальнейшее чтение
- Простой способ понять прототип наследования JavaScript
- Идет полностью прототип в JavaScript . Показывает API, который принимает чисто прототипный подход к наследованию.
- traits.js : Приносит черту программной конструкции в JavaScript в стиле, который дополняет Object.create () в ECMAScript 5.