JavaScript裏的依賴注入

我喜歡引用這句話,「程序是對複雜性的管理」。計算機世界是一個巨大的抽象建築羣。咱們簡單的包裝一些東西而後發佈新工具,周而復始。如今思考下,你所使用的語言包括的一些內建的抽象函數或是低級操做符。這在JavaScript裏是同樣的。javascript

早晚你須要用到其餘開發人員的抽象成果——即你依靠別人的代碼。我喜歡依賴自由(無依賴)的模塊,但那是難以實現的。甚至你建立的那些漂亮的黑盒子組件也或多或少會依賴一些東西。這正是依賴注入大顯身手的之處。如今有效地管理依賴的能力是絕對必要的。本文總結了我對問題探索和一些的解決方案。html

目標

設想咱們有兩個模塊。第一個是負責Ajax請求服務(service),第二個是路由(router)。java

var service = function() {
    return { name: 'Service' };
}
var router = function() {
    return { name: 'Router' };
}

咱們有另外一個函數須要用到這兩個模塊。git

var doSomething = function(other) {
    var s = service();
    var r = router();
};

爲使看起來更有趣,這函數接受一個參數。固然,咱們徹底可使用上面的代碼,但這顯然不夠靈活。若是咱們想使用ServiceXML或ServiceJSON呢,或者若是咱們須要一些測試模塊呢。咱們不能僅靠編輯函數體來解決問題。首先,咱們能夠經過函數的參數來解決依賴性。即:angularjs

var doSomething = function(service, router, other) {
    var s = service();
    var r = router();
};

咱們經過傳遞額外的參數來實現咱們想要的功能,然而,這會帶來新的問題。想象若是咱們的doSomething 方法散落在咱們的代碼中。若是咱們須要更改依賴條件,咱們不可能更改全部調用函數的文件。github

咱們須要一個能幫咱們搞定這些的工具。這就是依賴注入嘗試解決的問題。讓咱們寫下一些咱們的依賴注入解決辦法應該達到的目標:web

  • 咱們應該可以註冊依賴關係
  • 注入應該接受一個函數,並返回一個咱們須要的函數
  • 咱們不能寫太多東西——咱們須要精簡漂亮的語法
  • 注入應該保持被傳遞函數的做用域
  • 被傳遞的函數應該可以接受自定義參數,而不只僅是依賴描述

堪稱完美的清單,下面 讓咱們實現它。正則表達式

RequireJS / AMD的方法

你可能對RequireJS早有耳聞,它是解決依賴注入不錯的選擇。數組

define(['service', 'router'], function(service, router) {       
    // ...
});

這種想法是先描述須要的依賴,而後再寫你的函數。這裏參數的順序很重要。如上所說,讓咱們寫一個叫作injector的模塊,能接受相同的語法。app

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變量轉換爲真正的數組。到目前爲止還不錯。咱們的測試經過了。這種實現的問題是,咱們須要寫所需部件兩次,而且咱們不能混淆他們的順序。附加的自定義參數老是位於依賴以後。

反射方法

根據維基百科的定義反射是指一個程序在運行時檢查和修改一個對象的結構和行爲的能力。簡單的說,在JavaScript的上下文裏,這具體指讀取和分析的對象或函數的源代碼。讓咱們完成文章開頭提到的doSomething函數。若是你在控制檯輸出doSomething.tostring()。你將獲得以下的字符串:

"function (service, router, other) {
    var s = service();
    var r = router();
}"

經過此方法返回的字符串給咱們遍歷參數的能力,更重要的是,可以獲取他們的名字。這實際上是Angular 實現它的依賴注入的方法。我偷了一點懶,直接截取Angular代碼中獲取參數的正則表達式。

/^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());

咱們循環遍歷dependencies數組,若是發現缺失項則嘗試從arguments對象中獲取。謝天謝地,當數組爲空時,shift方法只是返回undefined,而不是拋出一個錯誤(這得益於web的思想)。新版的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");

沒必要重寫依賴而且他們的順序能夠打亂。它仍然有效,咱們成功複製了Angular的魔法。

然而,這種作法並不完美,這就是反射類型注射一個很是大的問題。壓縮會破壞咱們的邏輯,由於它改變參數的名字,咱們將沒法保持正確的映射關係。例如,doSometing()壓縮後可能看起來像這樣:

var doSomething=function(e,t,n){var r=e();var i=t()}

Angular團隊提出的解決方案看起來像:

var doSomething = injector.resolve(['service', 'router', function(service, router) {

}]);

這看起來很像咱們開始時的解決方案。我沒能找到一個更好的解決方案,因此決定結合這兩種方法。下面是injector的最終版本。

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數組,下面是一個測試例子:

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」參數(佔位符)。這顯示了咱們是如何控制參數順序的。

直接注入Scope

有時我會用到第三個注入變量,它涉及到操做函數的做用域(換句話說,就是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[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");

結束語

其實咱們大部分人都用過依賴注入,只是咱們沒有意識到。即便你不知道這個術語,你可能在你的代碼裏用到它百萬次了。但願這篇文章能加深你對它的瞭解。

在這篇文章中提到的例子均可以在這裏找到。

譯者注

本文爲譯文,原文爲「Dependency Injection in JavaScript」。

 支持我繼續翻譯吧。

 關注個人微博吧

相關文章
相關標籤/搜索