IT技术博客大学习 共学习 共进步

javascript依赖注入

风影博客 2015-06-01 23:25:30 浏览 3,442 次

   控制反转(Inversion of Control,英文缩写为IoC)是一个重要的面向对象编程的法则来削减计算机程序的耦合问题,也是轻量级的Spring框架的核心。其中,依赖注入(Dependency Injection,简称DI)是IoC的流行实现方式。以前,做SSH项目的时候简单地用过Spring框架,该框架相当于给我们提供了一个大容器,我们只需在XML配置文件中给出对象的定义,再以XML配置的类名通过反射机制生成对象,该容器就可统一管理所有对象的生命周期、依赖关系...,提高了程序的灵活性和可维护性。当时,还是停留在使用上并没有好好去理解里面的原理,这两天正好看到一篇文章,深入浅出地讲解了依赖注入原理,并且采用目前我热情使用的javascript语言给出了一个实现,于是我就好好的研究了一下。

   依赖注入可将复杂的程序分解为很多小任务,便于程序的模块化,方便测试以及提高应用的灵活性和可维护性。

目标

   假如我们有两个模块,一个是负责Ajax请求的服务service,另一个是路由router:

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

   我们还有一个使用这两个模块的函数

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

   为了更有趣一点,上面的函数需要接受一个参数。我们可以使用上面的代码,但是如果我们还想要使用ServiceXML、ServiceJSON等其他的模块或者进行一些模块测试呢?该函数显然不能灵活的满足我们的需求!我们不能仅靠编辑函数体来解决问题,第一个我们都能想到的是把依赖模块以参数的形式传递给该函数,即:

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

   我们通过传递额外的参数来实现我们想要的功能,但是这也会带来新的问题。想象一下如果doSomething函数散落在我们的代码里,我们需要再添加一个依赖会发生什么?我们不能重新编辑所有的函数调用!我们需要一个能帮我们搞定这些问题的工具,这就是依赖注入尝试解决的问题。让我们明确一些依赖注入应该达到的目标:

  • 我们应该能够注册依赖关系

  • 注入应该接受一个函数,并返回一个我们需要的函数

  • 我们不能写太多东西——我们需要精简漂亮的语法

  • 注入应该保持被传递函数的作用域

  • 被传递的函数应该能够接受自定义参数,而不仅仅是依赖描述

  •    完美的清单,下面我们就一步一步的实现它啦。

    RequireJS / AMD方法

       你可能对RequireJS早有耳闻,它是解决依赖注入不错的选择

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

   该设计思路首先需要描述依赖关系,然后再写你的回调函数。这里参数的顺序非常重要。基于此,我们写一个叫做injector的模块,能接受相同的语法:

  var doSomething = injector.resolve(['service', 'router'], function(service, router, other) {
      console.log(service().name);
      console.log(router().name);
      console.log(other);
  });
  doSomething("Other");

   下面开始我们的injector模块,这是非常棒的一个单例模式,所以它能在我们程序的不同部分工作的很好:

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

   这是一个非常简单的对象,包括两个方法和一个用于存储的属性。我们要做的就是检查deps数组并在dependencies变量中搜索答案,剩下的只是调用.apply方法并传递之前的func方法的参数:

      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\' resolve' + d);
          }
        }
        return function() {
          func.apply(scope || {}, args.concat(Array.prototype.slice.call(arguments, 0));
        }
      }

   scope参数可选,用于确定作用域。Array.prototype.slice.call(arguments, 0)是必须的,用来将arguments类数组转化为真正的数组格式。目前为止一切都还不错,我们的测试用例都能通过。可是这种实现的问题是,我们需要两次编写所需组件,并且我们不能混淆他们的顺序。附加的自定义参数总是位于依赖之后。

反射方法

   根据维基百科的定义:反射是指一个程序在运行时检查和修改一个对象的结构和行为的能力。简单的说,在JavaScript的上下文里,这具体指读取和分析的对象或函数的源代码。让我们从文章开头提到的doSomething函数开始,如果你在控制台输出doSomething.tostring()的日志,你将得到如下的字符串:

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

   以字符串的形式返回该方法让我们有获取参数的能力,更重要的是,能够获取到它们的名字。这其实是Angular实现它的依赖注入的方法。我偷了一点懒,直接截取Angular代码中获取参数的正则表达式:

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

   我们正则表达式的结果如下:

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

   我们只需要第二项,一旦我们清除空格并分割字符串后就可得到deps数组。只有一个大的改变:

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

   我们循环遍历dependencies数组,如果发现缺失项则尝试从arguments对象中获取。谢天谢地,当数组为空时,shift方法只是返回undefined,而不是抛出一个错误。新版的injector能像下面这样使用:

    injector.register('service', service);
    injector.register('router', router);
    var doSomething = injector.resolve(function(service, other, router) {
      console.log(service().name);
      console.log(router().name);
      console.log(other);
    });
    doSomething('Other');

   我们无需重写依赖关系,并且可以打乱传参顺序。我们复制了Angular的魔法!

   然而,这种做法并不完美,利用反射实现注入存在一个非常大的问题。代码压缩会破坏我们的逻辑,因为改变参数的名字,我们将无法保持正确的映射关系。例如,doSomething()压缩后可能看起来像这样:

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

   Angular团队提出的解决方案看起来像:

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

  }]);

   这看起来很像我们开始时的解决方案。我个人没能找到一个更好的解决方案,所以决定结合这两种方法。下面是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方法接受两个或三个参数,如果有两个参数它实际上和文章前面写的一样。然而,如果有三个参数,它会将第一个参数转换并填充deps数组,下面是一个测试例子:

    injector.register('service', service);
    injector.register('router', router);
    var doSomething = injector.resolve('router, , service', function(a, b, c) {
      console.log(a().name);
      console.log(b);
      console.log(c().name);
    });
    doSomething('Other');

   你可能注意到在第一个参数后面有两个逗号——注意这不是笔误。空值实际上代表"Other"参数(占位符),这显示了我们是如何控制参数顺序的。

直接注入

   有时候我还会用到注入的第三个变形。它涉及到操作函数的作用域(换句话说,就是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));
          }        
      }
  }

   我们所要做的其实就是把依赖添加到scope。好处就是,开发者不需要写依赖参数了,它们已经是函数作用域的一部分,测试用例如下:

  var doSomething = injector.resolve(['service', 'router'], function(other) {
      console.log(this.service().name);
      console.log(this.router().name);
      console.log(other);
  });
  doSomething("Other");

结语

   其实我们大部分人都用过依赖注入,只是我们没有意识到。即使你不知道这个术语,你可能在你的代码里用到它百万次了。希望这篇文章能加深你对它的了解!

参考

  •    Dependency injection in JavaScript

  •    JavaScript里的依赖注入

  •    关于JavaScript依赖注入:你应该了解的那些事

  •    控制反转

  • 建议继续学习

    1. STRUTS2类型转换错误导致OGNL表达式注入漏洞分析 (阅读 10,163)
    2. 程序员疫苗:代码注入 (阅读 7,863)
    3. MySQL防范SQL注入风险 (阅读 3,928)
    4. Android安全–加强版Smali Log注入 (阅读 3,704)
    5. 使用参数化查询防止SQL注入漏洞 (阅读 3,625)
    6. bash代码注入的安全漏洞 (阅读 3,323)
    7. XML实体注入漏洞安全警告 (阅读 3,263)
    8. JavaScript里的依赖注入 (阅读 2,962)
    9. 解决 SQL 注入的另类方法 (阅读 2,423)