Классическое наследование в JavaScript. Разбор реализации в Babel, BackboneJS и Ember
- пятница, 20 апреля 2018 г. в 00:22:13
Лучшим советом по применению наследования будет использовать его в том виде, в каком оно описано в EcmaScript 6, с ключевыми словами class, extends, constructor, и т.д. Если у вас есть такая возможность,Рассмотрим некоторые популярные примеры реализации классического наследования.то можете не читать дальшето это лучший вариант с точки зрения читаемости кода и производительности. Всё последующее описание будет полезным для случая использования старой спецификации, когда проект уже начат с использованием ES5 и переход на новую версию не представляется доступным.
instanceof
и т.п.class BasicClass {
static staticMethod() {}
constructor(x) {
this.x = x;
}
someMethod() {}
}
class DerivedClass extends BasicClass {
static staticMethod() {}
constructor(x) {
super(x);
}
someMethod() {
super.someMethod();
}
}
_inherits
:function _inherits(subClass, superClass) {
if (typeof superClass !== "function" && superClass !== null) {
throw new TypeError("Super expression must either be null or a function, not " + typeof superClass);
}
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: {
value: subClass,
enumerable: false,
writable: true,
configurable: true
}
});
if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
}
subClass.prototype = Object.create(superClass.prototype);
prototype
конструктора subClass
указывает на новый объект, прототипом которого является prototype
родительского класса superclass
. Таким образом, это простое прототипное наследование, замаскированное под классическое в исходном коде.Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
setPrototypeOf
Babel предусматривает прямую запись прототипа в скрытое свойство __proto__
– техника не рекомендуемая, но подходящая на крайний случай при использовании старых браузеров._inherits
простым копированием ссылок в конструктор или его prototype
. При написании собственной реализации наследования можно использовать данный пример как основу и добавить в него объекты с динамическими и статическими полями в качестве дополнительных аргументов функции _inherits
.return _possibleConstructorReturn(this, (DerivedClass.__proto__ || Object.getPrototypeOf(DerivedClass)).call(this, x));
this
._super
в конструктор и его prototype
, чтобы иметь удобную ссылку на родительский класс, например:function extend(subClass, superClass) {
// ...
subClass._super = superClass;
subClass.prototype._super = superClass.prototype;
}
extend
для расширения классов библиотеки: Model, View, Collection, и т.д. При желании её можно позаимствовать и для собственных целей. Ниже приведён код функции extend
из версии Backbone 1.3.3.var extend = function(protoProps, staticProps) {
var parent = this;
var child;
// The constructor function for the new subclass is either defined by you
// (the "constructor" property in your `extend` definition), or defaulted
// by us to simply call the parent constructor.
if (protoProps && _.has(protoProps, 'constructor')) {
child = protoProps.constructor;
} else {
child = function(){ return parent.apply(this, arguments); };
}
// Add static properties to the constructor function, if supplied.
_.extend(child, parent, staticProps);
// Set the prototype chain to inherit from `parent`, without calling
// `parent`'s constructor function and add the prototype properties.
child.prototype = _.create(parent.prototype, protoProps);
child.prototype.constructor = child;
// Set a convenience property in case the parent's prototype is needed
// later.
child.__super__ = parent.prototype;
return child;
};
var MyModel = Backbone.Model.extend({
constructor: function() {
// ваш конструктор класса; его использование опционально,
// но если используете, нужно обязательно вызвать родительский конструктор
Backbone.Model.apply(this, arguments);
},
toJSON: function() {
// метод переопределён, но можно вызвать родительский через «__super__»
MyModel.__super__.toJSON.apply(this, arguments);
}
}, {
staticMethod: function() {}
});
child.prototype = _.create(parent.prototype, protoProps);
_.create()
– аналог Object.create()
из ES6, реализованный библиотекой Underscore JS. Её второй аргумент позволяет сразу записать в прототип свойства и методы protoProps
, переданные при вызове функции extend
._.extend(child, parent, staticProps);
initialize
, который вызывается автоматически изнутри родительского конструктора.this
. Без этого такой вызов привёл бы к зацикливанию в случае многоуровневой цепочки наследования. Метод суперкласса, имя которого, как правило, известно в текущем контексте, может быть вызван и напрямую, так что данное ключевое слово является лишь сокращением для:Backbone.Model.prototype.toJSON.apply(this, arguments);
extend
– «child». Этот недостаток может показаться несущественным, пока не столкнёшься с ним на практике при отладке цепочки классов, когда становится тяжело понять, экземпляром какого класса является данный объект и от какого класса он наследуется:constructor
является enumerable, т.е. перечисляемым при обходе экземпляра класса в цикле «for-in». Несущественно, однако Babel позаботился и об этом, объявляя конструктор с перечислением необходимых модификаторов.inherits
, реализованную Babel, так и свою собственную реализацию extend
– очень сложную и навороченную, с поддержкой миксинов и прочего. На приведение кода этой функции в данной статье просто не хватит места, что уже ставит под сомнение её производительность при использовании для собственных нужд вне фреймворка.var MyClass = MySuperClass.extend({
myMethod: function (x) {
this._super(x);
}
});
this._super(x)
) мы не указываем название метода. И никаких преобразований кода при компиляции не происходит._super
без преобразования кода? Всё дело в сложной работе с классами и в хитрой функции _wrap
, код которой приведён далее:function _wrap(func, superFunc) {
function superWrapper() {
var orig = this._super;
this._super = superFunc; // <--- магия здесь
var ret = func.apply(this, arguments);
this._super = orig;
return ret;
}
// здесь опущена нерелевантная часть кода
return superWrapper;
}
superWrapper
. _super
записывается указатель на родительский метод, соответствующий по названию вызываемому методу (работа по определению соответствий произошла ещё на этапе создания класса при вызове extend
). Далее вызывается оригинальная функция, изнутри которой можно обращаться к _super
как к родительскому методу. Затем свойству _super
присваивается исходное значение, что позволяет использовать его в глубоких цепочках вызовов._super
в нём, оборачивается в отдельную функцию. Поэтому при глубокой цепочке вызовов методов одного класса произойдёт разрастание стека. Особенно это критично для методов, вызываемых регулярно в цикле или при отрисовке пользовательского интерфейса. Поэтому можно сказать, что данная реализация является чересчур громоздкой и не оправдывает полученного преимущества в виде сокращённой формы записи.function BaseClass() {
this.x = this.initializeX();
this.runSomeBulkyCode();
}
// ...объявление методов BasicClass в прототипе...
function SubClass() {
BaseClass.apply(this, arguments);
this.y = this.initializeY();
}
// собственно наследование
SubClass.prototype = new BaseClass();
SubClass.prototype.constructor = SubClass;
// ...объявление методов SubClass в прототипе...
new SubClass(); // создание экземпдяра
prototype
создаётся экземпляр родительского класса, вызывается его конструктор, что приводит к лишним действиям, особенно если конструктор производит большую работу при создании объекта (runSomeBulkyCode). Так делать нельзя:SubClass.prototype = new BaseClass();
this.x
), записываются не в новый экземпляр, а в прототип всех экземпляров класса SubClass. Кроме того, тот же конструктор BaseClass вызывается затем повторно из конструктора подкласса. В случае, если родительский конструктор требует при вызове некоторые параметры, такую ошибку допустить тяжело, а вот при их отсутсвии – вполне возможно.SubClass.prototype = Object.create(BasicClass.prototype);
Babel | Backbone JS | Ember JS | |
---|---|---|---|
Память | Равнозначно | ||
Производительность | Высшая | Средняя | Низшая |
Статические поля | + (только в ES6)* | + | — (за исключением внутреннего использования наследования из Babel) |
Ссылка на суперкласс | super.methodName() (только в ES6) |
Constructor.__super__.prototype |
this._super() |
Косметические детали | Идеально с ES6; требует доработки в собственной реализации под ES5 |
Удобство объявления; проблемы при отладке | Зависит от способа наследования; те же проблемы с отладкой, что и в Backbone |
extend
на базе примера из компилятора Babel с учётом приведённых замечаний и дополнений из других примеров. Так наследование может быть реализовано наиболее гибким и подходящим под данный проект образом.