做为面向对象编程中实现控制反转(Inversion of Control,下文称IoC)最多见的技术手段之一,依赖注入(Dependency Injection,下文称DI)可谓在OOP编程中大行其道经久不衰。好比在J2EE中,就有大名鼎鼎的执牛耳者Spring。Javascript社区中天然也不乏一些积极的尝试,广为人知的AngularJS很大程度上就是基于DI实现的。遗憾的是,做为一款缺乏反射机制、不支持Annotation语法的动态语言,Javascript长期以来都没有属于本身的Spring框架。固然,伴随着ECMAScript草案进入快速迭代期的春风,Javascript社区中的各类方言、框架可谓群雄并起,方兴未艾。能够预见到,优秀的JavascriptDI框架的出现只是迟早的事。javascript
本文总结了Javascript中常见的依赖注入方式,并以inversify.js为例,介绍了方言社区对于Javascript中DI框架的尝试和初步成果。文章分为四节:html
一. 基于Injector、Cache和函数参数名的依赖注入
二. AngularJS中基于双Injector的依赖注入
三. TypeScript中基于装饰器和反射的依赖注入
四. inversify.js——Javascript技术栈中的IoC容器前端
一. 基于Injector、Cache和函数参数名的依赖注入java
尽管Javascript中不原生支持反射(Reflection)语法,可是Function.prototype上的toString方法却为咱们另辟蹊径,使得在运行时窥探某个函数的内部构形成为可能:toString方法会以字符串的形式返回包含function关键字在内的整个函数定义。从这个完整的函数定义出发,咱们能够利用正则表达式提取出该函数所须要的参数,从而在某种程度上得知该函数的运行依赖。
好比Student类上write方法的函数签名write(notebook, pencil)就说明它的执行依赖于notebook和pencil对象。所以,咱们能够首先把notebook和pencil对象存放到某个cache中,再经过injector(注入器、注射器)向write方法提供它所须要的依赖:git
var cache = {}; // 经过解析Function.prototype.toString()取得参数名 function getParamNames(func) { // 正则表达式出自http://krasimirtsonev.com/blog/article/Dependency-injection-in-JavaScript var paramNames = func.toString().match(/^function\s*[^\(]*\(\s*([^\)]*)\)/m)[1]; paramNames = paramNames.replace(/ /g, ''); paramNames = paramNames.split(','); return paramNames; } var injector = { // 将func做用域中的this关键字绑定到bind对象上,bind对象能够为空 resolve: function (func, bind) { // 取得参数名 var paramNames = getParamNames(func); var params = []; for (var i = 0; i < paramNames.length; i++) { // 经过参数名在cache中取出相应的依赖 params.push(cache[paramNames[i]]); } // 注入依赖并执行函数 func.apply(bind, params); } }; function Notebook() {} Notebook.prototype.printName = function () { console.log('this is a notebook'); }; function Pencil() {} Pencil.prototype.printName = function () { console.log('this is a pencil'); }; function Student() {} Student.prototype.write = function (notebook, pencil) { if (!notebook || !pencil) { throw new Error('Dependencies not provided!'); } console.log('writing...'); }; // 提供notebook依赖 cache['notebook'] = new Notebook(); // 提供pencil依赖 cache['pencil'] = new Pencil(); var student = new Student(); injector.resolve(student.write, student); // writing...
有时候为了保证良好的封装性,也不必定要把cache对象暴露给外界做用域,更多的时候是以闭包变量或者私有属性的形式存在的:github
function Injector() { this._cache = {}; } Injector.prototype.put = function (name, obj) { this._cache[name] = obj; }; Injector.prototype.getParamNames = function (func) { // 正则表达式出自http://krasimirtsonev.com/blog/article/Dependency-injection-in-JavaScript var paramNames = func.toString().match(/^function\s*[^\(]*\(\s*([^\)]*)\)/m)[1]; paramNames = paramNames.replace(/ /g, ''); paramNames = paramNames.split(','); return paramNames; }; Injector.prototype.resolve = function (func, bind) { var self = this; var paramNames = self.getParamNames(func); var params = paramNames.map(function (name) { return self._cache[name]; }); func.apply(bind, params); }; var injector = new Injector(); var student = new Student(); injector.put('notebook', new Notebook()); injector.put('pencil', new Pencil()) injector.resolve(student.write, student); // writing...
好比如今要执行Student类上的另外一个方法function draw(notebook, pencil, eraser),由于injector的cache中已经有了notebook和pencil对象,咱们只须要将额外的eraser也存放到cache中:正则表达式
function Eraser() {} Eraser.prototype.printName = function () { console.log('this is an eraser'); }; // 为Student增长draw方法 Student.prototype.draw = function (notebook, pencil, eraser) { if (!notebook || !pencil || !eraser) { throw new Error('Dependencies not provided!'); } console.log('drawing...'); }; injector.put('eraser', new Eraser()); injector.resolve(student.draw, student);
经过依赖注入,函数的执行和其所依赖对象的建立逻辑就被解耦开来了。
固然,随着grunt/gulp/fis等前端工程化工具的普及,愈来愈多的项目在上线以前都通过了代码混淆(uglify),于是经过参数名去判断依赖并不老是可靠,有时候也会经过为function添加额外属性的方式来明确地说明其依赖:编程
Student.prototype.write.depends = ['notebook', 'pencil']; Student.prototype.draw.depends = ['notebook', 'pencil', 'eraser']; Injector.prototype.resolve = function (func, bind) { var self = this; // 首先检查func上是否有depends属性,若是没有,再用正则表达式解析 func.depends = func.depends || self.getParamNames(func); var params = func.depends.map(function (name) { return self._cache[name]; }); func.apply(bind, params); }; var student = new Student(); injector.resolve(student.write, student); // writing... injector.resolve(student.draw, student); // draw...
二. AngularJS中基于双Injector的依赖注入gulp
熟悉AngularJS的同窗很快就能联想到,在injector注入以前,咱们在定义module时还能够调用config方法来配置随后会被注入的对象。典型的例子就是在使用路由时对$routeProvider的配置。也就是说,不一样于上一小节中直接将现成对象(好比new Notebook())存入cache的作法,AngularJS中的依赖注入应该还有一个"实例化"或者"调用工厂方法"的过程。
这就是providerInjector、instanceInjector以及他们各自所拥有的providerCache和instanceCache的由来。
在AngularJS中,咱们可以经过依赖注入获取到的injector一般是instanceInjector,而providerInjector则是以闭包中变量的形式存在的。每当咱们须要AngularJS提供依赖注入服务时,好比想要获取notebook,instanceInjector会首先查询instanceCache上是存在notebook属性,若是存在,则直接注入;若是不存在,则将这个任务转交给providerInjector;providerInjector会将"Provider"字符串拼接到"notebook"字符串的后面,组成一个新的键名"notebookProvider",再到providerCache中查询是否有notebookProvider这个属性,若有没有,则抛出异常Unknown Provider异常:前端工程化
若是有,则将这个provider返回给instanceInjector;instanceInjector拿到notebookProvider后,会调用notebookProvider上的工厂方法$get,获取返回值notebook对象,将该对象放到instanceCache中以备未来使用,同时也注入到一开始声明这个依赖的函数中。过程描述起来比较复杂,能够经过下面的图示来讲明:
须要注意的是,AngularJS中的依赖注入方式也是有缺陷的:利用一个instanceInjector单例服务全局的反作用就是没法单独跟踪和控制某一条依赖链条,即便在没有交叉依赖的状况下,不一样module中的同名provider也会产生覆盖,这里就不详细展开了。
另外,对于习惯于Java和C#等语言中高级IoC容器的同窗来讲,看到这里可能以为有些别扭,毕竟在OOP中,咱们一般不会将依赖以参数的形式传递给方法,而是做为属性经过constructor或者setters传递给实例,以实现封装。的确如此,1、二节中的依赖注入方式没有体现出足够的面向对象特性,毕竟这种方式在Javascript已经存在多年了,甚至都不须要ES5的语法支持。但愿了解Javascript社区中最近一两年关于依赖注入的研究和成果的同窗,能够继续往下阅读。
三. TypeScript中基于装饰器和反射的依赖注入
博主自己对于Javascript的各类方言的学习并非特别热情,尤为是如今EMCAScript提案、草案更新很快,不少时候借助于polyfill和babel的各类preset就能知足需求了。可是TypeScript是一个例外(固然如今Decorator也已是提案了,虽然阶段还比较早,可是确实已经有polyfill可使用)。上文提到,Javascript社区中迟迟没有出现一款优秀的IoC容器和自身的语言特性有关,那就依赖注入这个话题而言,TypeScript给咱们带来了什么不一样呢?至少有下面这几点:
* TypeScript增长了编译时类型检查,使Javascript具有了必定的静态语言特性
* TypeScript支持装饰器(Decorator)语法,和传统的注解(Annotation)颇为类似
* TypeScript支持元信息(Metadata)反射,再也不须要调用Function.prototype.toString方法
下面咱们就尝试利用TypeScript带来的新语法来规范和简化依赖注入。此次咱们再也不向函数或方法中注入依赖了,而是向类的构造函数中注入。
TypeScript支持对类、方法、属性和函数参数进行装饰,这里须要用到的是对类的装饰。继续上面小节中用到的例子,利用TypeScript对代码进行一些重构:
class Pencil { public printName() { console.log('this is a pencil'); } } class Eraser { public printName() { console.log('this is an eraser'); } } class Notebook { public printName() { console.log('this is a notebook'); } } class Student { pencil: Pencil; eraser: Eraser; notebook: Notebook; public constructor(notebook: Notebook, pencil: Pencil, eraser: Eraser) { this.notebook = notebook; this.pencil = pencil; this.eraser = eraser; } public write() { if (!this.notebook || !this.pencil) { throw new Error('Dependencies not provided!'); } console.log('writing...'); } public draw() { if (!this.notebook || !this.pencil || !this.eraser) { throw new Error('Dependencies not provided!'); } console.log('drawing...'); } }
下面是injector和装饰器Inject的实现。injector的resolve方法在接收到传入的构造函数时,会经过name属性取出该构造函数的名字,好比class Student,它的name属性就是字符串"Student"。再将Student做为key,到dependenciesMap中去取出Student的依赖,至于dependenciesMap中是什么时候存入的依赖关系,这是装饰器Inject的逻辑,后面会谈到。Student的依赖取出后,因为这些依赖已是构造函数的引用而非简单的字符串了(好比Notebook、Pencil的构造函数),所以直接使用new语句便可获取这些对象。获取到Student类所依赖的对象以后,如何把这些依赖做为构造函数的参数传入到Student中呢?最简单的莫过于ES6的spread操做符。在不能使用ES6的环境下,咱们也能够经过伪造一个构造函数来完成上述逻辑。注意为了使instanceof操做符不失效,这个伪造的构造函数的prototype属性应该指向原构造函数的prototype属性。
var dependenciesMap = {}; var injector = { resolve: function (constructor) { var dependencies = dependenciesMap[constructor.name]; dependencies = dependencies.map(function (dependency) { return new dependency(); }); // 若是可使用ES6的语法,下面的代码能够合并为一行: // return new constructor(...dependencies); var mockConstructor: any = function () { constructor.apply(this, dependencies); }; mockConstructor.prototype = constructor.prototype; return new mockConstructor(); } }; function Inject(...dependencies) { return function (constructor) { dependenciesMap[constructor.name] = dependencies; return constructor; }; }
injector和装饰器Inject的逻辑完成后,就能够用来装饰class Student并享受依赖注入带来的乐趣了:
// 装饰器的使用很是简单,只须要在类定义的上方添加一行代码 // Inject是装饰器的名字,后面是function Inject的参数 @Inject(Notebook, Pencil, Eraser) class Student { pencil: Pencil; eraser: Eraser; notebook: Notebook; public constructor(notebook: Notebook, pencil: Pencil, eraser: Eraser) { this.notebook = notebook; this.pencil = pencil; this.eraser = eraser; } public write() { if (!this.notebook || !this.pencil) { throw new Error('Dependencies not provided!'); } console.log('writing...'); } public draw() { if (!this.notebook || !this.pencil || !this.eraser) { throw new Error('Dependencies not provided!'); } console.log('drawing...'); } } var student = injector.resolve(Student); console.log(student instanceof Student); // true student.notebook.printName(); // this is a notebook student.pencil.printName(); // this is a pencil student.eraser.printName(); // this is an eraser student.draw(); // drawing student.write(); // writing
利用装饰器,咱们还能够实现一种比较激进的依赖注入,下文称之为RadicalInject。RadicalInject对原代码的侵入性比较强,不必定适合具体的业务,这里也一并介绍一下。要理解RadicalInject,须要对TypeScript装饰器的原理和Array.prototype上的reduce方法理解比较到位。
function RadicalInject(...dependencies){ var wrappedFunc:any = function (target: any) { dependencies = dependencies.map(function (dependency) { return new dependency(); }); // 使用mockConstructor的缘由和上例相同 function mockConstructor() { target.apply(this, dependencies); } mockConstructor.prototype = target.prototype; // 为何须要使用reservedConstructor呢?由于使用RadicalInject对Student方法装饰以后, // Student指向的构造函数已经不是一开始咱们声明的class Student了,而是这里的返回值, // 即reservedConstructor。Student的指向变了并非一件不能接受的事,可是若是要 // 保证student instanceof Student如咱们所指望的那样工做,这里就应该将 // reservedConstructor的prototype属性指向原Student的prototype function reservedConstructor() { return new mockConstructor(); } reservedConstructor.prototype = target.prototype; return reservedConstructor; } return wrappedFunc; }
使用RadicalInject,原构造函数实质上已经被一个新的函数代理了,使用上也更为简单,甚至都不须要再有injector的实现:
@RadicalInject(Notebook, Pencil, Eraser) class Student { pencil: Pencil; eraser: Eraser; notebook: Notebook; public constructor() {} public constructor(notebook: Notebook, pencil: Pencil, eraser: Eraser) { this.notebook = notebook; this.pencil = pencil; this.eraser = eraser; } public write() { if (!this.notebook || !this.pencil) { throw new Error('Dependencies not provided!'); } console.log('writing...'); } public draw() { if (!this.notebook || !this.pencil || !this.eraser) { throw new Error('Dependencies not provided!'); } console.log('drawing...'); } } // 再也不出现injector,直接调用构造函数 var student = new Student(); console.log(student instanceof Student); // true student.notebook.printName(); // this is a notebook student.pencil.printName(); // this is a pencil student.eraser.printName(); // this is an eraser student.draw(); // drawing student.write(); // writing
因为class Student的constructor方法须要接收三个参数,直接无参调用new Student()会形成TypeScript编译器报错。固然这里只是分享一种思路,你们能够暂时忽略这个错误。有兴趣的同窗也可使用相似的思路尝试代理一个工厂方法,而非直接代理构造函数,以免这类错误,这里再也不展开。
AngularJS2团队为了得到更好的装饰器和反射语法的支持,一度准备另起炉灶,基于AtScript(AtScript中的"A"指的就是Annotation)来进行新框架的开发。但最终却选择拥抱TypeScript,因而便有了微软和谷歌的奇妙组合。
固然,须要说明的是,在缺乏相关标准和浏览器厂商支持的状况下,TypeScript在运行时只是纯粹的Javascript,下节中出现的例子会印证这一点。
四. inversify.js——Javascript技术栈中的IoC容器
其实从Javascript出现各类支持高级语言特性的方言就能够预见到,IoC容器的出现只是迟早的事情。好比博主今天要介绍的基于TypeScript的inversify.js,就是其中的先行者之一。
inversity.js比上节中博主实现的例子还要进步不少,它最初设计的目的就是为了前端工程师同窗们能在Javascript中写出符合SOLID原则的代码,立意可谓很是之高。表如今代码中,就是到处有接口,将"Depend upon Abstractions. Do not depend upon concretions."(依赖于抽象,而非依赖于具体)表现地淋漓尽致。继续使用上面的例子,可是因为inversity.js是面向接口的,上面的代码须要进一步重构:
interface NotebookInterface { printName(): void; } interface PencilInterface { printName(): void; } interface EraserInterface { printName(): void; } interface StudentInterface { notebook: NotebookInterface; pencil: PencilInterface; eraser: EraserInterface; write(): void; draw(): void; } class Notebook implements NotebookInterface { public printName() { console.log('this is a notebook'); } } class Pencil implements PencilInterface { public printName() { console.log('this is a pencil'); } } class Eraser implements EraserInterface { public printName() { console.log('this is an eraser'); } } class Student implements StudentInterface { notebook: NotebookInterface; pencil: PencilInterface; eraser: EraserInterface; constructor(notebook: NotebookInterface, pencil: PencilInterface, eraser: EraserInterface) { this.notebook = notebook; this.pencil = pencil; this.eraser = eraser; } write() { console.log('writing...'); } draw() { console.log('drawing...'); } }
因为使用了inversity框架,此次咱们就不用本身实现injector和Inject装饰器啦,只须要从inversify模块中引用相关对象:
import { Inject } from "inversify"; @Inject("NotebookInterface", "PencilInterface", "EraserInterface") class Student implements StudentInterface { notebook: NotebookInterface; pencil: PencilInterface; eraser: EraserInterface; constructor(notebook: NotebookInterface, pencil: PencilInterface, eraser: EraserInterface) { this.notebook = notebook; this.pencil = pencil; this.eraser = eraser; } write() { console.log('writing...'); } draw() { console.log('drawing...'); } }
这样就好了吗?还记得上节中提到TypeScript中各类概念只是语法糖吗?不一样于上一节中直接将constructor引用传递给Inject的例子,因为inversify.js是面向接口的,而诸如NotebookInterface、PencilInterface之类的接口只是由TypeScript提供的语法糖,在运行时并不存在,所以咱们在装饰器中声明依赖时只能使用字符串形式而非引用形式。不过不用担忧,inversify.js为咱们提供了bind机制,在接口的字符串形式和具体的构造函数之间搭建了桥梁:
import { TypeBinding, Kernel } from "inversify"; var kernel = new Kernel(); kernel.bind(new TypeBinding<NotebookInterface>("NotebookInterface", Notebook)); kernel.bind(new TypeBinding<PencilInterface>("PencilInterface", Pencil)); kernel.bind(new TypeBinding<EraserInterface>("EraserInterface", Eraser)); kernel.bind(new TypeBinding<StudentInterface>("StudentInterface", Student));
注意这步须要从inversify模块中引入TypeBinding和Kernel,而且为了保证返回值类型以及整个编译时静态类型检查可以顺利经过,泛型语法也被使用了起来。
说到这里,要理解new TypeBinding<NotebookInterface>("NotebookInterface", Notebook)也就很天然了:为依赖于"NotebookInterface"字符串的类提供Notebook类的实例,返回值向上溯型到NotebookInterface。
完成了这些步骤,使用起来也还算顺手:
var student: StudentInterface = kernel.resolve<StudentInterface>("StudentInterface"); console.log(student instanceof Student); // true student.notebook.printName(); // this is a notebook student.pencil.printName(); // this is a pencil student.eraser.printName(); // this is an eraser student.draw(); // drawing student.write(); // writing
最后,顺带提一下ECMAScript中相关提案的现状和进展。Google的AtScript团队曾经有过Annotation的提案,可是AtScript胎死腹中,这个提案天然不了了之了。目前比较有但愿成为es7标准的是一个关于装饰器的提案:https://github.com/wycats/javascript-decorators。感兴趣的同窗能够到相关的github页面跟踪了解。尽管DI只是OOP编程众多模式和特性中的一个,但却能够折射出Javascript在OOP上艰难前进的道路。但总得说来,也算得上是路途坦荡,前途光明。回到依赖注入的话题上,一边是翘首以盼的Javascript社区,一边是姗姗来迟的IoC容器,这两者最终能产生怎样的化学反应,让咱们拭目以待。
做者:ralph_zhu
时间:2016-02-23 08:00