Этот пост в блоге иллюстрирует несколько тем наследования 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