This article mainly introduces dependency injection in JavaScript. This article describes the requirejsAMD method and reflection method, A friend who needs it can refer to the world of computer programming as a process of constantly abstracting and organizing simple parts. JavaScript is no exception. When we use JavaScript to write applications, do we always use code written by others, such as some famous open-source libraries or frameworks. As our projects grow, more and more modules are needed. At this time, how to effectively organize these modules becomes a very important issue. Dependency injection solves the problem of how to effectively organize code dependency modules. You may have heard of the word "dependency injection" in some frameworks or libraries. For example, the famous front-end framework AngularJS, dependency injection is a very important feature. However, dependency injection is nothing new at all. It has existed in other programming languages such as PHP for a long time. At the same time, dependency injection is not as complicated as imagined. In this article, we will learn the concept of dependency injection in JavaScript and explain how to compile the "dependency injection style" code in a simple way.
Target Setting
Suppose we have two modules. The first module is used to send Ajax requests, while the second module is used as a route.
The Code is as follows:
Var service = function (){
Return {name: 'service '};
}
Var router = function (){
Return {name: 'router '};
}
At this time, we have compiled a function that uses the two modules mentioned above:
The Code is as follows:
Var doSomething = function (other ){
Var s = service ();
Var r = router ();
};
Here, to make our code more interesting, this parameter needs to receive several more parameters. Of course, we can use the above Code, but the above Code is slightly less flexible in any aspect. What if the module name we need is changed to ServiceXML or ServiceJSON? Or what if we want to use fake modules for testing purposes. In this case, we cannot just edit the function itself. Therefore, the first thing we need to do is to pass the dependent module as a parameter to the function. The Code is as follows:
The Code is as follows:
Var doSomething = function (service, router, other ){
Var s = service ();
Var r = router ();
};
In the above Code, we completely passed the required modules. However, this brings about a new problem. Suppose we call the doSomething method in the Code's brother. What should we do if we need the third dependency. In this case, it is not wise to edit all function call code. Therefore, we need a piece of code to help us do this. This is the problem that the dependency injector tries to solve. Now we can set our goals:
1. We should be able to register Dependencies
2. The dependency injector should receive a function and return a function that can obtain the required resources.
3. The code should not be complex, but be simple and friendly.
4. The dependency injector should maintain the passed function scope.
5. The passed function should be able to receive custom parameters, not just described dependencies.
Requirejs/AMD Method
You may have heard of the well-known requirejs, which is a library that can effectively solve the dependency injection problem:
The Code is as follows:
Define (['service', 'router '], function (service, router ){
//...
});
The idea of requirejs is that we should first describe the required modules and then compile your own functions. The order of parameters is important. Suppose we need to write a module called injector, which can implement similar syntax.
The Code is as follows:
Var doSomething = injector. resolve (['service', 'router '], function (service, router, other ){
CT (service (). name). to. be ('service ');
CT (router (). name). to. be ('router ');
Except CT (other). to. be ('other ');
});
DoSomething ("Other ");
Before proceeding, we must note that in the doSomething function body, we use the assert library of objective CT. js to ensure code correctness. Here is a bit similar to TDD (test-driven development.
Now we are writing our injector module. First, it should be a single entity so that it can have the same function in all parts of our application.
The Code is as follows:
Var injector = {
Dependencies :{},
Register: function (key, value ){
This. dependencies [key] = value;
},
Resolve: function (deps, func, scope ){
}
}
This object is very simple. It only contains two functions and one variable for storage purposes. What we need to do is to check the deps array and then find the answer in the dependencies variable. The remaining part is to use the. apply method to call the passed func variable:
The Code is as follows:
Resolve: function (deps, func, scope ){
Var args = [];
For (var I = 0; I If (this. dependencies [d]) {
Args. push (this. dependencies [d]);
} Else {
Throw new Error ('can \'t resolve' + d );
}
}
Return function (){
Func. apply (scope | |{}, args. concat (Array. prototype. slice. call (arguments, 0 )));
}
}
If you need to specify a scope, the above Code can also run normally.
In the above Code, Array. prototype. slice. call (arguments, 0) is used to convert the arguments variable into a real Array. So far, our code can pass the test perfectly. However, the problem here is that we have to write the required modules twice, and we cannot arrange them in order at will. Additional parameters are always placed after all dependencies.
Reflection (reflection) Method
According to Wikipedia, reflection means that a program can modify its own structure and behavior during running. In JavaScript, you can simply read the source code of an object and analyze the source code. Return to our doSomething method. If you call doSomething. toString (), you can obtain the following string:
The Code is as follows:
"Function (service, router, other ){
Var s = service ();
Var r = router ();
}"
In this way, we can easily obtain the parameters we want and, more importantly, their names. This is also the method used by AngularJS to implement dependency injection. In AngularJS code, we can see the following regular expression:
The Code is as follows:
/^ Function \ s * [^ \ (] * \ (\ s * ([^ \)] *) \)/m
We can change the resolve Method to the following code:
The Code is as follows:
Resolve: function (){
Var func, deps, scope, args = [], self = this;
Func = arguments [0];
Deps = func. toString (). match (/^ function \ s * [^ \ (] * \ (\ s * ([^ \)] *) \)/m) [1]. replace (// g ,''). split (',');
Scope = arguments [1] || {};
Return function (){
Var a = Array. prototype. slice. call (arguments, 0 );
For (var I = 0; I Var d = deps [I];
Args. push (self. dependencies [d] & d! = ''? Self. dependencies [d]: a. shift ());
}
Func. apply (scope | |{}, args );
}
}
We use the above regular expression to match our defined function. We can get the following results:
The Code is as follows:
["Function (service, router, other)", "service, router, other"]
In this case, we only need the second item. However, once we remove unnecessary spaces and split the string with the extra space, we get the deps array. The following code is the modified part:
The Code is as follows:
Var a = Array. prototype. slice. call (arguments, 0 );
...
Args. push (self. dependencies [d] & d! = ''? Self. dependencies [d]: a. shift ());
In the code above, we traverse the dependent project. If there is a missing project, if there is a missing part in the dependent project, we will get it from the arguments object. If an array is an empty array, shift will only return undefined without throwing an error. So far, the New injector version looks as follows:
The Code is as follows:
Var doSomething = injector. resolve (function (service, other, router ){
CT (service (). name). to. be ('service ');
CT (router (). name). to. be ('router ');
Except CT (other). to. be ('other ');
});
DoSomething ("Other ");
In the above code, we can confuse the order of dependencies at will.
However, nothing is perfect. Dependency injection of reflection methods has a very serious problem. An error occurs when the code is simplified. This is because the parameter name has changed during the code simplification process, which will cause the dependency item to fail to be parsed. For example:
The Code is as follows:
Var doSomething = function (e, t, n) {var r = e (); var I = t ()}
Therefore, we need the following solution, as in AngularJS:
The Code is as follows:
Var doSomething = injector. resolve (['service', 'router ', function (service, router ){
}]);
This is similar to the AMD solution we saw at the beginning, so we can integrate the above two methods. The final code is as follows:
The Code is as follows:
Var injector = {
Dependencies :{},
Register: function (key, value ){
This. dependencies [key] = value;
},
Resolve: function (){
Var func, deps, scope, args = [], self = this;
If (typeof arguments [0] === 'string '){
Func = arguments [1];
Deps = arguments [0]. replace (// g, ''). split (',');
Scope = arguments [2] || {};
} Else {
Func = arguments [0];
Deps = func. toString (). match (/^ function \ s * [^ \ (] * \ (\ s * ([^ \)] *) \)/m) [1]. replace (// g ,''). split (',');
Scope = arguments [1] || {};
}
Return function (){
Var a = Array. prototype. slice. call (arguments, 0 );
For (var I = 0; I Var d = deps [I];
Args. push (self. dependencies [d] & d! = ''? Self. dependencies [d]: a. shift ());
}
Func. apply (scope | |{}, args );
}
}
}
In this version, the resolve method can accept two or three parameters. The following is a test code:
The Code is as follows:
Var doSomething = injector. resolve ('router, service', function (a, B, c ){
CT (a (). name). to. be ('router ');
CT (B). to. be ('other ');
CT (c (). name). to. be ('service ');
});
DoSomething ("Other ");
You may have noticed that there is nothing between the two commas. This is not an error. This vacancy is left to the Other parameter. This is the method for controlling the Parameter order.
Conclusion
In the above content, we have introduced several methods of dependency injection in JavaScript. I hope this article will help you start to use the dependency injection technique and write code in the dependency injection style.