-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
Description
While class emulation is widespread nowadays, prototypes are JavaScript's true vehicle of inheritance. The following snippets of code, which contain equivalent pairs, demonstrate that prototype-based programming is more fundamental and explicit:
// define constructor and prototype
// class emulation version
class BaseConstructor {}
const basePrototype = BaseConstructor.prototype;
// prototype version
const basePrototype = {
constructor() {}
};
const BaseConstructor = basePrototype.constructor;
BaseConstructor.prototype = basePrototype;// inheritance
// class emulation version
class ChildConstructor extends BaseConstructor {}
const childPrototype = ChildConstructor.prototype;
// prototype version
const childPrototype = Object.create(basePrototype, {
constructor() { return basePrototype.constructor.apply(this, arguments); }
});
const ChildConstructor = childPrototype.constructor;
ChildConstructor.prototype = childPrototype;// instantiation
// class emulation version
const baseInstance = new BaseConstructor();
// prototype version
let baseInstance = Object.create(basePrototype);
baseInstance = baseInstance.constructor() || baseInstance;Since prototypes are so fundamental, I believe there is space in Underscore for utility functions that make prototype-based programming more convenient. In draft, I propose the following. A real implementation would need more sophistication for ES3 compatibility, performance and possibly corner cases.
// get the prototype of any object
function prototype(obj) {
return Object.getPrototypeOf(obj);
}
// mixin for prototypes that lets you replace
// var instance = new BaseConstructor()
// by
// var instance = create(basePrototype).init()
// Of course, prototypes can also skip the constructor and directly define their
// own .init method instead.
var initMixin = {
init() {
var ctor = this.constructor;
return ctor && ctor.apply(this, arguments) || this;
}
};
// mixin for prototypes so you can replace
// var instance = create(prototype).init()
// by
// var instance = prototype.construct()
// Of course, prototypes can also directly define their own .construct method
// instead.
var constructMixin = extend({
construct() {
return this.init.apply(create(this), arguments);
}
}, initMixin);
// standalone version of the construct method, construct(prototype, ...args)
var construct = restArguments(function(prototype, args) {
return (prototype.construct || constructMixin.construct).apply(prototype, args);
});
// inheriting constructor creation for class emulation interop
function wrapConstructor(base, derived) {
return extend(
derived && has(derived, 'constructor') && derived.constructor ||
base && base.contructor && function() {
return base.constructor.apply(this, arguments);
} || function() {},
// The following line copies "static properties" from the base
// constructor. This is useless in prototype-based programming, but
// might be important for class-emulated code.
base && (base.constructor || null),
{ prototype: derived }
);
}
// mixin for prototypes with a constructor so you can replace
// class ChildConstructor extends BaseConstructor {}
// by
// var childPrototype = basePrototype.extend({})
// Of course, prototypes can also directly define their own .extend method.
var extendMixin = {
extend() {
var derived = create.apply(this, arguments);
// note: using the pre-existing standalone _.extend below
return extend(derived, {constructor: wrapConstructor(this, derived)});
}
};
// standalone version of the extendMixin.extend method, named differently in
// order to avoid clashing with the pre-existing _.extend. inherit also seems a
// more appropriate name for this function when used standalone.
var inherit = restArguments(function(base, props) {
return (base.extend || extendMixin.extend).apply(base, props);
});
// collection of mixins for quick and easy interoperability with
// constructor-based code
var prototypeMixins = {
init: initMixin,
construct: constructMixin,
extend: extendMixin,
all: extend({}, constructMixin, extendMixin)
};Note how construct and .extend/inherit are both based on create. This is no coincidence; in prototype-based programming, there is no fundamental distinction between prototypes and instances. Every object can have a prototype and be a prototype at the same time. From this point of view, .extend/inherit is just a special variant of construct that enables interoperability with class-emulated code. Without any class-emulated legacy, prototype, create and .init/construct would already cover all needs.
With the above utilities in place, we can revisit our examples from the beginning and find that the prototype-centric code is just as concise as the class-centric code:
// define constructor/prototype
// class-centric
class BaseConstructor {}
// prototype-centric
const basePrototype = {};// inheritance
// class-centric
class ChildConstructor extends BaseConstructor {}
// prototype-centric
const childPrototype = inherit(basePrototype, {});// instantiation
// class-centric
const baseInstance = new BaseConstructor();
// prototype-centric
const baseInstance = construct(basePrototype);Related: jashkenas/backbone#4245.