Статьи

Облегченные API наследования JavaScript


Создание объектов с помощью функций конструктора довольно просто в 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 позаботится об этом прозрачно.

Ресурсы:

  1. Джон Резиг — Простое наследование JavaScript . Объясняет, как используется API и как он был реализован.
  2. class.js , моя реализация API Simple Inheritance, которая, возможно, немного проще для понимания, чем код Ресига.

    • Предупреждение: в коде используется API ECMAScript 5, что означает, что вам понадобится шайба в старых браузерах.

Понимание кода Ресига:

  • Свойство prototype функции конструктора должно содержать объект, прототип которого является свойством prototype супер-конструктора. Это достигается с помощью кода, позволяя супер-конструктору производить как в экземпляре, но в специальном режиме, когда он не добавляет переменные экземпляра или иным образом инициализирует экземпляр. Моя реализация использует Object.create () для этой цели, устраняя необходимость в специальном режиме.
  • Глобальный класс создается внутри функции, не связанной с методом, путем присвоения this.Class. Таким образом, он использует причуду JavaScript, где это глобальный объект в не-методах. Это не разрешено в строгом режиме .
  • Регулярное выражение xyz проверяет, можно ли искать исходный код функции по имени свойства _super. Потенциальными препятствиями являются механизмы минификации и JavaScript, которые не возвращают исходный код функции, когда она приводится к строке.
  • Также несовместим со строгим режимом : доступ к arguments.callee.

дальнейшее чтение