原本打算寫篇文章介紹下控制反轉的常見模式-依賴注入。在翻看資料的時候,發現了一篇好文Dependency injection in JavaScript,就不本身折騰了,結合本身理解翻譯一下,好文共賞。javascript
我喜歡引用這樣一句話‘編程是對複雜性的管理’。可能你也聽過計算機世界是一個巨大的抽象結構。咱們簡單的包裝東西並重復的生產新的工具。思考那麼一下下,咱們使用的編程語言都包括內置的功能,這些功能多是基於其餘低級操做的抽象方法,包括咱們是用的javascript。
早晚,咱們都會須要使用別的開發者開發的抽象功能,也就是咱們要依賴其餘人的代碼。
我但願使用沒有依賴的模塊,顯然這是很難實現的。即便你建立了很好的像黑盒同樣的組件,但總有個將全部部分合並起來的地方。
這就是依賴注入起做用的地方,當前來看,高效管理依賴的能力是迫切須要的,本文總結了原做者對這個問題的見解。html
假設咱們有兩個模塊,一個是發出ajax請求的服務,一個是路由:java
var service = function() { return { name: 'Service' }; } var router = function() { return { name: 'Router' }; }
下面是另外一個依賴了上述模塊的函數:ajax
var doSomething = function(other) { var s = service(); var r = router(); };
爲了更有趣一點,該函數須要接受一個參數。固然咱們可使用上面的代碼,可是這不太靈活。
若是咱們想使用ServiceXML、ServiceJSON,或者咱們想要mock一些測試模塊,這樣咱們不能每次都是編輯函數體。
爲了解決這個現狀,首先咱們提出將依賴當作參數傳給函數,以下:正則表達式
var doSomething = function(service, router, other) { var s = service(); var r = router(); };
這樣,咱們把須要的模塊的具體實例傳遞過來。
然而這樣有個新的問題:想一下若是dosomething函數在不少地方被調用,若是有第三個依賴條件,咱們不能改變全部的調用doSomething的地方。
舉個小栗子:
假如咱們有不少地方用到了doSomething:編程
//a.js var a = doSomething(service,router,1) //b.js var b = doSomething(service,router,2) // 假如依賴條件更改了,即doSomething須要第三個依賴,才能正常工做 // 這時候就須要在上面不一樣文件中修改了,若是文件數量夠多,就不合適了。 var doSomething = function(service, router, third,thother) { var s = service(); var r = router(); //*** };
所以,咱們須要一個幫助咱們來管理依賴的工具。這就是依賴注入器想要解決的問題,先看一下咱們想要達到的目標:數組
能夠註冊依賴
注入器應該接受一個函數而且返回一個已經得到須要資源的函數
咱們不該該寫複雜的代碼,須要簡短優雅的語法
注入器應該保持傳入函數的做用域
被傳入的函數應該能夠接受自定義參數,不只僅是被描述的依賴。
看起來比較完美的列表就如上了,讓咱們來嘗試實現它。app
requirejs/AMD的方式
你們均可能據說過requirejs,它是很不錯的依賴管理方案。編程語言
define(['service', 'router'], function(service, router) { // ... });
這種思路是首先聲明須要的依賴,而後開始編寫函數。這裏參數的順序是很重要的。咱們來試試寫一個名爲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))); } }
若是scope存在,是能夠被有效傳遞的。Array.prototype.slice.call(arguments, 0)
將arguments(類數組)轉換成真正的數組。
目前來看很不錯的,能夠經過測試。當前的問題時,咱們必須寫兩次須要的依賴,而且順序不可變更,額外的參數只能在最後面。
從維基百科來講,反射是程序在運行時能夠檢查和修改對象結構和行爲的一種能力。
簡而言之,在js的上下文中,是指讀取而且分析對象或者函數的源碼。看下開頭的doSomething,若是使用doSomething.toString() 能夠獲得下面的結果。
function (service, router, other) { var s = service(); var r = router(); }
這種將函數轉成字符串的方式賦予咱們獲取預期參數的能力。而且更重要的是,他們的name。
下面是Angular依賴注入的實現方式,我從Angular那拿了點能夠獲取arguments的正則表達式:
/^function\s*[^\(]*\(\s*([^\)]*)\)/m
這樣咱們能夠修改resolve方法了:
這裏,我將測試例子拿上來應該更好理解一點。
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");
繼續來看咱們的實現。
resolve: function() { // agrs 傳給func的參數數組,包括依賴模塊及自定義參數 var func, deps, scope, args = [], self = this; // 獲取傳入的func,主要是爲了下面來拆分字符串 func = arguments[0]; // 正則拆分,獲取依賴模塊的數組 deps = func.toString().match(/^functions*[^(]*(s*([^)]*))/m)[1].replace(/ /g, '').split(','); //待綁定做用域,不存在則不指定 scope = arguments[1] || {}; return function() { // 將arguments轉爲數組 // 即後面再次調用的時候,doSomething("Other"); // 這裏的Other就是a,用來補充缺失的模塊。 var a = Array.prototype.slice.call(arguments, 0); //循環依賴模塊數組 for(var i=0; i<deps.length; i++) { var d = deps[i]; // 依賴隊列中模塊存在且不爲空的話,push進參數數組中。 // 依賴隊列中不存在對應模塊的話從a中取第一個元素push進去(shift以後,數組在改變) args.push(self.dependencies[d] && d != '' ? self.dependencies[d] : a.shift()); } //依賴當作參數傳入 func.apply(scope || {}, args); } }
使用這個正則來處理函數時,能夠獲得下面結果:
["function (service, router, other)", "service, router, other"]
咱們須要的只是第二項,一旦咱們清除數組並拆分字符串,咱們將會獲得依賴數組。主要變化在下面:
var a = Array.prototype.slice.call(arguments, 0);
...
args.push(self.dependencies[d] && d != '' ? self.dependencies[d] : a.shift());
這樣咱們就循環遍歷依賴項,若是缺乏某些東西,咱們能夠嘗試從arguments對象中獲取。
幸虧,當數組爲空的時候shift方法也只是返回undefined而非拋錯。因此新版的用法以下:
//不用在前面聲明依賴模塊了
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");
這樣就不用重複聲明瞭,順序也可變。咱們複製了Angular的魔力。
然而,這並不完美,壓縮會破壞咱們的邏輯,這是反射注入的一大問題。由於壓縮改變了參數的名稱因此咱們沒有能力去解決這些依賴。例如:
// 顯然根據key來匹配就是有問題的了
var doSomething=function(e,t,n){var r=e();var i=t()}
Angular團隊的解決方案以下:
var doSomething = injector.resolve(['service', 'router', function(service, router) {
}]);
看起來就和開始的require.js的方式同樣了。做者我的不能找到更優的解決方案,爲了適應這兩種方式。最終方案看起來以下:
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接受兩或者三個參數,若是是兩個就是咱們寫的第一種了,若是是三個,會將第一個參數解析並填充到deps。
下面就是測試例子(我一直認爲將這段例子放在前面可能你們更好閱讀一些。):
// 缺失了一項模塊other 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'); }); // 這裏傳的Other將會用來拼湊 doSomething("Other");
可能會注意到argumets[0]
中確實了一項,就是爲了測試填充功能的。
直接注入做用域
有時候,咱們使用第三種的注入方式,它涉及到函數做用域的操做(或者其餘名字,this對象),並不常用
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上 //這樣就能夠直接在函數做用域中調用了 scope[d] = this.dependencies[d]; } else { throw new Error('Can\'t resolve ' + d); } } return function() { func.apply(scope || {}, Array.prototype.slice.call(arguments, 0)); } } }
咱們作的就是將依賴加到做用域上,這樣的好處是不用再參數里加依賴了,已是函數做用域的一部分了。
var doSomething = injector.resolve(['service', 'router'], function(other) { expect(this.service().name).to.be('Service'); expect(this.router().name).to.be('Router'); expect(other).to.be('Other'); }); doSomething("Other");
結束語
依賴注入是咱們全部人都作過的事情中的一種,可能沒有意識到罷了。即便沒有聽過,你也可能用過不少次了。
經過這篇文章對於這個熟悉而又陌生的概念的瞭解加深了很多,但願能幫助到有須要的同窗。最後我的能力有限,翻譯有誤的地方歡迎你們指出,共同進步。
再次感謝原文做者原文地址
如水穿石,厚積纔可薄發