Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Utility functions for prototype-based programming #2947

Open
jgonggrijp opened this issue Dec 21, 2021 · 1 comment
Open

Utility functions for prototype-based programming #2947

jgonggrijp opened this issue Dec 21, 2021 · 1 comment

Comments

@jgonggrijp
Copy link
Collaborator

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.

@jashkenas
Copy link
Owner

I fairly strongly feel like Underscore shouldn't try to introduce a new system for doing OOP-in-JS, elaborated on a bit here: jashkenas/backbone#4245 (comment)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants