📜 ⬆️ ⬇️

The universal function of creating objects on the example of the implementation of $ injector.instantiate in angularjs

Have you ever wondered how instances of angularJS types you use are created? Controllers, factories, services, decorators, values ​​— literally every one of them is finally passed to execution in the instantiate function of the $injector , where they are awaited by a rather entertaining construct, which I would like to talk about today.


Namely, it will be about the following line:

 return new (Function.prototype.bind.apply(ctor, args))(); 

Is the principle of its action obvious to you at once? If the answer is yes, I thank you for your attention and time :)
')
Now, when all the readers who ate the javascript dog left us, I would like to answer my own question: when I first saw this line, I was confused and understood absolutely nothing in all these relationships and the intricacies of the bind , apply , new and () functions. Let's figure it out. I suggest starting from the reverse, namely: let us have a certain parameterized constructor, an instance of which we want to create:

 function Animal(name, sound) { this.name = name; this.sound = sound; } 

new


“What could be simpler,” you will say, and you will be right: var dog = new Animal('Dog', 'Woof!'); . The new operator is the first thing we need to get an instance of a call to the Animal constructor. A small digression on how new works:

When new Foo (...) is executed, the following happens:

1. A new object is created that inherits Foo.prototype.
2. The constructor is called - the function Foo with the specified arguments and this, bound to the newly created object. new Foo is equivalent to new Foo (), that is, if no arguments are specified, Foo is called without arguments.
3. The result of the expression new is the object returned by the constructor. If the constructor does not explicitly return an object, the object from item 1 is used. (Usually, constructors do not return a value, but they can do it if you need to override the normal process of creating objects.)
Read more

Great, now let's wrap our call to the Animal constructor in a function so that the initialization code is common to all the required calls:

 function CreateAnimal(name, sound) { return new Animal(name, sound); } 

Over time, we begin to want to create not only animals, but also people (I agree, the example is not the most successful), which means that we have at least 2 options:

  1. Implement a factory, which, depending on the type we require, will create the necessary instance itself;
  2. Pass the constructor function as a parameter and, on its basis, create a new one with the arguments already passed to it (with which the bind function helps us perfectly).

And in the case of $injector.instantiate , the second path 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') ); 

A small digression on how bind works:

The bind () method creates a new function that, when invoked, sets the value provided as the execution context for this. The method also passes a set of arguments that will be set before the arguments passed to the bound function when it is called.
Read more

In our case, we pass null as context. we plan to use the new bind function with the new operator, which ignores this and creates an empty object for it. The result of the bind function is the new function with the arguments already attached to it (i.e. return new fn; where fn is the result of the bind call).

Great, now we can use our function to create any animals and people whose constructors ... accept the name and sound parameters. “But after all, not all the arguments that are required for animals will be necessary for people,” you will say and you will be right, two problems are brewing:

  1. Arguments of constructors can begin to change (for example, the order or their number), which means we will need to make changes in several places at once: in the signatures of constructors, the call lines of the Create function and the instance creation string return new (ctorFunc.bind(null, name, sound )) ;
  2. The more constructors we have, the higher the likelihood that we will need different arguments to create them, and we will no longer be able to use a single function (or we will have to list all of them and fill only the necessary ones).

apply


The solution to these problems can be the pass-through of arguments from the creation function straight to the constructor, in other words, a universal function that accepts a constructor and the required array of arguments and returns a new function to which these arguments are attached. For this, javascript has a great function apply (or its analog call , if the number of arguments is known in advance).

A small digression on how apply works:

The apply () method calls a function with the specified value this and the arguments provided as an array (or an array-like object).

Although the syntax of this function is almost completely identical to the call () function, the fundamental difference between them is that the call () function accepts a list (s) of arguments, while the apply () function takes an array of arguments (with a single parameter).
Read more

Here begins, perhaps, the most difficult part, because we have to use apply to set the context for the bind function to our constructor (similar to ctorFunc.bind ), and as arguments to the bind function (not forgetting that the first argument is the context to be set), pass the array of constructor parameters shifted one position to the right using ctorArgs.unshift(null) .



The bind function is not available in the execution context of Create , since it is the window object, but it is accessible through the function prototype Function.prototype .

The final result will be the following universal 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']) ); 

Returning to angularJS, we can see that as Animal and Human , for example, constructors of factories or other types act, and arrays of arguments ['Dog', 'Woof'] are dependencies found (split) by name:

 angular .module('app') .factory(function($scope) { // constructor }); 

or

 angular .module('app') .factory(['$scope', function($scope) { // constructor }]); 

All that remains to be done to implement the full-fledged $injector.instantiate method is to find the constructor function and get the necessary arguments and create it :)

Source: https://habr.com/ru/post/330214/


All Articles