作为面向对象编程中实现控制反转(Inversion of Control,下文称IoC)最常见的技术手段之一,依赖注入(Dependency Injection,下文称DI)可谓在OOP编程中大行其道经久不衰。比如在J2EE中,就有大名鼎鼎的执牛耳者Spring。Javascript社区中自然也不乏一些积极的尝试,广为人知的AngularJS很大程度上就是基于DI实现的。遗憾的是,作为一款缺少反射机制、不支持Annotation语法的动态语言,Javascript长期以来都没有属于自己的Spring框架。当然,伴随着ECMAScript草案进入快速迭代期的春风,Javascript社区中的各种方言、框架可谓群雄并起,方兴未艾。可以预见到,优秀的JavascriptDI框架的出现只是早晚的事。
本文总结了Javascript中常见的依赖注入方式,并以inversify.js为例,介绍了方言社区对于Javascript中DI框架的尝试和初步成果。文章分为四节:
一. 基于Injector、Cache和函数参数名的依赖注入
二. AngularJS中基于双Injector的依赖注入
三. TypeScript中基于装饰器和反射的依赖注入
四. inversify.js——Javascript技术栈中的IoC容器
一. 基于Injector、Cache和函数参数名的依赖注入
尽管Javascript中不原生支持反射(Reflection)语法,但是Function.prototype上的toString方法却为我们另辟蹊径,使得在运行时窥探某个函数的内部构造成为可能:toString方法会以字符串的形式返回包含function关键字在内的整个函数定义。从这个完整的函数定义出发,我们可以利用正则表达式提取出该函数所需要的参数,从而在某种程度上得知该函数的运行依赖。
比如Student类上write方法的函数签名write(notebook, pencil)就说明它的执行依赖于notebook和pencil对象。因此,我们可以首先把notebook和pencil对象存放到某个cache中,再通过injector(注入器、注射器)向write方法提供它所需要的依赖:
var cache = {}; // 通过解析Function.prototype.toString()取得参数名 function getParamNames(func) { // 正则表达式出自 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对象暴露给外界作用域,更多的时候是以闭包变量或者私有属性的形式存在的:
function Injector() { this._cache = {}; } Injector.prototype.put = function (name, obj) { this._cache[name] = obj; }; Injector.prototype.getParamNames = function (func) { // 正则表达式出自 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);