JavaScript中的依賴注入

我喜歡那么一句話,就是編程就是管理復(fù)雜性葫督。也許你聽說過計(jì)算機(jī)世界就是一個(gè)巨大的抽象結(jié)構(gòu)竭鞍。我們只是簡(jiǎn)單地把東西包裝起來,一遍又一遍地生產(chǎn)新的工具橄镜。只需要試想一下偎快。你使用的語言具有內(nèi)置功能,它們可能是其他底層操作的抽象功能洽胶。JavaScript也一樣晒夹。

你遲早需要使用其他開發(fā)人員所寫的抽象。也就是說姊氓,你依賴于別人的代碼丐怯。我喜歡模塊沒有依賴,但這有點(diǎn)難實(shí)現(xiàn)翔横。即使你創(chuàng)建了一些像組件這樣的很不錯(cuò)的黑盒读跷,但你仍然有一部分是組合了所有的東西。這就是依賴注入的位置棕孙。如今舔亭,有效管理依賴關(guān)系的能力是絕對(duì)必要的。這篇文章總結(jié)了我對(duì)這個(gè)問題的看法蟀俊。

目標(biāo)

假設(shè)我們有兩個(gè)模塊钦铺。第一個(gè)是發(fā)出Ajax請(qǐng)求的服務(wù),第二個(gè)是路由器肢预。

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

我們還有一個(gè)函數(shù)需要這些模塊矛洞。

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

為了讓事情變得更有趣,這個(gè)函數(shù)需要再接受一個(gè)更多的參數(shù)烫映。當(dāng)然沼本,我們可以使用上面的代碼,但是這并不是很靈活锭沟。如果我們想使用ServiceXMLServiceJSON怎么辦抽兆?或者,如果我們想要模擬一些模塊來進(jìn)行測(cè)試族淮,該怎么辦辫红?我們不能只編輯函數(shù)體凭涂。我們首先想到的是將依賴項(xiàng)作為參數(shù)傳遞給函數(shù)。例如:

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

通過這樣做贴妻,我們傳遞了我們想要的模塊的確切實(shí)現(xiàn)切油。然而,這帶來了一個(gè)新問題名惩。想象一下澎胡,如果我們的代碼中到處都是doSomething。如果我們需要第三個(gè)依賴娩鹉,會(huì)發(fā)生什么攻谁。我們不能修改所有的函數(shù)調(diào)用。所以底循,我們需要一種工具來幫我們做到這一點(diǎn)巢株。這就是依賴注入器試圖解決的問題槐瑞。讓我們寫下幾個(gè)我們想要實(shí)現(xiàn)的目標(biāo):

  • 我們應(yīng)該能夠去注冊(cè)依賴項(xiàng)
  • 注入器應(yīng)該接受一個(gè)函數(shù)并返回一個(gè)以某種方式獲取所需的資源的函數(shù)熙涤。
  • 我們不應(yīng)該寫太多,我們需要短小精悍(emmm)的語法困檩。
  • 注入器應(yīng)該保持傳遞的函數(shù)的作用域祠挫。
  • 傳遞的函數(shù)應(yīng)該能夠接受自定義參數(shù),而不僅僅是描述的依賴項(xiàng)悼沿。

一個(gè)不錯(cuò)的列表等舔,不是嗎?就讓我們一探究竟吧糟趾。

requirejs / AMD 方法

你可能已經(jīng)了解過requirejs慌植。它是解決依賴關(guān)系的一個(gè)很好的變體。

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

其思想是首先描述所需的依賴關(guān)系义郑,然后編寫函數(shù)蝶柿。參數(shù)的順序在這里當(dāng)然很重要。假設(shè)我們將編寫一個(gè)名為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ù)的主體劫笙。我使用expect.js作為斷言庫(kù)芙扎,只是為了確保我正在編寫的代碼能夠按照我的預(yù)期工作。有點(diǎn)TDD方法填大。

我們的injector模塊從這里開始戒洼。作為一個(gè)單例對(duì)象是很好的,因此它可以從應(yīng)用程序的不同部分完成它的工作允华。

var injector = {
    dependencies: {},
    register: function(key, value) {
        this.dependencies[key] = value;
    },
    resolve: function(deps, func, scope) {

    }
}

