The world of computer programming is actually a process of constantly abstracting simple parts and organizing these abstractions. JavaScript is no exception. When we use JavaScript to write applications, do we use code written by others, such as some famous open source libraries or frameworks. As our projects grow, more and more modules we need to rely on. At this time, how to effectively organize these modules has become a very important issue. Dependency injection solves the problem of how to effectively organize code dependency modules. You may have heard of the term "dependency injection" in some frameworks or libraries, such as the famous front-end framework AngularJS. Dependency injection is one of the very important features. However, dependency injection is nothing new at all, it has been around for a long time in other programming languages such as PHP. 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 in an easy-to-understand way how to write "dependency injection style" code.
Target setting
Suppose we have two modules now. The function of the first module is to send Ajax requests, while the function of the second module is to use as a routing.
The code copy is as follows:
var service = function() {
return { name: 'Service' };
}
var router = function() {
return { name: 'Router' };
}
At this time, we wrote a function that needs to use the two modules mentioned above:
The code copy is as follows:
var doSomething = function(other) {
var s = service();
var r = router();
};
Here, in order to make our code more interesting, this parameter needs to receive several more parameters. Of course, we can use the above code, but no matter from any aspect, the above code seems a little less flexible. What should we do if the module name we need to use becomes ServiceXML or ServiceJSON? Or what if we want to use some fake modules for testing purposes? At this time, we cannot just edit the function itself. Therefore, the first thing we need to do is to pass the dependent module as parameters to the function, the code is as follows:
The code copy is as follows:
var doSomething = function(service, router, other) {
var s = service();
var r = router();
};
In the above code, we pass the modules we need completely. But this brings up a new problem. Suppose we call the doSomething method in the brother part of the code. At this time, what should we do if we need a third dependency. At this time, it is not a wise way to edit all the function call code. So we need a piece of code to help us do this. This is the problem that the dependency injector is trying 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 then return a function that can obtain the required resources
3. The code should not be complicated, but should 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 the described dependencies
requirejs/AMD method
Perhaps you have heard of the famous requirejs, which is a library that can solve the dependency injection problem well:
The code copy is as follows:
define(['service', 'router'], function(service, router) {
// ...
});
The idea of requirejs is that first we should describe the required modules and then write your own functions. Among them, the order of parameters is very important. Suppose we need to write a module called injector that can implement similar syntax.
The code copy is as follows:
var doSomething = injector.resolve(['service', 'router'], function(service, router, other) {
expect(service().name).to.be('Service');
expect(router().name).to.be('Router');
expect(other).to.be('Other');
});
doSomething("Other");
Before continuing, one thing to note is that in the function body of doSomething, we use the assertion library expect.js to ensure the correctness of the code. Here is a bit similar to TDD (test-driven development).
Now we officially start writing our injector module. First it should be a monomer so that it can have the same functionality in every part of our application.
The code copy is as follows:
var injector = {
dependencies: {},
register: function(key, value) {
this.dependencies[key] = value;
},
resolve: function(deps, func, scope) {
}
}
This object is very simple, with only two functions and a variable for storage purposes. What we need to do is check the deps array and then look for the answer in the dependencies variables. The rest is to use the .apply method to call the func variable we passed:
The code copy 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)));
}
}
If you need to specify a scope, the above code will also run normally.
In the above code, the role of Array.prototype.slice.call(arguments, 0) is to convert the arguments variable into a real array. So far, our code has passed the test perfectly. But the problem here is that we have to write the required modules twice, and we cannot arrange them in arbitrarily. The extra parameters are always followed by all dependencies.
Reflection method
According to the explanation in Wikipedia, reflection refers to the fact that an object can modify its own structure and behavior during the run. In JavaScript, simply put it is the ability to read the source code of an object and analyze the source code. Or go back to our doSomething method, if you call the doSomething.toString() method, you can get the following string:
The code copy is as follows:
"function (service, router, other) {
var s = service();
var r = router();
}"
In this way, as long as we use this method, we can easily get 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 copy is as follows:
/^function/s*[^/(]*/(/s*([^/)]*)/)/m
We can modify the resolve method to the following code:
The code copy 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);
}
}
We use the above regular expression to match the function we defined, and we can get the following result:
The code copy is as follows:
["function (service, router, other)", "service, router, other"]
At this point, we only need the second item. But once we remove the extra spaces and slice the string with, we get the deps array. The following code is the part we modified:
The code copy is as follows:
var a = Array.prototype.slice.call(arguments, 0);
...
args.push(self.dependencies[d] && d != '' ? self.dependencies[d] : a.shift());
In the above code, we traverse the dependency project, if there are missing items in it, if there are missing parts in the dependency project, we get it from the arguments object. If an array is an empty array, using the shift method will only return undefined without throwing an error. So far, the new version of injector looks like this:
The code copy is as follows:
var doSomething = injector.resolve(function(service, other, router) {
expect(service().name).to.be('Service');
expect(router().name).to.be('Router');
expect(other).to.be('Other');
});
doSomething("Other");
In the above code, we can confuse the order of dependencies at will.
But nothing is perfect. There is a very serious problem with the dependency injection of reflection methods. An error occurs when the code is simplified. This is because during the simplification of the code, the name of the parameter changes, which will cause the dependencies to be resolved. For example:
The code copy is as follows:
var doSomething=function(e,t,n){var r=e();var i=t()}
So we need the following solution, like in AngularJS:
The code copy is as follows:
var doSomething = injector.resolve(['service', 'router', function(service, router) {
}]);
This is very similar to the AMD solution I saw at the beginning, so we can integrate the above two methods, and the final code is as follows:
The code copy 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);
}
}
}
This version of the resolve method can accept two or three parameters. Here is a test code:
The code copy is as follows:
var doSomething = injector.resolve('router,,service', function(a, b, c) {
expect(a().name).to.be('Router');
expect(b).to.be('Other');
expect(c().name).to.be('Service');
});
doSomething("Other");
You may have noticed that there is nothing between two commas, and that's not a mistake. This vacancy is left for the Other parameter. This is how we control the order of parameters.
Conclusion
In the above content, we introduced several methods of dependency injection in JavaScript. I hope this article can help you start using the dependency injection technique and write dependency injection style code.