var cache = {};// 通过解析Function.prototype.toString()取得参数名function getParamNames(func) { var paramNames = func.toString().match(/^functions*[^(]*(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对象暴露给外界作用域,更多的时候是以闭包变量或者私有属性的形式存在的:function Injector() { this._cache = {};} Injector.prototype.put = function (name, obj) { this._cache[name] = obj;}; Injector.prototype.getParamNames = function (func) { var paramNames = func.toString().match(/^functions*[^(]*(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);通过依赖注入,函数的执行和其所依赖对象的创建逻辑就被解耦开来了。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的依赖注入
如果有,则将这个provider返回给instanceInjector;instanceInjector拿到notebookProvider后,会调用notebookProvider上的工厂方法$get,获取返回值notebook对象,将该对象放到instanceCache中以备将来使用,同时也注入到一开始声明这个依赖的函数中。过程描述起来比较复杂,可以通过下面的图示来说明:

需要注意的是,AngularJS中的依赖注入方式也是有缺陷的:利用一个instanceInjector单例服务全局的副作用就是无法单独跟踪和控制某一条依赖链条,即使在没有交叉依赖的情况下,不同module中的同名provider也会产生覆盖,这里就不详细展开了。
另外,对于习惯于Java和C#等语言中高级IoC容器的同学来说,看到这里可能觉得有些别扭,毕竟在OOP中,我们通常不会将依赖以参数的形式传递给方法,而是作为属性通过constructor或者setters传递给实例,以实现封装。的确如此,一、二节中的依赖注入方式没有体现出足够的面向对象特性,毕竟这种方式在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); // truestudent.notebook.printName(); // this is a notebookstudent.pencil.printName(); // this is a pencilstudent.eraser.printName(); // this is an eraserstudent.draw(); // drawingstudent.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的prototypefunction 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); // truestudent.notebook.printName(); // this is a notebookstudent.pencil.printName(); // this is a pencilstudent.eraser.printName(); // this is an eraserstudent.draw(); // drawingstudent.write(); // writing由于class Student的constructor方法需要接收三个参数,直接无参调用new Student()会造成TypeScript编译器报错。当然这里只是分享一种思路,大家可以暂时忽略这个错误。有兴趣的同学也可以使用类似的思路尝试代理一个工厂方法,而非直接代理构造函数,以避免这类错误,这里不再展开。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,并且为了保证返回值类型以及整个编译时静态类型检查能够顺利通过,泛型语法也被使用了起来。var student: StudentInterface = kernel.resolve<StudentInterface>("StudentInterface");console.log(student instanceof Student); // truestudent.notebook.printName(); // this is a notebookstudent.pencil.printName(); // this is a pencilstudent.eraser.printName(); // this is an eraserstudent.draw(); // drawingstudent.write(); // writing以上就是关于Javascript技术栈中的四种依赖注入的全部内容,希望对大家的学习有所帮助。