非常簡(jiǎn)單的對(duì)象圈浇,它有兩個(gè)函數(shù)和一個(gè)變量作為存儲(chǔ)敷矫。我們要做的是檢查deps數(shù)組并在dependencies變量中搜索答案。其余的只是針對(duì)傳過去的func參數(shù)調(diào)用.apply方法汉额。

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變量轉(zhuǎn)換為實(shí)際數(shù)組所必需的。這個(gè)實(shí)現(xiàn)的問題是蠕搜,我們必須編寫兩次所需的組件怎茫,并且我們不能真正混合它們的順序。其他自定義參數(shù)始終位于依賴項(xiàng)之后妓灌。

reflection(反射)方法

根據(jù)Wikipedia轨蛤,反射是程序在運(yùn)行時(shí)檢查和修改對(duì)象的結(jié)構(gòu)和行為的能力。簡(jiǎn)單的說就是虫埂,在JavaScript上下文中祥山,就是讀取對(duì)象或函數(shù)的源代碼并分析它。讓我們從一開始就得到doSomething函數(shù)掉伏。如果你對(duì)doSomething.toString()進(jìn)行輸出缝呕,你將會(huì)得到下面字符串:

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

將該方法作為字符串使我們能夠獲取預(yù)期的參數(shù)。 而且斧散,更重要的是供常,他們的名字。這就是Angular用來實(shí)現(xiàn)依賴注入的方法鸡捐。我做了一點(diǎn)小小的改動(dòng)栈暇,得到了一個(gè)正則表達(dá)式,它直接從Angular的代碼中導(dǎo)出參數(shù)箍镜。

/^function\\s*[^\\(]*\\(\\s*([^\\)]*)\\)/m

我們可以將resolve類更改為以下內(nèi)容:

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);
    }        
}

我們根據(jù)函數(shù)的定義運(yùn)行RegExp源祈。其結(jié)果是:

["function (service, router, other)", "service, router, other"]

所以,我們只需要第二項(xiàng)色迂。一旦我們清理了空的空格并拆分了字符串香缺,我們就填充了deps數(shù)組。還有一個(gè)變化:

var a = Array.prototype.slice.call(arguments, 0);
...
args.push(self.dependencies[d] && d != '' ? self.dependencies[d] : a.shift());

我們循環(huán)遍歷依賴項(xiàng)脚草,如果缺少什么赫悄,我們將嘗試從arguments對(duì)象中獲取它。值得慶幸的是馏慨,如果數(shù)組為空埂淮,則shift方法返回undefined。他不會(huì)拋出一個(gè)錯(cuò)誤写隶。新版本的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ù)寫依賴項(xiàng)倔撞,我們可以混合它們的順序。它仍然有效慕趴,我們復(fù)制了Angular的魔法痪蝇。

然而鄙陡,世界并不完美,反射式注入有一個(gè)很大的問題躏啰。壓縮將打破我們的邏輯趁矾。那是因?yàn)樗淖兞藚?shù)的名稱,我們將無法解析依賴關(guān)系给僵。例如:

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

這就是我們的doSomething函數(shù)被傳遞給了一個(gè)壓縮器毫捣。 Angular團(tuán)隊(duì)提出的解決方案看起來像這樣:

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

}]);

它看起來像我們開始時(shí)的東西。我個(gè)人無法找到更好的解決方案帝际,于是決定將這兩種方法混合使用蔓同。這就是注入器(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方法接受兩個(gè)或三個(gè)參數(shù)蹲诀。如果是兩個(gè)斑粱,就像我們最近寫的一樣。是脯爪,如果有三個(gè)參數(shù)则北,它將獲得第一個(gè)參數(shù),然后解析它并填充deps數(shù)組披粟。下面是測(cè)試用例:

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");

你可能會(huì)注意到有兩個(gè)逗號(hào)一個(gè)接一個(gè)咒锻。這不是手誤∈靥耄空值實(shí)際上表示"Other"參數(shù)。這就是我們?nèi)绾慰刂茀?shù)順序的蒿辙。

直接注入到作用域

有時(shí)我會(huì)使用第三種注入方式拇泛。它涉及到對(duì)函數(shù)作用域(或者換句話說,就是this對(duì)象)的操作思灌。所以俺叭,它并不總是合適的。

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));
        }        
    }
}

