Universal function for creating objects using the example of $ injector.instantiate implementation in angularjs

    Have you ever wondered how to instantiate the angularJS types you use? Controllers, factories, services, decorators, values ​​- literally each of them is ultimately passed for execution to the instantiate function of the object $injector, where they are waiting for a rather interesting design, which I would like to talk about today.


    Namely, we will talk about the following line:

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

    Is the principle of its action obvious to you right away? If the answer is yes, then I thank you for your attention and the time you took :)

    Now, when all the readers who have eaten the dog on javascript have left us, I would like to answer my own question: when I first saw this line, I was confused and absolutely did not understand anything in all these relationships and the intricacies of functions bind, apply, newand (). Let's get it right. I suggest starting from the opposite, namely: let us have some parameterized constructor, an instance of which we want to create:

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

    new


    "What could be easier," - you say and will be right: var dog = new Animal('Dog', 'Woof!');. A statement newis the first thing we need to get an instance of a constructor call Animal. 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 Foo function 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 new expression is the object returned by the constructor. If the constructor does not return the object explicitly, the object from step 1 is used. (Usually, designers do not return a value, but they can do this if you need to override the normal process of creating objects.)
    More

    Great, now let's wrap our constructor call Animalin a function so that the initialization code is common to all 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 that, depending on the type we require, will itself create the necessary instance;
    2. Pass the constructor function as a parameter and based on it create a new one with the arguments passed to it that are already attached to it (with which the function helps us perfectly bind).

    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 it works bind:

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

    In our case, we pass as a context null, because We plan to use the new bindfunction created using the operator new, which ignores thisand creates an empty object for it. The result of executing the bind function will be a new function with arguments already bound to it (i.e. return new fn;, where fnis the result of the call bind).

    Well, now we can use our function to create any animals and people whose designers ... take parameters nameand sound. “But not all the arguments that are required for animals will be necessary for people,” you say and you’ll be right, 2 problems are brewing:

    1. The arguments of the constructors can begin to change (for example, the order or their number), which means that we will need to make changes in several places at once: in the signatures of the constructors, function call lines, Createand instance creation line return new (ctorFunc.bind(null, name, sound ));
    2. The more constructors we have, the greater 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 out only the necessary ones).

    apply


    The solution to these problems can be through passing the arguments from the creation function directly to the constructor, in other words, a universal function that takes the constructor and the necessary array of arguments and returns a new function to which these arguments are bound. There is a wonderful function in javascript for this apply(or its analogue call, if the number of arguments is known in advance).

    A small digression about how apply works:

    The apply () method calls a function with the specified this value and 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 the two is that the call () function takes a list (list) of arguments, while the apply () function takes an array of arguments (as a single parameter).
    More details

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



    The function is bindnot available in the execution context Createsince it is an object window, but is accessible through a 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 notice that as Animaland Human, for example, there are designers of factories or other types, and as an array of arguments ['Dog', 'Woof']are found (resolved) by the name of the dependency:

    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 method $injector.instantiateis to find the constructor function and get the necessary arguments, and you can create :)

    Also popular now: