Wednesday, December 24, 2014

JavaScript Mixins

Building a JavaScript Mixin


In JavaScript a "mixin" is to add the properties of one or more source objects to a target object. To be precise, there are actually two kinds of mixins: a shallow copy and a deep copy. A shallow copy simply copies the references from the source to the target, and a deep copy clones the references.

So why a mixin? Well, there are two really good examples and both of them revolve around prototypes. The first involves AJAX. When JSON data is returned from a web service call it only contains data, no methods. That is intentional; if instantiating a JSON object added methods to the local program that would be an opportunity for evil code to be injected into a program from a remote source. But what if the data returned really represents something like a bank account, where our design depends on methods defined as part of the object? The ECMAScript 5 (current) and earlier specifications do not allow a prototype to be assigned to an existing object (our JSON object), nor does the JSON.parse method provide a means for a prototype to be attached. A prototype can only be attached to a new object during creation. So our solution is to create a new, empty object with the right prototype and then mixin the data properties from the JSON object.

The second case is also important: when a function object is created the initial prototype object appears empty but not really. Several properties, notably the constructor property, are assigned values by the JavaScript engine. And these properties are not enumerable, so they are often overlooked. Simply assigning a new empty object to the prototype property of the function object will cause this information to be lost in the new objects created. As many people copy sources with examples that do exactly that it is fortunate that everything still works if the constructor property is missing. The only correct ways to add properties to a prototype are to set them one by one, use Object.defineProperty (or defineProperties), or mixin another object containing the property definitions with the initial prototype object.

var SomeFunctionObject = function () {
};

// Do not do this!
SomeFunctionObject.prototype = {

@tab;someProperty: null
};

// Do this instead:
Object.assign(SomeFunctionObject.prototype, {

@tab;someProperty: null
});

An important point to notice is that neither of these examples require a deep copy. That is a normal occurrence; most operations where a mixin is required need only a shallow copy.

Enter the Mixins

There are many libraries that provide mixin functionality, for example the jQuery method $.extend() accepts a target and one or more source objects to mix into the target. The method returns the target object. And the jQuery method can perform either a shallow or deep copy. The signal is to provide a boolean as the first parameter ahead of the target object, and if it is true a deep copy is performed.

It's simple to choose a library with a mixin and leverage it. So why not use them?

For one, if you are not already using a library for its primary purpose then adding it just to get the mixin is wasteful. Second, the proposal for ECMAScript 6 includes an efficient mixin function: Object.assign. It provides a shallow copy, and that is what we need most. So it makes sense to start using Object.assign as your mixin function, even if your JavaScript engine does not offer it yet.

Since we already know how Object.assign is supposed to work all you have to do is check to see if Object.assign exists. If it does not, then  provide your own version of Object.assign as a stand-in. Eventually, when it is available, your code will automatically skip injecting your version and use the JavaScript engine.

Object.assign

All the  assign method does is take a target and one or more source objects, mix the properties from the source objects into the target, and returns the target object as the result. That is a pretty simple piece of code.

We will wrap the code with an if statement that checks to see if the property exists in Object before we create it. And, around that we will add a function wrapper just on principle to isolate the scope and build a CommonJS module that can be required in Node:

(function () {

@tab;if (!Object.assign) {

@tab;@tab;// assign
@tab;@tab;// Mixin one or more source objects with a target
@tab;@tab;// object and return the target.

@tab;@tab;Object.assign = function(target) {

@tab;@tab;@tab;for (var i = 1; i < arguments.length; i++) {

@tab;@tab;@tab;@tab;for (var p in arguments[i]) {

@tab;@tab;@tab;@tab;@tab;target[p] = arguments[i][p];
@tab;@tab;@tab;@tab;}
@tab;@tab;@tab;}

@tab;@tab;@tab;return arguments[0];
@tab;@tab;};
@tab;}
}).call(this);

If you want to take it one step further and build a RequireJS module, then all you have to do is change the wrapping function to a RequireJS define statement:

define (function (require, exports, module) {

@tab;if (!Object.assign) {

@tab;@tab;// assign
@tab;@tab;// Mixin one or more source objects with a target
@tab;@tab;// object and return the target.

@tab;@tab;Object.assign = function(target) {

@tab;@tab;@tab;for (var i = 1; i < arguments.length; i++) {

@tab;@tab;@tab;@tab;for (var p in arguments[i]) {

@tab;@tab;@tab;@tab;@tab;target[p] = arguments[i][p];
@tab;@tab;@tab;@tab;}
@tab;@tab;@tab;}

@tab;@tab;@tab;return arguments[0];
@tab;@tab;};
@tab;}
});

That's it, you're done! Just include this module anywhere you need to make sure there is a mixin available, and if the environment doesn't already have one then yours will stand in for it.

No comments:

Post a Comment