jashkenas / jashkenas/underscore

Utility functions for prototype-based programming

Open
#2,947 1 comment 0 reactions 0 assignees View on GitHub
enhancement
Dominant language
JavaScript
Stars
27.3k
Forks
5.4k
Avg merge
2d 5h
Merged PRs (30d)
1

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:

```js
// 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;
```

```js
// 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;
```

```js
// 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.

```js
// 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:

```js
// define constructor/prototype

// class-centric
class BaseConstructor {}

// prototype-centric
const basePrototype = {};
```

```js
// inheritance

// class-centric
class ChildConstructor extends BaseConstructor {}

// prototype-centric
const childPrototype = inherit(basePrototype, {});
```

```js
// instantiation

// class-centric
const baseInstance = new BaseConstructor();

// prototype-centric
const baseInstance = construct(basePrototype);
```

Related: https://github.com/jashkenas/backbone/issues/4245.

Contributor guide

Open the contributing guide

Research direction

Start by reviewing the existing create, extend, and restArguments utilities referenced in the proposal, then compare the suggested prototype, init, construct, inherit, and mixin APIs with the related Backbone issue. Before implementation, the scope, ES3 compatibility, performance expectations, and corner cases need agreement; done would require an accepted API and corresponding implementation plan.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript
Domain
tooling
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.