Js 获取并设置嵌套的对象属性

我有一个关于 Backbone.js 的 走开准备好了函数的简单问题。

1)使用下面的代码,如何直接“获取”或“设置”objec1.myAttribute1?

还有一个问题:

2)在 Model 中,除了 违约对象,我可以/应该在哪里声明我的模型的其他属性,以便它们可以通过 Backbone 的 get 和 set 方法访问?

var MyModel = Backbone.Model.extend({
defaults: {
obj1 : {
"myAttribute1" : false,
"myAttribute2" : true,
}
}
})


var MyView = Backbone.View.extend({
myFunc: function(){
console.log(this.model.get("obj1"));
//returns the obj1 object
//but how do I get obj1.myAttribute1 directly so that it returns false?
}
});

我知道我能做到:

this.model.get("obj1").myAttribute1;

但这样做好吗?

84267 次浏览

While this.model.get("obj1").myAttribute1 is fine, it's a bit problematic because then you might be tempted to do the same type of thing for set, i.e.

this.model.get("obj1").myAttribute1 = true;

But if you do this, you won't get the benefits of Backbone models for myAttribute1, like change events or validation.

A better solution would be to never nest POJSOs ("plain old JavaScript objects") in your models, and instead nest custom model classes. So it would look something like this:

var Obj = Backbone.Model.extend({
defaults: {
myAttribute1: false,
myAttribute2: true
}
});


var MyModel = Backbone.Model.extend({
initialize: function () {
this.set("obj1", new Obj());
}
});

Then the accessing code would be

var x = this.model.get("obj1").get("myAttribute1");

but more importantly the setting code would be

this.model.get("obj1").set({ myAttribute1: true });

which will fire appropriate change events and the like. Working example here: http://jsfiddle.net/g3U7j/

I created backbone-deep-model for this - just extend Backbone.DeepModel instead of Backbone.Model and you can then use paths to get/set nested model attributes. It maintains change events too.

model.bind('change:user.name.first', function(){...});
model.set({'user.name.first': 'Eric'});
model.get('user.name.first'); //Eric

Domenic's solution will work however each new MyModel will point to the same instance of Obj. To avoid this, MyModel should look like:

var MyModel = Backbone.Model.extend({
initialize: function() {
myDefaults = {
obj1: new Obj()
}
this.set(myDefaults);
}
});

See c3rin's answer @ https://stackoverflow.com/a/6364480/1072653 for a full explanation.

I had the same problem @pagewil and @Benno had with @Domenic's solution. My answer was to instead write a simple sub-class of Backbone.Model that fixes the problem.

// Special model implementation that allows you to easily nest Backbone models as properties.
Backbone.NestedModel = Backbone.Model.extend({
// Define Backbone models that are present in properties
// Expected Format:
// [{key: 'courses', model: Course}]
models: [],


set: function(key, value, options) {
var attrs, attr, val;


if (_.isObject(key) || key == null) {
attrs = key;
options = value;
} else {
attrs = {};
attrs[key] = value;
}


_.each(this.models, function(item){
if (_.isObject(attrs[item.key])) {
attrs[item.key] = new item.model(attrs[item.key]);
}
},this);


return Backbone.Model.prototype.set.call(this, attrs, options);
}
});


var Obj = Backbone.Model.extend({
defaults: {
myAttribute1: false,
myAttribute2: true
}
});


var MyModel = Backbone.NestedModel.extend({
defaults: {
obj1: new Obj()
},


models: [{key: 'obj1', model: Obj}]
});

What NestedModel does for you is allow these to work (which is what happens when myModel gets set via JSON data):

var myModel = new MyModel();
myModel.set({ obj1: { myAttribute1: 'abc', myAttribute2: 'xyz' } });
myModel.set('obj1', { myAttribute1: 123, myAttribute2: 456 });

It would be easy to generate the models list automatically in initialize, but this solution was good enough for me.

I use this approach.

If you have a Backbone model like this:

var nestedAttrModel = new Backbone.Model({
a: {b: 1, c: 2}
});

You can set the attribute "a.b" with:

var _a = _.omit(nestedAttrModel.get('a')); // from underscore.js
_a.b = 3;
nestedAttrModel.set('a', _a);

Now your model will have attributes like:

{a: {b: 3, c: 2}}

with the "change" event fired.

While in some cases using Backbone models instead of nested Object attributes makes sense as Domenic mentioned, in simpler cases you could create a setter function in the model:

var MyModel = Backbone.Model.extend({
defaults: {
obj1 : {
"myAttribute1" : false,
"myAttribute2" : true,
}
},
setObj1Attribute: function(name, value) {
var obj1 = this.get('obj1');
obj1[name] = value;
this.set('obj1', obj1);
}
})

If you interact with backend, which requires object with nesting structure. But with backbone more easy to work with linear structure.

backbone.linear can help you.

Solution proposed by Domenic has some drawbacks. Say you want to listen to 'change' event. In that case 'initialize' method will not be fired and your custom value for attribute will be replaced with json object from server. In my project I faced with this problem. My solution to override 'set' method of Model:

set: function(key, val, options) {
if (typeof key === 'object') {
var attrs = key;
attrs.content = new module.BaseItem(attrs.content || {});
attrs.children = new module.MenuItems(attrs.children || []);
}


return Backbone.Model.prototype.set.call(this, key, val, options);
},

There is one solution nobody thought of yet which is lots to use. You indeed can't set nested attributes directly, unless you use a third party library which you probably don't want. However what you can do is make a clone of the original dictionary, set the nested property there and than set that whole dictionary. Piece of cake.

//How model.obj1 looks like
obj1: {
myAttribute1: false,
myAttribute2: true,
anotherNestedDict: {
myAttribute3: false
}
}


//Make a clone of it
var cloneOfObject1 = JSON.parse(JSON.stringify(this.model.get('obj1')));


//Let's day we want to change myAttribute1 to false and myAttribute3 to true
cloneOfObject1.myAttribute2 = false;
cloneOfObject1.anotherNestedDict.myAttribute3 = true;


//And now we set the whole dictionary
this.model.set('obj1', cloneOfObject1);


//Job done, happy birthday