Skip to content

Instantly share code, notes, and snippets.

@aroc
Created June 5, 2013 16:14
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save aroc/5715154 to your computer and use it in GitHub Desktop.
Save aroc/5715154 to your computer and use it in GitHub Desktop.
Factory Pattern for Backbone.js
// Allows you to create a child which is a copy of the parent and changing the child or parent doesn't affect one another.
// Calling "this" inside the child won't change attributes of the parent, as it is a copy and not connected through the prototype chain.
define(['jquery', 'underscore', 'backbone'], function ($, _, Backbone) {
// NOTE: This works in every scenario I've tested it in. However, this code looks bad
// and there may very well be a much cleaner + better way to handle this. Or maybe not
// because JS prototypes are crazy.
function childOfClass (parentFunction, newAttributes) {
// Create the new function and ensure to call Backbone's standard constructor
// with the context of *this* (this function).
// NOTE: We could change this to be the constructor of the parentFunction in order to
// have the parentFunction constructor code ran, then add in a call to
// this.initialize.apply(this, arguments) to *also* run this function's initialize method upon
// object construction.
function childFunction () {
Backbone.Model.prototype.constructor.apply(this, arguments);
}
// Set the prototype equal to that of a NEW parentFunction. We do new
// parentFUnction instead of just parentFunction or parentFunction.prototype
// because this breaks the prototype chain and allows us to inherit a *copy* of the
// parentFunction's attributes.
childFunction.prototype = new parentFunction();
// After we've set the prototype equal to that of the parentFunction, ensure
// the constructor is set to the childFunction function, or the Backbone.Model
// constructor code won't get run when the object is created.
childFunction.prototype.constructor = childFunction;
// Extend the prototype with the new / additional functionality.
_.extend(childFunction.prototype, newAttributes);
// Return the new function / class.
return childFunction;
}
return childOfClass;
});
@percyhanna
Copy link

What does this do that Backbone.Model.extend() doesn't do? Were there other issues you were having?

@kastiglione
Copy link

I don't totally understand why you need this, but I see what you're doing here. Looks like these are necessary hoops for javascript.

Being curious, I wondered how it could be done in coffeescript, this is the closest I could get.

childOfClass = (parentFunction, newAttributes) ->
  parentInstance = new parentFunction
  _.extend(parentInstance, newAttributes)
  class childFunction extends parentInstance
    constructor: ->
      Backbone.Model.prototype.constructor.apply(this, arguments)

    # Without this, the instance's prototype would have parentInstance.prototype
    # in the prototype chain, but that's not wanted.
    # Uses knowledge of how coffeescript defines classes, total hack.
    this::prototype = parentInstance

Which generates:

// Generated by CoffeeScript 1.6.2
(function() {
  var childOfClass,
    __hasProp = {}.hasOwnProperty,
    __extends = function(child, parent) { /* excluded for brevity */ };

  childOfClass = function(parentFunction, newAttributes) {
    var childFunction, parentInstance;

    parentInstance = new parentFunction;
    _.extend(parentInstance, newAttributes);
    return childFunction = (function(_super) {
      __extends(childFunction, _super);

      function childFunction() {
        Backbone.Model.prototype.constructor.apply(this, arguments);
      }

      childFunction.prototype.prototype = parentInstance;

      return childFunction;

    })(parentInstance);
  };

}).call(this);

@percyhanna
Copy link

I'm still confused. When you create an instance with the new operator, you don't directly modify the prototype chain at all. This also seems like it's pretty much the exact same as the built-in code that Backbone has for Backbone.Model.extend().

Do you have an example that demonstrates the problem you're trying to solve? I mentioned earlier about using hashes and arrays as default values in a class's prototype.

Example:

MyView = Backbone.View.extend({
stuff: [1,2,3]
});

MyOtherView = MyView.extend({});

myView = new MyView();
otherView = new MyOtherView();

otherView.stuff.push(4);
console.log(myView.stuff);
// -> [1,2,3,4]

Because all objects (arrays and hashes included) are stored by referenced in JavaScript, if you store one in the prototype, then that one object reference is shared across all instances of the prototype. Numbers and strings are not affected in the same way because they are immutable in JavaScript.

I just wasn't sure if that's the issue you were dealing with, but I've dealt with that issue in the past, and the way to solve it is to initialize non-constant value (e.g. arrays/hashes) in your constructor. Based on the description you were giving, I thought perhaps this is what you were dealing with.

Notice the constructor usage instead of defining it in the prototype:

MyView = Backbone.View.extend({
initialize: function() { this.stuff = [1,2,3] }
});

MyOtherView = MyView.extend({});

myView = new MyView();
otherView = new MyOtherView();