我們所做的就是將依賴項(xiàng)附加到作用域上泰偿。這里的好處是開發(fā)人員不應(yīng)該將依賴項(xiàng)作為參數(shù)編寫熄守。他們只是函數(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");

最后的話

依賴注入是我們都在做的事情之一耗跛,但從來沒有想過裕照。即使你沒有聽說過這個(gè)詞,你可能已經(jīng)用過幾百萬次了调塌。

本文中提到的所有示例都可以在這里看到晋南。

本文原文:JavaScript中的依賴注入

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市羔砾,隨后出現(xiàn)的幾起案子负间,更是在濱河造成了極大的恐慌偶妖,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,188評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件政溃,死亡現(xiàn)場(chǎng)離奇詭異趾访,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)董虱,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,464評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門腹缩,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人空扎,你說我怎么就攤上這事藏鹊。” “怎么了转锈?”我有些...
    開封第一講書人閱讀 165,562評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵盘寡,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我撮慨,道長(zhǎng)竿痰,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,893評(píng)論 1 295
  • 正文 為了忘掉前任砌溺,我火速辦了婚禮影涉,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘规伐。我一直安慰自己蟹倾,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,917評(píng)論 6 392
  • 文/花漫 我一把揭開白布猖闪。 她就那樣靜靜地躺著鲜棠,像睡著了一般。 火紅的嫁衣襯著肌膚如雪培慌。 梳的紋絲不亂的頭發(fā)上豁陆,一...
    開封第一講書人閱讀 51,708評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音吵护,去河邊找鬼盒音。 笑死,一個(gè)胖子當(dāng)著我的面吹牛馅而,可吹牛的內(nèi)容都是我干的祥诽。 我是一名探鬼主播,決...
    沈念sama閱讀 40,430評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼用爪,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼原押!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起偎血,我...
    開封第一講書人閱讀 39,342評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤诸衔,失蹤者是張志新(化名)和其女友劉穎盯漂,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體笨农,經(jīng)...
    沈念sama閱讀 45,801評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡就缆,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,976評(píng)論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了谒亦。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片竭宰。...
    茶點(diǎn)故事閱讀 40,115評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖份招,靈堂內(nèi)的尸體忽然破棺而出切揭,到底是詐尸還是另有隱情,我是刑警寧澤锁摔,帶...
    沈念sama閱讀 35,804評(píng)論 5 346
  • 正文 年R本政府宣布廓旬,位于F島的核電站,受9級(jí)特大地震影響谐腰,放射性物質(zhì)發(fā)生泄漏孕豹。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,458評(píng)論 3 331
  • 文/蒙蒙 一十气、第九天 我趴在偏房一處隱蔽的房頂上張望励背。 院中可真熱鬧,春花似錦砸西、人聲如沸叶眉。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,008評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)竟闪。三九已至,卻和暖如春杖狼,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背妖爷。 一陣腳步聲響...
    開封第一講書人閱讀 33,135評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工蝶涩, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人絮识。 一個(gè)月前我還...
    沈念sama閱讀 48,365評(píng)論 3 373
  • 正文 我出身青樓绿聘,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親次舌。 傳聞我的和親對(duì)象是個(gè)殘疾皇子熄攘,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,055評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • 本文作者:言墨兒 原文出處:簡(jiǎn)書,csdn 未經(jīng)同意彼念,禁止轉(zhuǎn)載 本人在研究前端自動(dòng)化時(shí)挪圾,開始深入學(xué)習(xí)設(shè)計(jì)模式浅萧,由此...
    儂姝沁兒閱讀 4,123評(píng)論 0 2
  • 一、什么是依賴注入 控制反轉(zhuǎn)(IoC) 控制反轉(zhuǎn)的概念最早在2004年由Martin Fowler提出哲思,是針對(duì)面向...
    Keriy閱讀 3,179評(píng)論 0 8
  • 任何一個(gè)好的東西(語言洼畅、框架等)最終還取決于用的人 語言和框架本身并不能保證用戶的代碼清晰、解耦等棚赔, 當(dāng)然它只是盡...
    約書亞Luis閱讀 781評(píng)論 0 1
  • 第一次接觸這個(gè)詞“心錨”是來自一個(gè)心理學(xué)實(shí)驗(yàn)靠益,看完以后覺得像變魔術(shù)似得引導(dǎo)實(shí)驗(yàn)者做了一系列讓我頗為詫異的連鎖反...
    流暢ing閱讀 518評(píng)論 1 1
  • 下班在電梯里丧肴,同事拿著手機(jī)念道: 上班和退休的區(qū)別是:上班,能掙錢才有飯吃胧后;退休芋浮,能吃飯就能掙錢! 大家都笑了绩卤,一...
    珞歷閱讀 2,106評(píng)論 2 16