コンピュータープログラミングの世界は、実際には、単純な部分を常に抽象化し、それらの抽象概念を整理するプロセスです。 JavaScriptも例外ではありません。JavaScriptを使用してアプリケーションを作成するとき、私たちは皆、有名なオープンソースライブラリやフレームワークなど、他の人によって書かれたコードを使用しているのでしょうか。プロジェクトが成長するにつれて、依存する必要のあるモジュールがますます増えており、現時点では、これらのモジュールを効果的に編成する方法が非常に重要な問題となっています。 依存性注入 は、コードに依存するモジュールを効果的に編成する方法の問題を解決します。有名な フロントエンド フレームワークAngularJS など、一部のフレームワークやライブラリで「依存性注入」という用語を聞いたことがあるかもしれません。依存性注入は非常に重要な機能の 1 つです。ただし、依存関係注入はまったく新しいものではなく、PHP などの他の プログラミング言語 には以前から存在していました。同時に、依存関係の注入は想像されているほど複雑ではありません。この記事では、JavaScript における依存性注入の概念を学び、「依存性注入スタイル」のコードの書き方をわかりやすく説明します。
今、2 つのモジュールがあるとしましょう。最初のモジュールは Ajax リクエストの送信に使用され、2 番目のモジュールは ルーター として使用されます。
var service = function() { return { name: 'Service' }; } var router = function() { return { name: 'Router' }; }
この時点で、上記の 2 つのモジュールを使用する必要がある function を作成しました。
var doSomething = function(other) { var s = service(); var r = router(); };
ここで、コードをより興味深いものにするために、このパラメータはさらにいくつかのパラメータを受け取る必要があります。もちろん、上記のコードを完全に使用することはできますが、上記のコードはどの面から見ても柔軟性が若干劣ります。使用する必要があるモジュールの名前が Service<a href="http://www.php.cn/wiki/1527.html" target="_blank">XML<code>Service<a href="http://www.php.cn/wiki/1527.html" target="_blank">XML</a>
或者Service<a href="http://www.php.cn/wiki/1488.html" target="_blank">JSON</a>
该怎么办?或者说如果我们基于测试的目的想要去使用一些假的模块改怎么办。这时,我们不能仅仅去编辑函数本身。因此我们需要做的第一件事情就是将依赖的模块作为参数传递给函数,代码如下所示:
var doSomething = function(service, router, other) { var s = service(); var r = router(); };
在上面的代码中,我们完全传递了我们所需要的模块。但是这又带来了一个新的问题。假设我们在代码的哥哥部分都调用了doSomething
方法。这时,如果我们需要第三个依赖项该怎么办。这个时候,去编辑所有的函数调用代码并不是一个明智的方法。因此,我们需要一段代码来帮助我们做这件事情。这就是依赖注入器试图去解决的问题。现在我们可以来定下我们的目标了:
我们应该能够去注册依赖项
依赖注入器应该接收一个函数,然后返回一个能够获取所需资源的函数
代码不应该复杂,而应该简单友好
依赖注入器应该保持传递的函数作用域
传递的函数应该能够接收自定义的参数,而不仅仅是被描述的依赖项
或许你已经听说过了大名鼎鼎的requirejs,它是一个能够很好的解决依赖注入问题的库:
define(['service', 'router'], function(service, router) { // ... });
requirejs的思想是首先我们应该去描述所需要的模块,然后编写你自己的函数。其中,参数的顺序很重要。假设我们需要编写一个叫做injector
的模块,它能够实现类似的语法。
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
的函数体中我们使用了expect.js这个断言库来确保代码的正确性。这里有一点类似TDD(测试驱动开发)的思想。
现在我们正式开始编写我们的injector
または < になった場合code> サービスJSON
var injector = { dependencies: {}, register: function(key, value) { this.dependencies[key] = value; }, resolve: function(deps, func, scope) { } }
doSomething
メソッドを呼び出すとします。この時点で、3 番目の依存関係が必要な場合はどうなるでしょうか。現時点では、すべての関数呼び出しコードを編集するのは賢明な考えではありません。したがって、これを行うにはコードが必要です。これは、依存性インジェクターが解決しようとしている問題です。これで目標を設定できます: 🎜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))); } }
injector
というモジュールを作成する必要があるとします。 🎜"function (service, router, other) { var s = service(); var r = router(); }"
doSomething
の関数本体では、コードの正確性を保証するために Expect.js アサーション ライブラリを使用しているということです。 TDD (テスト駆動🎜開発)の考え方に似たものがここにあります。 🎜🎜ここで、injector
モジュールの作成を正式に開始します。まず、アプリケーションのすべての部分で同じ機能を持たせるために、モノリスである必要があります。 🎜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="http://www.php.cn/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中依赖注入的方法,希望本文能够帮助你开始使用依赖注入这个技巧,并且写出依赖注入风格的代码。
以上がJavaScript の依存関係注入サンプル コードの詳細な分析の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。