In-depth understanding of dependency injection in Javascript

Source: Internet
Author: User


Sooner or later, you need to use the abstract results of other developers-that is, you rely on others' code. I like to rely on free (no dependency) modules, but that is hard to implement. Even the beautiful black box components you have created will depend more or less on something. This is exactly what dependency injection shows. It is absolutely necessary to effectively manage dependencies. This article summarizes my exploration of the problem and some solutions.

I. Objectives
Suppose we have two modules. The first is the Ajax request service, and the second is the router ).
Copy codeThe Code is as follows:
Var service = function (){
Return {name: 'service '};
}
Var router = function (){
Return {name: 'router '};
}
Another function needs to use these two modules.
Copy codeThe Code is as follows:
Var doSomething = function (other ){
Var s = service ();
Var r = router ();
};
To make it look more interesting, this function accepts a parameter. Of course, we can use the above Code, but this is obviously not flexible enough. If we want to use ServiceXML or ServiceJSON, or if we need some test modules. We cannot solve the problem by editing the function body. First, we can solve the dependency through the function parameters. That is:
Copy codeThe Code is as follows:
Var doSomething = function (service, router, other ){
Var s = service ();
Var r = router ();
};
We implement the functions we want by passing additional parameters. However, this will bring new problems. Imagine if our doSomething method is scattered in our code. If we need to change the dependency condition, we cannot change all the files that call the function.

We need a tool that can help us solve these problems. This is the problem solved by dependency injection. Let's write down some goals that our dependency injection solution should achieve:

We should be able to register Dependencies
1. The injection should accept a function and return a function we need.
2. We cannot write too many things-we need to streamline the beautiful syntax
3. injection should keep the scope of the passed Function
4. The passed function should be able to accept custom parameters, not just dependency descriptions.
5. A perfect list. Let's implement it below.
Iii. RequireJS/AMD Method
You may have heard about RequireJS, which is a good choice for solving dependency injection.
Copy codeThe Code is as follows:
Define (['service', 'router '], function (service, router ){
//...
});
This idea is to first describe the required dependencies and then write your function. The order of parameters is very important. As mentioned above, let's write a module called injector, which can accept the same syntax.
Copy codeThe 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, I should explain the content of the doSomething function body clearly. I use reverse CT. js (assertion Library) is only used to ensure that the behavior of the code I write is the same as what I expected, reflecting a little TDD (test-driven development) method.
Next we start our injector module, which is a great Singleton mode, so it can work well in different parts of our program.
Copy codeThe Code is as follows:
Var injector = {
Dependencies :{},
Register: function (key, value ){
This. dependencies [key] = value;
},
Resolve: function (deps, func, scope ){

}
}
This is a very simple object. There are two methods, one for storing attributes. All we need to do is check the deps array and search for the answer in the dependencies variable. The rest is to call the. apply method and pass the parameters of the previous func method.
Copy codeThe Code is as follows:
Resolve: function (deps, func, scope ){
Var args = [];
For (var I = 0; I <deps. length, d = deps [I]; 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 )));
}
}
Scope is optional. Array. prototype. slice. call (arguments, 0) is required to convert the arguments variable to a real Array. So far, it is not bad. Our test passed. The problem with this implementation is that we need to write the required parts twice and we cannot confuse their order. The additional custom parameters are always after the dependency.

Iv. Reflection Method
Definition reflection in Wikipedia refers to the ability of a program to check and modify the structure and behavior of an object at runtime. Simply put, in the context of JavaScript, this refers to the source code of the object or function that is read and analyzed. Let's complete the doSomething function mentioned at the beginning of the article. If you output doSomething. tostring () in the console (). You will get the following string:
Copy codeThe Code is as follows:
"Function (service, router, other ){
Var s = service ();
Var r = router ();
}"
The string returned by this method gives us the ability to traverse parameters. More importantly, they can be obtained. This is actually the method for Angular to implement its dependency injection. I am a little lazy and directly intercept the Regular Expression of parameters in Angular code.
Copy codeThe Code is as follows:
/^ Function \ s * [^ \ (] * \ (\ s * ([^ \)] *) \)/m
We can modify the resolve Code as follows:
Copy codeThe 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 <deps. length; I ++ ){
Var d = deps [I];
Args. push (self. dependencies [d] & d! = ''? Self. dependencies [d]: a. shift ());
}
Func. apply (scope | |{}, args );
}
}
The result of executing the regular expression is as follows:
Copy codeThe Code is as follows:
["Function (service, router, other)", "service, router, other"]
It seems that we only need the second item. Once space is clearly defined and the string is split, the deps array is obtained. There is only one major change:
Copy codeThe Code is as follows:
Var a = Array. prototype. slice. call (arguments, 0 );
...
Args. push (self. dependencies [d] & d! = ''? Self. dependencies [d]: a. shift ());
We traverse the dependencies array cyclically. If a missing item is found, we try to get it from the arguments object. Fortunately, when the array is empty, the shift method returns undefined instead of throwing an error (thanks to the web idea ). The new injector can be used as follows:
Copy codeThe 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 ");
There is no need to override dependencies and their order can be disrupted. It still works. We copied Angular magic.

However, this approach is not perfect, which is a very big problem for reflection injection. Compression destroys our logic because it changes the parameter name and we cannot maintain the correct ing relationship. For example, after doSometing () is compressed, it may look like this:
Copy codeThe Code is as follows:
Var doSomething = function (e, t, n) {var r = e (); var I = t ()}
The solution proposed by the Angular team looks like:

Var doSomething = injector. resolve (['service', 'router ', function (service, router ){

}]);
This looks like the solution at the beginning. I failed to find a better solution, so I decided to combine the two methods. The final version of injector is as follows.
Copy codeThe 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 <deps. length; I ++ ){
Var d = deps [I];
Args. push (self. dependencies [d] & d! = ''? Self. dependencies [d]: a. shift ());
}
Func. apply (scope | |{}, args );
}
}
}
The resolve guest accepts two or three parameters. If there are two parameters, they are actually the same as the one written earlier in the article. However, if there are three parameters, it will convert the first parameter and fill the deps array. Below is a test example:
Copy codeThe 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 notice that there are two commas after the first parameter-note that this is not a mistake. The null value actually represents the "Other" parameter (placeholder ). This shows how we control the Parameter order.

5. directly inject Scope
Sometimes I will use the third injection variable, which involves the scope of the operation function (in other words, this object ). Therefore, you do not need to use this variable.
Copy codeThe Code is as follows:
Var injector = {
Dependencies :{},
Register: function (key, value ){
This. dependencies [key] = value;
},
Resolve: function (deps, func, scope ){
Var args = [];
Scope = scope | {};
For (var I = 0; I <deps. length, d = deps [I]; I ++ ){
If (this. dependencies [d]) {
Scope [d] = this. dependencies [d];
} Else {
Throw new Error ('can \'t resolve' + d );
}
}
Return function (){
Func. apply (scope | |{}, Array. prototype. slice. call (arguments, 0 ));
}
}
}
All we do is add the dependency to the scope. The advantage of doing so is that developers no longer need to write dependency parameters; they are already part of the function scope.
Copy codeThe Code is as follows:
Var doSomething = injector. resolve (['service', 'router '], function (other ){
CT (this. service (). name). to. be ('service ');
CT (this. router (). name). to. be ('router ');
Except CT (other). to. be ('other ');
});
DoSomething ("Other ");
Vi. Conclusion
In fact, most of us have used dependency injection, but we are not aware of it. Even if you don't know the term, you may have used it millions of times in your code. I hope this article will help you better understand it.

Related Article

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.