Have you ever thought about initiating objects within your angularjs applications? Controllers, factories, services, decorators and even values - all of them finally are created using the instantiating method of the $injector
class and here is a very interesting line code that I'd like to discover for you.
Today I'm going to describe the next code line:
return new (Function.prototype.bind.apply(ctor, args))();
Is the work principle of this line obvious to you? If the answer is "Yes", so, thanks for your time and patience, hope to see you in next articles. :)
Now, when all the readers who cut their teeth on JavaScript have left us, I'd like to answer my own question: When I saw this line for the first time, I was confused and didn't understand anything about these "relationships" among bind
, apply
, new
and ()
. Let's try to puzzle out! I offer to start doing it from the end, meaning: assume that we have some parameterized constructor, the instance of which we would like to instantiate:
function Animal(name, sound) {
this.name = name;
this.sound = sound;
}
new
"What could be easier?" - you would say and will be absolutely right:
var dog = new Animal('Dog', 'Woof!');
The new
operator is the first thing that we will need if we'd like to get a new instance of the Animal
constructor. Let me give you some details about the new
operator usage details:
Quote:
When the code new Foo(...) is executed, the following things happen:
- A new object is created, inheriting from Foo.prototype.
- The constructor function Foo is called with the specified arguments, and with this bound to the newly created object. new Foo is equivalent to new Foo(), i.e. if no argument list is specified, Foo is called without arguments.
- The object returned by the constructor function becomes the result of the whole new expression. If the constructor function doesn't explicitly return an object, the object created in step 1 is used instead. (Normally constructors don't return a value, but they can choose to do so if they want to override the normal object creation process.)
For further details: https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Operators/new
Great, now let's wrap our Animal
constructor execution with the function to make it common for all possible calls in the application:
function CreateAnimal(name, sound) {
return new Animal(name, sound);
}
After a while, we decide to initiate not only an animal but also a human being (I agree with you, this is not the best example ever) and this means that we have at least 2 possible ways to achieve it:
- Implement the factory which will initiate the required constructor instance itself depending on the required type
- Extend the declaration of our current function with an argument that should contain the constructor function and, based on this constructor, return the new function with the bounded arguments (and in this case
bind
is really helpful)
In case of $injector.instantiate
implementation, the second point was chosen.
bind
function Create(ctorFunc, name, sound) {
return new (ctorFunc.bind(null, name, sound));
}
console.log( Create(Animal, 'Dog', 'Woof') );
console.log( Create(Human, 'Person') );
Let me give you some details about bind
function usage details:
Quote:
The bind() method creates a new function that, when called, has its this keyword set to the provided value, with a given sequence of arguments preceding any provided when the new function is called.
For further details: https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Function/bind
In our case, we pass null
as this
because we are going to use the returned from bind
function with the new operator, which ignores existing this
and replaces it with a new empty object. The result of the bind
function call is the new function with the already bounded arguments (in other words, return new fn;
where fn
is the bind
call result).
Excellent, now we can use our Create
function to create any animals and human beings whose constructors... are declared with the name
and sound
arguments. "But it's not true that all the arguments that are required for animal constructors will be required for human constructors in the same way" - you could reply and will be absolutely right again. And there are 2 problems that we could notice:
- Constructor declaration can vary or be changed (for example, the order or quantity of parameters), which means that we have to make changes simultaneously in a few places: constructor declaration, lines of code that call the
Create
function and instantiating line of the instance return new (ctorFunc.bind(null, name, sound))
; - The more constructors we have, the higher chance that required arguments will be different and we won't be able to continue using the same
Create
function (otherwise, we have to pass all the declared arguments for each constructor, but use only the required ones).
apply
<meta charset="utf-8" />The transparent passing arguments from the Create
function directly to the constructor could become the solution of the above problems, in other words - the universal function that accepts the constructor and required by-passed constructor array of arguments and returns a new function with the already bounded arguments. There is a wonderful function named apply
in JavaScript for this case (or its analogue named call
if we know the number of arguments beforehand).
Let me give you a small excursus about apply
function usage details:
Quote:
The apply() method calls a function with a given this value, and arguments provided as an array (or an array-like object).
apply is very similar to call(), except for the type of arguments it supports. You use an arguments array instead of a list of arguments (parameters).
For further details: https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Function/apply
<meta charset="utf-8" />I think here is the most difficult part of the article, because first of all, we have to apply
our constructor as bind
function this
keyword (similarly to ctorFunc.bind
). Secondly, we have to pass the shifted right by 1 constructor arguments using ctorArgs.unshift(null)
to bind
function (as we remember, the first argument of the bind function will be used as this
function's keyword).
The bind
function is not accessible inside the Create
, because the window
object is used as the function context and that's why we have to access it using Function.prototype
.
Finally, we get the next universal instantiating function:
function Create(ctorFunc, ctorArgs) {
ctorArgs.unshift(null);
return new (Function.prototype.bind.apply(ctorFunc, ctorArgs ));
}
console.log( Create(Animal, ['Dog', 'Woof']) );
console.log( Create(Human, ['Person', 'John', 'Engineer', 'Moscow']) );
If we return to angularJS, we will notice that Animal
and Human
are used precisely as factory constructors, and their array arguments are represented by found and resolved dependencies.
angular
.module('app')
.factory(function($scope) {
});
or:
angular
.module('app')
.factory(['$scope', function($scope) {
}]);
All we have to do at the final stage of implementing our own $injector.instantiate
method is to find out the instantiating instance constructor, receive (as possible resolve) the required arguments and that's it. :)
Feel free to send comments, vote and thank you for reading. Hope to see you in the next articles.