Skip to content
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

Avoid Object.prototype collisions with defaults #3843

Merged
merged 1 commit into from
Feb 4, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions backbone.js
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,8 @@
this.attributes = {};
if (options.collection) this.collection = options.collection;
if (options.parse) attrs = this.parse(attrs, options) || {};
attrs = _.defaults({}, attrs, _.result(this, 'defaults'));
var defaults = _.result(this, 'defaults');
attrs = _.defaults(_.extend({}, defaults, attrs), defaults);
this.set(attrs, options);
this.changed = {};
this.initialize.apply(this, arguments);
Expand Down Expand Up @@ -714,7 +715,7 @@

// Check if the model is currently in a valid state.
isValid: function(options) {
return this._validate({}, _.defaults({validate: true}, options));
return this._validate({}, _.extend({}, options, {validate: true}));
},

// Run validation against the next complete set of model attributes,
Expand Down Expand Up @@ -824,7 +825,7 @@
set: function(models, options) {
if (models == null) return;

options = _.defaults({}, options, setOptions);
options = _.extend({}, setOptions, options);
if (options.parse && !this._isModel(models)) {
models = this.parse(models, options) || [];
}
Expand Down
37 changes: 21 additions & 16 deletions test/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@
assert.equal(model.collection, collection);
});

QUnit.test('Object.prototype properties are overridden by attributes', function(assert) {
assert.expect(1);
var model = new Backbone.Model({hasOwnProperty: true});
assert.equal(model.get('hasOwnProperty'), true);
});

QUnit.test('initialize with attributes and options', function(assert) {
assert.expect(1);
var Model = Backbone.Model.extend({
Expand All @@ -57,19 +63,6 @@
assert.equal(model.get('value'), 2);
});

QUnit.test('initialize with defaults', function(assert) {
assert.expect(2);
var Model = Backbone.Model.extend({
defaults: {
firstName: 'Unknown',
lastName: 'Unknown'
}
});
var model = new Model({'firstName': 'John'});
assert.equal(model.get('firstName'), 'John');
assert.equal(model.get('lastName'), 'Unknown');
});

QUnit.test('parse can return null', function(assert) {
assert.expect(1);
var Model = Backbone.Model.extend({
Expand Down Expand Up @@ -428,7 +421,7 @@
});

QUnit.test('defaults', function(assert) {
assert.expect(4);
assert.expect(9);
var Defaulted = Backbone.Model.extend({
defaults: {
one: 1,
Expand All @@ -438,6 +431,9 @@
var model = new Defaulted({two: undefined});
assert.equal(model.get('one'), 1);
assert.equal(model.get('two'), 2);
model = new Defaulted({two: 3});
assert.equal(model.get('one'), 1);
assert.equal(model.get('two'), 3);
Defaulted = Backbone.Model.extend({
defaults: function() {
return {
Expand All @@ -449,6 +445,15 @@
model = new Defaulted({two: undefined});
assert.equal(model.get('one'), 3);
assert.equal(model.get('two'), 4);
Defaulted = Backbone.Model.extend({
defaults: {hasOwnProperty: true}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why'd you get rid of this test case?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's testing the same thing as the one above it, no?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No... it's testing that passing undefined is thrown away, not that the key isn't passed at all.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

var defaults = {foo: true};

_.defaults({}, defaults, {foo: undefined}); // { foo: true }
_.defaults({}, defaults, {bar: undefined}); // { foo: true, bar: undefined }

_.extend({}, defaults, {foo: undefined}); // { foo: undefined }
_.extend({}, defaults, {bar: undefined}); // { foo: true, bar: undefined }

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No... it's testing that passing undefined is thrown away, not that the key isn't passed at all.

Isn't that this test?

var defaults = {foo: true};

With my "defaults" implementation:

defaults = {foo: true};

defaults({}, defaults, {foo: undefined}); // { foo: true }
defaults({}, defaults, {bar: undefined}); // { foo: true, bar: undefined }

The only difference with mine is it won't respect properties that are already on the dest object:

_.defaults({foo: false}, {foo: true}); // { foo: false }
defaults({foo: false}, {foo: true}); // { foo: true }

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't that this test?

Sorry, missed that in the diff.

The only difference with mine is it won't respect properties that are already on the dest object

Then I'm confused. That changes _.default's behavior completely, no?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're only using defaults with an "empty" destination object, so it works the same. Externally, this wouldn't be acceptable.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah but I dunno. Either we should fix this in Underscore or we shouldn't fix this at all. What's good for the goose is good for the gander...

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we made it return a new instance instead of modifying the destination, it would work perfectly. We're kind of headed down that path with jashkenas/underscore#2232.

});
model = new Defaulted();
assert.equal(model.get('hasOwnProperty'), true);
model = new Defaulted({hasOwnProperty: undefined});
assert.equal(model.get('hasOwnProperty'), true);
model = new Defaulted({hasOwnProperty: false});
assert.equal(model.get('hasOwnProperty'), false);
});

QUnit.test('change, hasChanged, changedAttributes, previous, previousAttributes', function(assert) {
Expand Down Expand Up @@ -989,8 +994,8 @@

QUnit.test('`previous` for falsey keys', function(assert) {
assert.expect(2);
var model = new Backbone.Model({0: true, '': true});
model.set({0: false, '': false}, {silent: true});
var model = new Backbone.Model({'0': true, '': true});
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whys this needed? JS automatically casts keys to strings

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was a liter warning for inconsistent string usage. Must have committed it by accident. 😊

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Linting rules are not perfect 😞

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All good, they're much better now than before.

model.set({'0': false, '': false}, {silent: true});
assert.equal(model.previous(0), true);
assert.equal(model.previous(''), true);
});
Expand Down