Backbone.js Empty Array Attribute

I'm running into an odd issue with a Backbone.js Model where an array member is being shown as blank. It looks something like this:

var Session = Backbone.Model.extend({
    defaults: {
        // ...
        widgets: []
    },
    addWidget: function (widget) {
        var widgets = this.get("widgets");

        widgets.push(widget);
        this.trigger("change:widgets", this, widgets);
    },
    // ...
    // I have a method on the model to grabbing a member of the array
    getWidget: function (id) {
        console.log(this.attributes);
        console.log(this.attributes.widgets);

        // ...
    }
});

I then add a widget via addWidget. When trying getWidget the result I get (in Chrome) is this:

Object
    widgets: Array[1]
        0: child
        length: 1
        __proto__: Array[0]
    __proto__: Object
[]

It's showing that widgets is not empty when logging this.attributes but it's shown as empty when logging this.attributes.widgets. Does anyone know what would cause this?

EDIT I've changed the model to instantiate the widgets array in the initialization method to avoid references across multiple instances, and I started using backbone-nested with no luck.


Solution 1:

Be careful about trusting the console, there is often asynchronous behavior that can trip you up.

You're expecting console.log(x) to behave like this:

  1. You call console.log(x).
  2. x is dumped to the console.
  3. Execution continues on with the statement immediately following your console.log(x) call.

But that's not what happens, the reality is more like this:

  1. You call console.log(x).
  2. The browser grabs a reference to x, and queues up the "real" console.log call for later.
  3. Various other bits of JavaScript run (or not).
  4. Later, the console.log call from (2) gets around to dumping the current state of x into the console but this x won't necessarily match the x as it was in (2).

In your case, you're doing this:

console.log(this.attributes);
console.log(this.attributes.widgets);

So you have something like this at (2):

         attributes.widgets
             ^         ^
             |         |
console.log -+         |
console.log -----------+

and then something is happening in (3) which effectively does this.attributes.widgets = [...] (i.e. changes the attributes.widget reference) and so, when (4) comes around, you have this:

         attributes.widgets // the new one from (3)
             ^
             |
console.log -+
console.log -----------> widgets // the original from (1)

This leaves you seeing two different versions of widgets: the new one which received something in (3) and the original which is empty.

When you do this:

console.log(_(this.attributes).clone());
console.log(_(this.attributes.widgets).clone());

you're grabbing copies of this.attributes and this.attributes.widgets that are attached to the console.log calls so (3) won't interfere with your references and you see sensible results in the console.

That's the answer to this:

It's showing that widgets is not empty when logging this.attributes but it's shown as empty when logging this.attributes.widgets. Does anyone know what would cause this?

As far as the underlying problem goes, you probably have a fetch call somewhere and you're not taking its asynchronous behavior into account. The solution is probably to bind to an "add" or "reset" event.

Solution 2:

Remember that [] in JS is just an alias to new Array(), and since objects are passed by reference, every instance of your Session model will share the same array object. This leads to all kinds of problems, including arrays appearing to be empty.

To make this work the way you want, you need to initialize your widgets array in the constructor. This will create a unique widget array for each Session object, and should alleviate your problem:

var Session = Backbone.Model.extend({
    defaults: {
        // ...
        widgets: false
    },
    initialize: function(){
        this.set('widgets',[]);
    },
    addWidget: function (widget) {
        var widgets = this.get("widgets");

        widgets.push(widget);
        this.trigger("change:widgets", this, widgets);
    },
    // ...
    // I have a method on the model to grabbing a member of the array
    getWidget: function (id) { 
        console.log(this.attributes);
        console.log(this.attributes.widgets);
    // ...
    }
});