General object initiating function by the example of $injector.instantiate implementation in angularjs

0
177

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:


  1. A new object is created, inheriting from Foo.prototype.
  2. 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.
  3. 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:

  1. Implement the factory which will initiate the required constructor instance itself depending on the required type;
  2. 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:

  1. 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));
  2. 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 theirs 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.

LEAVE A REPLY