Этот пост в блоге иллюстрирует несколько тем наследования JavaScript на примере: мы начнем с наивных реализаций конструктора Point и его подконструктора ColorPoint, а затем улучшим их, шаг за шагом. Этот пост должен быть понятен, но там, где это не так, вы можете ознакомиться с более подробным введением в наследование JavaScript в [1] .
Конструкторы
Конструкторы — это фабрики для объектов (аналогично классам в языках классов). Каждая функция foo может быть вызвана двумя способами:
- как функция: foo (arg1, arg2)
- как конструктор: new foo (arg, arg2)
Следующая функция является конструктором для точек:
function Point(x, y) { this.x = x; this.y = y; this.dist = function () { return Math.sqrt((this.x*this.x)+(this.y*this.y)); }; this.toString = function () { return "("+this.x+", "+this.y+")"; }; }
Когда мы выполняем new Point (), задача конструктора состоит в том, чтобы установить свежий объект, переданный ему через неявный параметр this. Point () добавляет четыре свойства к этому объекту. Свойства dist и toString называются
методами , потому что их значения являются функциями. Объект возвращается конструктором и считается его экземпляром:
> var p = new Point(3, 1); > p instanceof Point true
Вы можете вызвать методы и получить доступ к свойствам не-метода:
> p.dist() 3.1622776601683795 > p.toString() '(3, 1)' > p.x 3 > p.y 1
Методы не должны быть в каждом экземпляре, они должны быть разделены между экземплярами, чтобы сохранить память. Для этой цели вы можете использовать
прототип : объект, сохраненный в Point.prototype, становится прототипом экземпляров Point. Свойства объекта являются общими для всех объектов, прототип которых он составляет, поэтому Point.prototype — это место, где вы размещаете методы:
function Point(x, y) { this.x = x; this.y = y; } Point.prototype = { dist: function () { return Math.sqrt((this.x*this.x)+(this.y*this.y)); }, toString: function () { return "("+this.x+", "+this.y+")"; } }
Мы присваиваем объект Point.prototype через
литерал объекта, который имеет два свойства dist и toString. Теперь существует четкое разделение ответственности: конструктор отвечает за настройку данных конкретного экземпляра, прототип содержит общие данные (т. Е. Методы). Обратите внимание, что прототипы высоко оптимизированы в механизмах JavaScript, поэтому обычно нет никакого снижения производительности для размещения методов там. Методы вызываются так же, как и раньше, вы не замечаете, хранятся ли они в экземпляре или в прототипе. Остается одна проблема: для каждой функции f должно выполняться следующее уравнение
[2] :
f.prototype.constructor === f
Каждая функция настроена так по умолчанию. Но мы заменили значение по умолчанию Point.prototype. Чтобы удовлетворить уравнению, мы можем либо добавить конструктор свойства к литералу объекта выше, либо сохранить значение по умолчанию, не заменяя его, добавив в него методы:
function Point(x, y) { this.x = x; this.y = y; } Point.prototype.dist = function () { return Math.sqrt((this.x*this.x)+(this.y*this.y)); }; Point.prototype.toString = function () { return "("+this.x+", "+this.y+")"; };
Свойство конструктора не так важно; в основном это позволяет вам определить, какой конструктор создал данный экземпляр:
> var p = new Point(2, 2) > p.constructor [Function: Point] > p.constructor.name 'Point'
простирающийся
В JavaScript термин
расширение объекта означает деструктивное добавление к нему новых свойств: чтобы расширить объект A с помощью объекта B, мы (поверхностно) копируем свойства B в A. Несколько необычное определение этого термина в JavaScript происходит из-за инфраструктуры Prototype, который имеет метод
Object.extend () . Следующее является наивной реализацией:
function extend(target, source) { for (var propName in source) { target[propName] = source[propName]; } return target; }
Проблема с этим кодом заключается в том, что for-in выполняет итерацию по всем свойствам объекта, в том числе наследуемым от прототипа. Это можно увидеть здесь:
> extend({}, new Point()) { x: undefined, y: undefined, dist: [Function], toString: [Function] }
Нам нужны «собственные» (прямые) свойства x и y экземпляра Point. Но нам не нужны его унаследованные свойства dist и toString. Почему унаследованные свойства копируются в первый аргумент? Потому что for-in видит все свойства объекта, в том числе наследуемые. Point наследует несколько свойств от Object, например valueOf:
> var p = new Point(7, 1); > p.valueOf [Function: valueOf]
Эти свойства не копируются, потому что for-in может видеть только
перечислимые свойства
[3] и они не перечислимы:
> p.propertyIsEnumerable("valueOf") false > p.propertyIsEnumerable("dist") true
Чтобы исправить extension (), мы должны убедиться, что учитываются только собственные свойства источника.
function extend(target, source) { for (var propName in source) { // Is propName an own property of source? if (source.hasOwnProperty(propName)) { target[propName] = source[propName]; } } return target; }
Есть еще одна проблема: приведенный выше код завершается ошибкой, если у источника есть собственное свойство с именем hasOwnProperty
[4] :
> extend({}, { hasOwnProperty: 123 }) TypeError: Property 'hasOwnProperty' is not a function
Ошибка вызвана тем, что source.hasOwnProperty (строка 4) получает доступ к собственному свойству (число) вместо унаследованного метода. Мы можем решить эту проблему, ссылаясь на этот метод напрямую, а не через источник:
function extend(target, source) { var hasOwnProperty = Object.prototype.hasOwnProperty; for (var propName in source) { // Invoke hasOwnProperty() with this = source if (hasOwnProperty.call(source, propName)) { target[propName] = source[propName]; } } return target; }
На движках ECMAScript 5 (или на более старых движках, в которых загружен shim
[5] ) лучше использовать следующую версию extend (), поскольку она сохраняет атрибуты свойств,
такие как перечисляемость:
function extend(target, source) { Object.getOwnPropertyNames(source) .forEach(function(propName) { Object.defineProperty(target, propName, Object.getOwnPropertyDescriptor(source, propName)); }); return target; }
Установка прототипа объекта
До сих пор мы видели, как добавить свойства к объекту разрушительным образом. Мы также видели, что прототипы делают то же самое, но не разрушительно: его свойства «проявляются» в экземпляре, но не относятся к его собственным свойствам. Было бы неплохо, если бы мы могли более непосредственно влиять на этот тип наследования и устанавливать прототип объекта без конструктора. Учитывая, что прототип объекта является такой фундаментальной, сильно оптимизированной функцией, единственный стандартный способ сделать это — создать новый объект. То есть вы можете установить прототип объекта только один раз при его создании. Следующий код использует ECMAScript 5 Object.create () для создания нового объекта, прототипом которого является объект proto.
var proto = { bla: true }; var obj = Object.create(proto); obj.foo = 123; obj.bar = "abc";
У obj есть как унаследованные, так и собственные свойства:
> obj.bla true > obj.foo 123
Шим ECMAScript 5 использует код, подобный следующему, чтобы сделать Object.create доступным в старых браузерах.
if (Object.create === undefined) { Object.create = function (proto) { function Tmp() {} Tmp.prototype = proto; // New empty object whose prototype is proto return new Tmp(); }; }
Приведенный выше код использует временный конструктор для создания одного экземпляра с заданным прототипом. До сих пор мы игнорировали необязательный второй параметр Object.create (), который позволяет вам определять свойства вновь создаваемого объекта:
var obj = Object.create(proto, { foo: { value: 123 }, bar: { value: "abc" } });
Свойства определяются через
дескрипторы свойств . С помощью дескриптора вы можете указать атрибуты свойств, такие как перечисляемость, а не только значения. В качестве упражнения давайте реализуем protoChain (), упрощенную версию Object.create (). Это позволяет избежать сложностей дескрипторов свойств и просто расширяет новый объект вторым параметром. Например:
var obj = protoChain(proto, { foo: 123, bar: "abc" });
Мы можем обобщить приведенную выше идею на произвольное количество параметров:
protoChain(obj_0, obj_1, ..., obj_n-1, obj_n)
Помните, что мы должны создавать свежие объекты, чтобы назначать прототипы. Следовательно, protoChain () возвращает поверхностную копию obj_n, прототип которой представляет собой поверхностную копию obj_n-1 и т. Д. Obj_0 — единственный объект в возвращаемой цепочке, который не был продублирован. protoChain () может быть реализован так:
function protoChain() { if (arguments.length === 0) return null; var prev = arguments[0]; for(var i=1; i < arguments.length; i++) { // Create duplicate of arguments[i] with prototype prev prev = Object.create(prev); extend(prev, arguments[i]); } return prev; }
подтипировании
Идея создания подтипов заключается в создании нового конструктора, основанного на существующем. Новый конструктор называется суб-конструктором, а существующий — супер-конструктором. Ниже приведен суб-конструктор Point:
function ColorPoint(x, y, color) { Point.call(this, x, y); this.color = color; }
Приведенный выше код устанавливает свойства экземпляра x, y и цвет. Это делается путем передачи this (экземпляра ColorPoint) в Point: Point вызывается как функция, но метод call () позволяет нам сохранить this для ColorPoint. Поэтому Point () добавляет x и y для нас, а мы сами добавляем цвет. Нам все еще нужно позаботиться о методах: с одной стороны, мы хотим наследовать методы Point, с другой стороны, мы хотим определить наши собственные методы. Это простой способ сделать это с помощью extend ():
extend(ColorPoint.prototype, Point.prototype); ColorPoint.prototype.toString = function () { return this.color+" "+Point.prototype.toString.call(this); };
Сначала мы копируем методы из Point.prototype в ColorPoint.prototype, а затем добавляем наш собственный метод: мы заменяем toString () в Point версией, чей результат объединяет цвет с выходными данными Point.prototype.toString (). Мы напрямую ссылаемся на последний метод и вызываем его с помощью ColorPoint. Для получения дополнительной информации о вызовах методов супер-прототипа обратитесь к
[6] . ColorPoint работает как положено:
> var cp = new ColorPoint(5, 3, "red"); > cp.toString() 'red (5, 3)'
В качестве улучшения мы можем избежать добавления избыточных свойств в ColorPoint.prototype, сделав Point.prototype его прототипом.
ColorPoint.prototype = Object.create(Point.prototype); ColorPoint.prototype.constructor = ColorPoint; ColorPoint.prototype.toString = function () { return this.color+" "+Point.prototype.toString.call(this); };
В строке 1 мы заменили значение по умолчанию ColorPoint.prototype и, таким образом, должны установить свойство конструктора в строке 2. Хотя написание одного конструктора довольно просто, приведенный выше код слишком сложен для выполнения вручную. Вспомогательная функция наследует () сделает это проще:
ColorPoint.prototype.toString = function () { return this.color+" "+Point.prototype.toString.call(this); }; inherits(ColorPoint, Point);
Функция наследует () смоделирован Node.js в
util.inherits () . Это дает вам подтип при сохранении простоты обычных конструкторов. Требования:
- Не должно иметь значения, вызываем ли мы методИС () до или после добавления методов в прототип.
- Методоризмы () должен гарантировать, что свойство конструктора установлено правильно.
Это реализация:
function inherits(SubC, SuperC) { var subProto = Object.create(SuperC.prototype); // At the very least, we keep the "constructor" property // At most, we keep additions that have already been made extend(subProto, SubC.prototype); SubC.prototype = subProto; };
Есть еще одна вещь, которую мы можем улучшить: ColorPoint.prototype.toString () делает следующий вызов.
Point.prototype.toString.call(this)
Это не идеально, потому что мы жестко закодировали супер-конструктор. Вместо этого мы бы предпочли использовать:
ColorPoint._super.toString.call(this)
Для того, чтобы сделать приведенный выше код возможным, у функцииоля (только) нужно сделать следующее присваивание:
SubC._super = SuperC.prototype;
Здесь мы расходимся с Node.js, где SubC.super_ относится к SuperC. Конструктор ColorPoint по-прежнему содержит жестко заданную ссылку на Point. Это можно устранить, заменив Point.call (…) на
ColorPoint._super.constructor.call(this, x, y);
Не совсем красиво, но это делает работу. Наша окончательная версия ColorPoint выглядит следующим образом:
function ColorPoint(x, y, color) { ColorPoint._super.constructor.call(this, x, y); this.color = color; } ColorPoint.prototype.toString = function () { return this.color+" "+ColorPoint._super.toString.call(this); }; inherits(ColorPoint, Point);
Вывод
На нашем работающем примере мы увидели, как реализовать конструкторы, как расширить объекты, как установить прототип объекта и как наследовать конструктор. Мы не рассматривали, как сохранить конфиденциальность данных — это тема для другого поста в блоге.
Рекомендации
- Прототипы как классы — введение в наследование JavaScript
- Что случилось со свойством «конструктор» в JavaScript?
- Свойства JavaScript: наследование и перечислимость
- Подводные камни использования объектов в качестве карт в JavaScript
- es5-shim: используйте ECMAScript 5 в старых браузерах
- Пристальный взгляд на супер-ссылки в JavaScript и ECMAScript.next
С http://www.2ality.com/2012/01/js-inheritance-by-example.html