otherView.stuff.push(4);
console.log(myView.stuff);
// -> [1,2,3]

@aroc
Copy link
Author

aroc commented Jun 6, 2013

The problem I was having was this: I have a UserModel and I have a SignupModel which extends (Backbone extends) the UserModel. I wanted to have the SignupModel make changes to some of the properties it inherited from the UserModel, but doing so updated the UserModel's prototype as well as the SignupModel's - thus any new instances of UserModel now have the changes I made in SignupModel. I was making the changes by referring to "this.X" in the initialize function.

UserModel = Backbone.Model.extend({
  validation: 'user'
});

SignupModel = UserModel.extend({
  initialize: function (attributes, options) {
    this.validation = 'signup';
  }
});

Now if I instantiate a SignupModel, the UserModel's "validation" value gets changed - which adversely affects my application. I just basically want a copy of all the functionality UserModel has, then I can make changes as I please.

@aroc
Copy link
Author

aroc commented Jun 6, 2013

I tried to find ways to call "this.validation" specifying the SignupModel as the specific context, but I couldn't get anything to work properly outside of the solution I posted above.

@kastiglione
Copy link

With the code you've shown, it's mixing prototype properties and instance properties. UserModel has a validation property on its prototype, while SignupModel does not. Instances of UserModel do not have an instance property for validation, instead getting their value from UserModel's prototype. Instances of SignupModel do have an instance property for validation, which means the prototype chain is not referenced.

Here's a fiddle showing what I mean, open the console to see the output.

http://jsfiddle.net/Lt9kz/4/

@kastiglione
Copy link

I forgot to mention the main point, that the fiddle shows that an instance of SignupModel and instance of UserModel have independent validation values. In your code, I'm unsure how setting this.validation in initialize has any effect outside of that instance of SignupModel.

@kastiglione
Copy link

How are you creating SignupModel instances? Are you using call, apply, or _.bind to create SignupModel instances? The only way I can think of that would cause assigning to this.validation to affect UserModel is by supply UserModel.prototype as this via one of those functions. Maybe another possibility is forgetting to invoke with new.

@percyhanna
Copy link

I'm also not seeing how this would break, unless you're not using the new keyword. SignupModel.prototype should inherit validation from UserModel's prototype. But setting this.validation in the constructor on SignupModel should only change the value on that instance, not either of the prototypes.

Are you using call or apply anywhere to change the value of this used when your constructor is called? That's what @kastiglione is getting at.

@aroc
Copy link
Author

aroc commented Jun 6, 2013

I've come across my issue. I want SignupModel to inherit values from UserModel, then I want to be able to directly modify those values while keeping a separate copy. This jsfiddle outlines precisely my issue.

http://jsfiddle.net/Lt9kz/5/

@percyhanna
Copy link

@aroc: That's exactly what I was referring to. You're using a Hash/Object in the UserModel prototype, and you're just changing a single value in the child class. Both classes are sharing the same instance of the Hash/Object.

@kastiglione
Copy link

I haven't tried it, but try adding this.validation = $.extend(true, {}, this.validation) to get a local deep copy of the validation hash.

http://api.jquery.com/jQuery.extend/#jQuery-extend-deep-target-object1-objectN

@ekryski
Copy link

ekryski commented Jun 6, 2013

Ya @kastiglione that should work. The issue with the previous solutions is that we are needing a deep copy of the validations object.

@aroc
Copy link
Author

aroc commented Jun 6, 2013

We tried that here a couple days ago. It does work, but the problem is I need to deep copy every attribute I want to from UserModel over to SignupModel. I'd prefer if I didn't need to go about it that way.

We tried doing a deep copy of the UserModel's prototype, but couldn't manage to get it working properly - it still got overridden.

@tylermercier
Copy link

The issue is that hasOwnProperty will only look at the immediate object and won't navigate the prototype chain. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/hasOwnProperty
hasOwnProperty is only good for telling where the function/object is defined, not if the object will respond to the function. For that I would call it and see if you get 'undefined', unless there is a better way.

Add the following to your fiddle and you'll see it works fine.

alert((new UserModel).validation['user']['type']);
alert((new SignupModel).validation['user']['type']);

Unless I am missing something from your fiddle.

@tylermercier
Copy link

The other thing to be aware of is that it appears Backbone defines it's own extend function. If you are looking at the one in underscore, you are looking in the wrong place. It's at the bottom of the page. http://backbonejs.org/docs/backbone.html

@kastiglione
Copy link

@tylermercier The parts in the gist about hasOwnProperty were from me, but they're irrelevant. See the last two comments by @ekryski and @aroc. Basically, they want deep copying of a number of properties from the parent's prototype, but they don't want to explicitly do that in each subclass initializer.

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