深入理解Javascript里的依賴注入
遲早你需要用到其他開發(fā)人員的抽象成果——即你依靠別人的代碼。我喜歡依賴自由(無依賴)的模塊,但那是難以實現(xiàn)的。甚至你創(chuàng)建的那些漂亮的黑盒子組件也或多或少會依賴一些東西。這正是依賴注入大顯身手的之處?,F(xiàn)在有效地管理依賴的能力是絕對必要的。本文總結(jié)了我對問題探索和一些的解決方案。
一、目標
設(shè)想我們有兩個模塊。第一個是負責Ajax請求服務(wù)(service),第二個是路由(router)。
var service = function() {
return { name: 'Service' };
}
var router = function() {
return { name: 'Router' };
}
我們有另一個函數(shù)需要用到這兩個模塊。
var doSomething = function(other) {
var s = service();
var r = router();
};
為使看起來更有趣,這函數(shù)接受一個參數(shù)。當然,我們完全可以使用上面的代碼,但這顯然不夠靈活。如果我們想使用ServiceXML或ServiceJSON呢,或者如果我們需要一些測試模塊呢。我們不能僅靠編輯函數(shù)體來解決問題。首先,我們可以通過函數(shù)的參數(shù)來解決依賴性。即:
var doSomething = function(service, router, other) {
var s = service();
var r = router();
};
我們通過傳遞額外的參數(shù)來實現(xiàn)我們想要的功能,然而,這會帶來新的問題。想象如果我們的doSomething 方法散落在我們的代碼中。如果我們需要更改依賴條件,我們不可能更改所有調(diào)用函數(shù)的文件。
我們需要一個能幫我們搞定這些的工具。這就是依賴注入嘗試解決的問題。讓我們寫下一些我們的依賴注入解決辦法應(yīng)該達到的目標:
我們應(yīng)該能夠注冊依賴關(guān)系
1.注入應(yīng)該接受一個函數(shù),并返回一個我們需要的函數(shù)
2.我們不能寫太多東西——我們需要精簡漂亮的語法
3.注入應(yīng)該保持被傳遞函數(shù)的作用域
4.被傳遞的函數(shù)應(yīng)該能夠接受自定義參數(shù),而不僅僅是依賴描述
5.堪稱完美的清單,下面 讓我們實現(xiàn)它。
三、RequireJS / AMD的方法
你可能對RequireJS早有耳聞,它是解決依賴注入不錯的選擇。
define(['service', 'router'], function(service, router) {
// ...
});
這種想法是先描述需要的依賴,然后再寫你的函數(shù)。這里參數(shù)的順序很重要。如上所說,讓我們寫一個叫做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");
再繼續(xù)之前我應(yīng)該解釋清楚doSomething函數(shù)體內(nèi)容,我使用expect.js (斷言方面的庫)僅是為了保證我寫的代碼的行為和我期望的是一樣的,體現(xiàn)一點點TDD(測試驅(qū)動開發(fā))方法。
下面開始我們的injector模塊,這是非常棒的一個單例模式,所以它能在我們程序的不同部分工作的很好。
var injector = {
dependencies: {},
register: function(key, value) {
this.dependencies[key] = value;
},
resolve: function(deps, func, scope) {
}
}
這是一個非常簡單的對象,有兩個方法,一個用來存儲的屬性。我們要做的是檢查deps數(shù)組并在dependencies變量中搜索答案。剩下的只是調(diào)用.apply方法并傳遞之前的func方法的參數(shù)。
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變量轉(zhuǎn)換為真正的數(shù)組。到目前為止還不錯。我們的測試通過了。這種實現(xiàn)的問題是,我們需要寫所需部件兩次,并且我們不能混淆他們的順序。附加的自定義參數(shù)總是位于依賴之后。
四、反射方法
根據(jù)維基百科的定義反射是指一個程序在運行時檢查和修改一個對象的結(jié)構(gòu)和行為的能力。簡單的說,在JavaScript的上下文里,這具體指讀取和分析的對象或函數(shù)的源代碼。讓我們完成文章開頭提到的doSomething函數(shù)。如果你在控制臺輸出doSomething.tostring()。你將得到如下的字符串:
"function (service, router, other) {
var s = service();
var r = router();
}"
通過此方法返回的字符串給我們遍歷參數(shù)的能力,更重要的是,能夠獲取他們的名字。這其實是Angular 實現(xiàn)它的依賴注入的方法。我偷了一點懶,直接截取Angular代碼中獲取參數(shù)的正則表達式。
/^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);
}
}
我們執(zhí)行正則表達式的結(jié)果如下:
["function (service, router, other)", "service, router, other"]
看起來,我們只需要第二項。一旦我們清楚空格并分割字符串就得到deps數(shù)組。只有一個大的改變:
var a = Array.prototype.slice.call(arguments, 0);
...
args.push(self.dependencies[d] && d != '' ? self.dependencies[d] : a.shift());
我們循環(huán)遍歷dependencies數(shù)組,如果發(fā)現(xiàn)缺失項則嘗試從arguments對象中獲取。謝天謝地,當數(shù)組為空時,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");
不必重寫依賴并且他們的順序可以打亂。它仍然有效,我們成功復(fù)制了Angular的魔法。
然而,這種做法并不完美,這就是反射類型注射一個非常大的問題。壓縮會破壞我們的邏輯,因為它改變參數(shù)的名字,我們將無法保持正確的映射關(guān)系。例如,doSometing()壓縮后可能看起來像這樣:
var doSomething=function(e,t,n){var r=e();var i=t()}
Angular團隊提出的解決方案看起來像:
var doSomething = injector.resolve(['service', 'router', function(service, router) {
}]);
這看起來很像我們開始時的解決方案。我沒能找到一個更好的解決方案,所以決定結(jié)合這兩種方法。下面是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訪客接受兩或三個參數(shù),如果有兩個參數(shù)它實際上和文章前面寫的一樣。然而,如果有三個參數(shù),它會將第一個參數(shù)轉(zhuǎn)換并填充deps數(shù)組,下面是一個測試例子:
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");
你可能注意到在第一個參數(shù)后面有兩個逗號——注意這不是筆誤??罩祵嶋H上代表“Other”參數(shù)(占位符)。這顯示了我們是如何控制參數(shù)順序的。
五、直接注入Scope
有時我會用到第三個注入變量,它涉及到操作函數(shù)的作用域(換句話說,就是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));
}
}
}
我們所做的一切其實就是將依賴添加到作用域。這樣做的好處是,開發(fā)人員不用再寫依賴性參數(shù);它們已經(jīng)是函數(shù)作用域的一部分。
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");
六、結(jié)束語
其實我們大部分人都用過依賴注入,只是我們沒有意識到。即使你不知道這個術(shù)語,你可能在你的代碼里用到它百萬次了。希望這篇文章能加深你對它的了解。
相關(guān)文章
JS實現(xiàn)常用導(dǎo)航鼠標下經(jīng)過下方橫線自動跟隨效果
這篇文章主要介紹了JS寫常用導(dǎo)航鼠標下經(jīng)過下方橫線自動跟隨效果,文中還給大家講解了基于css?+?js?實現(xiàn)導(dǎo)航欄下劃線跟隨鼠標滑動效果,需要的朋友可以參考下2023-01-01
innerHTML動態(tài)添加html代碼和腳本兼容多個瀏覽器
innerHTML動態(tài)添加html代碼和腳本,給某個元素的innerHTML賦值,并使得值中的js代碼有效且兼容多個瀏覽器,很棒的一個方法2014-10-10
JavaScript架構(gòu)前端不能沒有監(jiān)控系統(tǒng)原因
這篇文章主要為大家介紹了為什么前端不能沒有監(jiān)控系統(tǒng)的原因,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-06-06

