電腦程式設計的世界其實就是一個將簡單的部分不斷抽象,並將這些抽象組織起來的過程。 JavaScript也不例外,當我們使用JavaScript編寫應用程式時,我們是否都會使用到別人寫的程式碼,例如一些著名的開源程式庫或框架 。隨著我們專案的成長,我們需要依賴的模組變得越來越多,這個時候,如何有效的組織這些模組就成了一個非常重要的問題。 依賴注入解決的正是如何有效組織程式碼依賴模組的問題。你可能在某些框架或庫種聽過「依賴注入」這個詞,比如說著名的前端框架AngularJS,依賴注入就是其中一個非常重要的特性。但是,依賴注入根本就不是什麼新鮮玩意,它在其他的程式語言例如PHP中已經存在已久。同時,依賴注入也沒有想像種那麼複雜。在本文中,我們將一起來學習JavaScript中的依賴注入的概念,深入淺出的講解如何寫「依賴注入風格」的程式碼。
假設我們現在擁有兩個模組。第一個模組的作用是發送Ajax請求,而第二個模組的作用則是用作路由。
var service = function() { return { name: 'Service' }; } var router = function() { return { name: 'Router' }; }
這時,我們寫了一個函數,它需要使用上面提到的兩個模組:
var doSomething = function(other) { var s = service(); var r = router(); };
在這裡,為了讓我們的程式碼變得有趣一些,這個參數需要多接收幾個參數。當然,我們完全可以使用上面的程式碼,但是無論從哪個方面來看上面的程式碼都略顯得不那麼靈活。要是我們需要使用的模組名稱變成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
模組。首先它應該是一個單體,以便它能夠在我們應用的各個部分都擁有相同的功能。
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中文網其他相關文章!