Pre-Binding Properties To An Object Constructor In JavaScript
Yesterday, I talked about the change in constructor pre-binding as a "breaking change" between AngularJS 1.x and AngularJS 2 Beta 1. Since AngularJS 2 is a completely new platform, however, I was, of course, using the term "breaking change" very loosely. But, the mechanics of pre-binding an object constructor in JavaScript are actually kind of interesting; so, I thought I would put together a quick demo of it.
Run this demo in my JavaScript Demos project on GitHub.
To be honest, I didn't even know that this was possible until I saw it being done in the AngularJS 1.x source code (which just goes to show you how valuable it can be to read source code). Internally, AngularJS uses this approach to setup (ie, pre-bind) isolate scope properties before calling the constructor of the controller to which they are bound.
The key to this approach is the Object.create() method, which creates an object that inherits from a given prototype. For a couple of years now, I've known that the Object.create() method is an important part of prototypal inheritance; however, I had never used it to bypass a constructor before. And that's how pre-binding works - the target instance gets created and injected (with pre-bound properties) before the constructor is invoked.
To see this in action, I've put together a small script that defines a "Widget" object. The Widget expects both a constructor argument and a pre-bound property to be available in the constructor context. To do this, we need to create the widget instance first, inject the pre-bound property, and then run the constructor.
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>
Pre-Binding Properties To An Object Constructor In JavaScript
</title>
</head>
<body>
<h1>
Pre-Binding Properties To An Object Constructor In JavaScript
</h1>
<!-- Load scripts. -->
<script type="text/javascript">
// Here, we are defining our object constructor. The constructor takes one
// argument; but, it is also expecting to have a property pre-bound to it
// and available within the constructor context.
function Widget( name ) {
// Log the explicit argument.
this.log( "constructor", "name", name );
// Log the injected and pre-bound property.
// --
// NOTE: This property is neither a constructor argument nor part of the
// Widget prototype definition - it is being injected as a pre-binding.
this.log( "constructor", "prebound", this.prebound );
// I am returning a new object just for the sake of demonstrating that
// the instance can still be overridden by the result of the constructor,
// the same way that any constructor would work in JavaScript.
var self = this;
return({
toString: function() {
self.log( "toString", "prebound", self.prebound );
return( "Widget for " + name + "." );
}
});
}
// Define the object prototype.
Widget.prototype = {
// I log a message based on the given inputs to echo.
log: function( context, key, value ) {
console.log( "Widget:" + context + "( " + key + " ):", value );
}
};
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
// Normally, when invoking a constructor, we'd just "new" it into existence.
// However, when pre-binding properties, we actually have to create the instance
// before we initialize it. By using Object.create(), we can bypass the
// constructor while still creating an object with the correct prototype chain.
var container = Object.create( Widget.prototype || Object.prototype );
// Now that we have our object instance, we can inject all of the pre-bound
// properties that we want to expose in the constructor.
container.prebound = "Such pre-binding, much exciting!";
// At this point, we have an object with the correct prototype chain and the
// injected properties. Now, all we have to do is invoke the constructor in the
// correct object context (ie, this-binding).
// --
// NOTE: I am storing the return value into an new variable so as to allow the
// constructor and opportunity to return a new object.
var instance = ( Widget.call( container, "Thinger" ) || container );
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
// Since we know that our constructor is actually returning a different
// reference, let's just test to make sure the .toString() works.
console.log( "- - - - - - - - - - " );
console.log( instance.toString() );
</script>
</body>
</html>
As you can see, we're using Object.create() to create a new object that extends the prototype of the Widget. This essentially creates a new instance of the Widget class without it being initialized. Then, we inject the pre-binding and call the constructor. And, when we run the above code, we get the following output:
As you can see, the "prebind" property is available in the Widget constructor despite the fact that it is neither a constructor argument nor part of the Widget prototype definition.
In a standard JavaScript context, this approach is likely to be a bit "too magical." After all, this allows properties to mysteriously appear out of nowhere. But, in the context of something like AngularJS, where the framework is providing all kinds of "magic", this approach is much more acceptable. That said, the mechanics of this are kind of beautiful; and, seeing it in action is just kind of exhilarating.
Want to use code from this post? Check out the license.
Reader Comments
Similar to some voodoo magic I was trying recently. I tried to bind a controller constructor function to certain parameters and then pass that controller to a DDO letting Angular's injector provide the rest of the arguments. It looked something like this:
function bindCtor(Ctor, argsToBind) {
var BoundCtor = Ctor.bind.apply(Ctor, argsToBind);
BoundCtor.$inject = Ctor.$inject;
return BoundCtor;
}
In the hope that I could do the following:
function WidgetCtrl (title) {
this.title = title;
}
...
.directive('Widget', {
...
controller: bindCtor(WidgetCtrl, 'A title'),
...
});
However, when Angular calls Object.create, it is called as you have in your demo (instance = Object.create(controllerPrototype || null)) - and my bound constructor doesn't have the same prototype as WidgetCtrl resulting in the `this` context of the constructor to be the `window` when the constructor actually runs.
I know there are better ways of providing values to a controller (an isolated scope for instance) but I needed to do this for reasons that aren't worth discussing. An interesting investigation even so!