Augmenting Objects
When you’re working with JavaScript, you’re working with objects. The dojo/_base/lang
resource makes it easy to augment objects and prototypes using lang.mixin
, lang.extend
, and declare.safeMixin
when using dojo/_base/declare
.
Getting Started
lang.mixin
, lang.extend
, and declare.safeMixin
are all used to augment an original object with properties from one or more other objects. In this context, the other objects are called “mixins”. There are small differences between each of these functions that make them suitable for different use cases. Here's a quick overview of the differences before we dive in:
Method: | lang.mixin | declare.safeMixin | lang.extend |
---|---|---|---|
Operates on | object | object | object.prototype |
Mixes in constructor property |
yes | no | yes |
Mixes in multiple objects at once | yes | no | yes |
Annotates functions to support this.inherited |
no | yes | no |
Speed | fast | slow | fast |
Use primarily with | A plain object | A declare instance | A constructor |
lang.mixin
The lang.mixin
method is a simple utility function that, given any number of objects as arguments, adds the properties of subsequent objects to the first object and returns it. For example, let's say we have an existing object to which we need to add some additional properties. This might be a collection of form data, some data for a template, a namespace object, or a settings object. In any case, without lang.mixin
, copying over several properties might look like this:
var formData = domForm.formToObject(dom.byId('form'));
formData.name = currentUser.name;
formData.phone = currentUser.phone;
formData.address = currentUser.address;
formData.city = currentUser.city;
formData.province = currentUser.province;
formData.country = currentUser.country;
formData.postalCode = currentUser.postalCode;
That's a lot of typing just to copy data from one object to another.
Assuming all the properties in the second object were meant to be
copied to the first, lang.mixin
makes this work much simpler:
var formData = domForm.formToObject(dom.byId("form"));
lang.mixin(formData, currentUser);
Notice how the return value of this call was discarded. This is because lang.mixin
works directly on the first object passed in, so we don't need to manually update the formData
variable at all.
lang.mixin
can also mix in multiple objects at the same time. For example, if you had a settings object on a prototype that always needed to contain certain default values whenever a new instance was created, you could simply write it like this:
var defaultSettings = {
useTheForce: true,
isEvil: false,
length: 75,
color: "blue"
};
function Lightsaber(settings){
// `defaultSettings` is first mixed into the blank object,
// then `settings` is mixed into the blank object, overriding
// any properties from `defaultSettings` without altering
// the `defaultSettings` object
this.settings = lang.mixin({}, defaultSettings, settings);
}
var darthsaber = new Lightsaber({
isEvil: true,
color: "red"
});
// { useTheForce: true, isEvil: true, length: 75, color: "red" }
console.log("darthsaber:", darthsaber.settings);
declare.safeMixin
declare.safeMixin
primarily accomplishes the same task as lang.mixin
, with three important differences:
- It can only mix in one object at a time
- It will not mix in the
constructor
property - It will add annotations to functions to make them function properly with
declare
'sthis.inherited
function
Despite what the name implies, there is nothing “unsafe” about the original lang.mixin
, unless:
- You are adding functions to an instance of an object created with
declare
, and: - you rely on calls to
this.inherited
within those new functions, or - one or more of the mixins contains a
constructor
property
Thus, outside of cases involving instances of instances of declare
d classes, lang.mixin
is likely to be sufficient.
declare
d constructors expose an extend
method, which is really just an alias for calling declare.safeMixin
with the declared constructor's prototype as the first argument. This is not to be confused with lang.extend
, which we will examine next.
lang.extend
lang.extend
is very similar to lang.mixin
, except that it adds properties to the first object's prototype
instead of directly on the object itself. Directly augmenting an object's prototype is useful if you want to make changes that immediately affect all inheriting instances:
// Assume Lightsaber is defined as in the previous example
var darthsaber = new Lightsaber({
isEvil: true,
color: "red"
});
var weaponMixin = {
hp: 5,
maxHp: 10,
repair: function() {
if(this.hp >= this.maxHp) {
console.log("Can't repair!");
return;
}
this.hp++;
},
swing: function() {
if(!this.hp) {
console.log("Weapon is broken!");
return;
}
this.hp--;
console.log(Math.random() >= 0.5 ? "hit!" : "miss!");
}
};
lang.extend(Lightsaber, weaponMixin);
// Now we can call swing() on our Lightsaber instance,
// even though we augmented the prototype after creating the instance.
darthsaber.swing(); // "hit!" (or "miss!" if you are unlucky)
This is much more performant than calling lang.mixin
on every new object that is created, as it modifies one inherited object rather than several child objects. It is also faster than using declare.safeMixin
. However, should you need to augment a declare
d constructor with functions that respect calls to this.inherited
, you should use declare.safeMixin
, or the extend
method on the constructor:
var Lightsaber = declare({
constructor: function(settings){
this.settings = lang.mixin({}, defaultSettings, settings);
}
});
// same augmentation, but calls to this.inherited won't break:
Lightsaber.extend(weaponMixin);
Note that using lang.extend
or declaredConstructor.extend
effectively modifies the prototype of the constructor in question, affecting all created instances. Thus, it is important to limit usage of these functions to cases where you are certain this effect is appropriate, as it could otherwise produce unwanted side-effects. In particular, remember that when creating customizations of out-of-the-box components, it is recommended to create a derivative using declare
, rather than augment the existing prototype.
It's important to note that all mixin functions perform "shallow" copies. This means that the following is true:
It is important to note that the mixin functions we have introduced perform “shallow” copies. For example:
var a = {
name: "a",
subObject: {
foo: "bar"
}
};
var b = lang.mixin({}, a);
b.name = "b";
b.subObject.foo = "baz";
console.log("a b, as expected:",
a.name, b.name);
console.log("true - both subObjects reference the exact same object:",
a.subObject === b.subObject);
console.log("baz baz - a change to one subObject affects both:",
a.subObject.foo, b.subObject.foo);
In simple cases, this behavior is not a problem; in some cases it may even be desirable. However, in cases where you do not want objects to be shared, you can perform a “deep” copy using lang.clone
:
var a = {
name: "a",
subObject: {
foo: "bar"
}
};
var b = lang.clone(a);
b.name = "b";
b.subObject.foo = "baz";
console.log("a b, same as before:",
a.name, b.name);
console.log("false - the subObjects are different now:",
a.subObject === b.subObject);
console.log("bar baz - a change to one subObject no longer affects all:",
a.subObject.foo, b.subObject.foo);
Keep in mind that deep copies can be significantly slower than shallow copies, and will cause scripts to hang if used on recursive data structures. Deep copies should be used only when absolutely necessary.
Conclusion
Dojo simplifies the process of creating and augmenting objects and classes. The lang.mixin
and declare.safeMixin
methods offer a convenient way to add and modify properties on an object, and lang.extend
makes it easy to modify the prototype of an object. Remember, though, that these functions perform shallow copies.
dojo/_base/lang Resources
Looking for more detail about lang.mixin
, lang.extend
, declare.safeMixin
and lang.clone
? Check out these great resources: