-
-
Notifications
You must be signed in to change notification settings - Fork 5.4k
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
Embrace prototypes #4245
Comments
It's a neat idea, but I strongly feel like modern JavaScript should be standardizing on the class User extends Backbone.Model { Although I agree that ES6 classes could have been designed better than they were, I don't agree that prototypes are more fundamental — I think that prototypes have always been a (minor) mistake. The basic argument is this: When you're working with Objects, you have some objects that tend to serve as blueprints, and other objects that are formed in the shape of those blueprints to hold real data and do real work. It almost never makes sense to start with a real object-containing-data and then use that to blueprint new objects — you pretty much always start with a blank state. So we have:
I think that recognizing the fact that these aren't the same types of objects — that one is always a blueprint, and never holds data directly, and the other is an instantiation of that blueprint — is a good thing. Ultimately, classes and prototypes become the same pattern, but classes are clearer about the fact that you have two different roles these objects are serving, and are clearer about which role belongs to which. JavaScript screwed this up with a funky scheme where a prototype was tacked on as a property on a constructor, so that you actually had three objects involved in this dance, but the basic pattern of use has always been essentially classical. e.g. So let's not muddle matters further, just use |
I agree that there is a distinction to be made in any case, between the blueprint and the instance. For the record, this is why I was proposing separate
No, JavaScript is really the odd one out in this regard. There are relatively few prototype-based languages to begin with, with probably Lua being the currently most popular after JavaScript and Self being a historical name that might ring a bell with some people. JavaScript is the only one that pretends to have classes, since recently. wiki-en:Lua§Object-oriented programming has a code example that follows a pattern very similar to what I was proposing in the opening post. wiki-en:Prototype-based programming§Criticism articulates your main points of recognizability and safety, and also mentions JavaScript as a special case that added class-like syntactic sugar. Regarding recognizability, I must point out that var user = model.extend({
idAttribute: '_id',
// ...
}); looks extremely similar to var User = Model.extend({
idAttribute: '_id',
// ...
}); which has been in the Backbone documentation for the past 11 years. There is still a large crowd of faithful Backbone fans, who seem to take no offense from this pattern. Contrast this with class User extends Model {
// only methods here
}
_.extend(User.prototype, {
idAttribute: '_id',
// other properties here, which you'd generally prefer to place above the methods
}); which is what you end up with if you try to use current Backbone with ES6 classes. This Standardization, sure. I'm not married to the prototype proposal, but I believe we need a solution for the conflict between ES6 classes and current Backbone. I can think of a few options:
If you can think of another option, please share it. |
In my previous comment, I forgot to mention decorators. This is an interesting idea, which could theoretically allow syntax like the following: @props({
idAttribute: '_id',
// more prototype properties
})
class User extends Model {
// methods
}
@props({
tagName: 'button',
})
class SubmitButton extends View {
@on('click')
submit() { /*...*/ }
} This is much better than the
I was aware of decorators when I started the large Backbone+TypeScript project that I mentioned in the previous comment, but ended up not using them because of the above reasons. Relevant: #3560, https://benmccormick.org/2015/07/06/backbone-and-es6-classes-revisited. |
I'm just freestyling here, so forgive any holes or omissions, but — I think a promising direction for a 2.0 would be to adapt Backbone’s current structure to the limitations of class User extends Backbone.Model {
idAttribute = '_id'
signup() { ... }
}
class SubmitButton extends Backbone.View {
tagName = 'button'
submit() { ... }
} |
Unfortunately - and this is my real gripe with ES6 classes - that syntax desugars to this: function User(attributes, options) {
Backbone.Model.call(this, attributes, options);
this.idAttribute = '_id';
}
User.prototype.signup = function() { ... };
function SubmitButton(options) {
Backbone.View.call(this, options);
this.tagName = 'button';
}
SubmitButton.prototype.submit = function() { ... }; Which means that |
For completeness I'll mention a few more options:
const userMeta = {
idAttribute: '_id',
};
class User extends Backbone.Model {
signup() { ... }
}
_.extend(User.prototype, userMeta); Side note: after writing my earlier comment about decorator notation, I realized that the old const SubmitButton = Backbone.View.extend({
tagName: 'button',
submit: on('click', function() { ... }),
}); It just takes some administration, where the |
FYI in my fork, idAttribute and cidPrefix are read from a static class field, i.e., a constructor property https://github.com/blikblum/nextbone/blob/master/nextbone.js#L549 |
@blikblum To me, that feels very much like a workaround, but I'll admit it is a clever one. |
defaults and Collection model can also be defined as static fields as well as methods
IMO this is the closest to get using ES classes. The DX is pretty good. See example below: import { Model, Collection } from 'nextbone'
class Task extends Model {
static defaults = {
title: '',
done: false
}
}
class Tasks extends Collection {
static model = Task
}
const tasks = new Tasks()
tasks.fetch() It can be written as: import { Model, Collection } from 'nextbone'
class Task extends Model {
defaults() {
return {
title: '',
done: false
}
}
}
const tasks = new Collection({model: Task})
tasks.fetch() |
@blikblum’s |
@blikblum's workaround pivots on this helper function, which replaces the current role of While it looks reasonable at first sight, it effectively means that anything that needs to be accessible through import { Model, Collection } from 'nextbone'
class Task extends Model {
defaults() {
return {
title: '',
done: false
}
}
}
class PrioritizedTask extends Task {
static defaults = {
title: '',
priority: 1,
done: false
};
}
(new PrioritizedTask()).get('priority'); // undefined
class Task extends Model {
static defaults() {
return {
title: '',
done: false
}
}
}
class PrioritizedTask extends Task {
static defaults = {
title: '',
priority: 1,
done: false
};
}
(new PrioritizedTask()).get('priority'); // 1 There are many problems with this:
I don't want to go there. So far, we have collected many non-ideal solutions. I think that the following requirements summarize a solution that we could all agree to be ideal:
Going over all the solutions that have passed again, it is easy to identify why none of them was entirely satisfactory:
Rather than sacrificing one of the requirements, I suggest aiming high and just letting this sit for the time being. I think we can afford, and hope it will pay off, to wait until somebody comes up with an ideal solution (or decorators become viable). In the meanwhile, I would still like to experiment with the prototype-centric approach. I'll implement it in a plugin instead of in Backbone itself. |
Sounds good! Thanks for digging in deeper... |
The ES6 class emulation convention doesn't sit well with Backbone, mostly because it provides no convenient way to set non-function prototype properties. In fact, in my opinion, the ES6 class emulation convention doesn't sit well in general for this same reason. On top of that, classes don't sit well with JavaScript, anyway. I'm not the first to say this; consider Walker 2014 and Crockford 2008. I consider #4079 a symptom of classes not sitting well.
Therefore, in Backbone version 2, rather than adapting the library to ES6 classes, I would like to do away with classes entirely and embrace prototypes instead. That would mean that instead of the following in Backbone 1,
we would be writing something like the following in Backbone 2:
where
model
is an object that serves as a prototype, instead of a function that emulates a class.model.extend(protoprops)
(andcollection.extend
, etcetera) would default to just being a shorthand forObject.create(model, protoprops)
. (I would likely use_.create
instead ofObject.create
, but that is an implementation detail.) This method can still be overridden by plugins in order to enable things like shorthand syntax at prototype extension time.model.construct(attributes, options)
would first doObject.create(model)
and then perform the same logic on the created instance as the current constructor. In fact, we could retain the old constructor and simply implementmodel.construct
asObject.create(model).constructor(attributes, options)
. This would enable people who really want to use class emulation to setModel = model.constructor
and continue working in the old way.In summary, the code would not necessarily change that much. It's just that the library exports prototypes instead of constructors,
extend
moves from the constructor to the prototype and there is a newconstruct
method that replaces thenew
keyword. As a result, everyone using Backbone can seamlessly and interchangeably write their code in the same way, regardless of what particular flavor of class emulation they are using.Feedback welcome. I'm not starting on Backbone 2 anytime soon, so there is plenty of time.
The text was updated successfully, but these errors were encountered: