The world of
ComputerProgramming 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 all use codes written by others, such as some famous open source libraries or frameworks . As our project grows, more and more modules we need to rely on become more and more important. 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 dependent modules. You may have heard the term "dependency injection" in some frameworks or libraries, such as the famous front-end frameworkAngularJS, where dependency injection is one of the very important features. However, dependency injection is nothing new at all. It has existed in other programming languagessuch 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 in a simple and easy way how to write "dependency injection style" code.
Suppose we now have two modules. The first module is used to send Ajax requests, while the second module is used as a router.
var service = function() { return { name: 'Service' }; } var router = function() { return { name: 'Router' }; }
At this time, we wrote a function, which requires the use of the two modules mentioned above:
var doSomething = function(other) { var s = service(); var r = router(); };
Here, in order to make our code What's interesting is that this parameter needs to receive a few more parameters. Of course, we can completely use the above code, but the above code is slightly less flexible from any aspect. What if the name of the module we need to use becomes Service<a href="//m.sbmmt.com/wiki/1527.html" target="_blank">XML</a>
or Service<a href="//m.sbmmt.com/wiki/1488.html" target="_blank">JSON</a>
? Or what if we want to use some fake modules for testing purposes. At this point, we can't just edit the function itself. So the first thing we need to do is to pass the dependent modules as parameters to the function. The code is as follows:
var doSomething = function(service, router, other) { var s = service(); var r = router(); };
In the above code, we pass exactly the modules we need. But this brings up a new problem. Suppose we call the doSomething
method in both parts of the code. At this point, what if we need a third dependency. At this time, it is not a wise idea to edit all the function call code. Therefore, we need a piece of code to help us do this. This is the problem that dependency injector tries to solve. Now we can set our goals:
We should be able to register dependencies
The dependency injector should receive a function, Then return a function that can obtain the required resources
The code should not be complex, but should be simple and friendly
The dependency injector should remain transitive Function Scope
The passed function should be able to receive custom parameters, not just the described dependencies
Perhaps you have heard of the famous requirejs, which is a library that can solve dependency injection problems very well:
define(['service', 'router'], function(service, router) { // ... });
injector that can implement similar syntax.
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");
doSomething we use the expect.js assertion library to ensure the correctness of the code. There is something similar to the idea of TDD (test
driven) here.
injector module. First it should be a monolith so that it has the same functionality in every part of our application.
var injector = { dependencies: {}, register: function(key, value) { this.dependencies[key] = value; }, resolve: function(deps, func, scope) { } }
这个对象非常的简单,其中只包含两个函数以及一个用于存储目的的变量。我们需要做的事情是检查deps
数组,然后在dependencies
变量种寻找答案。剩余的部分,则是使用.apply
方法去调用我们传递的func
变量:
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))); } }
如果你需要指定一个作用域,上面的代码也能够正常的运行。
在上面的代码中,Array.prototype.slice.call(arguments, 0)
的作用是将arguments
变量转换为一个真正的数组。到目前为止,我们的代码可以完美的通过测试。但是这里的问题是我们必须要将需要的模块写两次,而且不能够随意排列顺序。额外的参数总是排在所有的依赖项之后。
根据维基百科中的解释,反射(reflection)指的是程序可以在运行过程中,一个对象可以修改自己的结构和行为。在JavaScript中,简单来说就是阅读一个对象的源码并且分析源码的能力。还是回到我们的doSomething
方法,如果你调用doSomething.to<a href="//m.sbmmt.com/wiki/57.html" target="_blank">String</a>()
方法,你可以获得下面的字符串:
"function (service, router, other) { var s = service(); var r = router(); }"
这样一来,只要使用这个方法,我们就可以轻松的获取到我们想要的参数,以及更重要的一点就是他们的名字。这也是AngularJS实现依赖注入所使用的方法。在AngularJS的代码中,我们可以看到下面的正则表达式:
/^function\s*[^\(]*\(\s*([^\)]*)\)/m
我们可以将resolve
方法修改成如下所示的代码:
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); } }
我们使用上面的正则表达式去匹配我们定义的函数,我们可以获取到下面的结果:
["function (service, router, other)", "service, router, other"]
此时,我们只需要第二项。但是一旦我们去除了多余的空格并以,
来切分字符串以后,我们就得到了deps
数组。下面的代码就是我们进行修改的部分:
var a = Array.prototype.slice.call(arguments, 0); ... args.push(self.dependencies[d] && d != '' ? self.dependencies[d] : a.shift());
在上面的代码中,我们遍历了依赖项目,如果其中有缺失的项目,如果依赖项目中有缺失的部分,我们就从arguments
对象中获取。如果一个数组是空数组,那么使用shift
方法将只会返回undefined
,而不会抛出一个错误。到目前为止,新版本的injector
看起来如下所示:
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");
在上面的代码中,我们可以随意混淆依赖项的顺序。
但是,没有什么是完美的。反射方法的依赖注入存在一个非常严重的问题。当代码简化时,会发生错误。这是因为在代码简化的过程中,参数的名称发生了变化,这将导致依赖项无法解析。例如:
var doSomething=function(e,t,n){var r=e();var i=t()}
因此我们需要下面的解决方案,就像AngularJS中那样:
var doSomething = injector.resolve(['service', 'router', function(service, router) { }]);
这和最一开始看到的AMD的解决方案很类似,于是我们可以将上面两种方法整合起来,最终代码如下所示:
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); } } }
这一个版本的resolve
方法可以接受两个或者三个参数。下面是一段测试代码:
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");
你可能注意到了两个逗号之间什么都没有,这并不是错误。这个空缺是留给Other
这个参数的。这就是我们控制参数顺序的方法。
在上面的内容中,我们介绍了几种JavaScript中依赖注入的方法,希望本文能够帮助你开始使用依赖注入这个技巧,并且写出依赖注入风格的代码。
The above is the detailed content of Detailed analysis of dependency injection sample code in JavaScript. For more information, please follow other related articles on the PHP Chinese website!