javascript依赖注入详解

本文作者:言墨儿 原文出处:简书,csdn 未经同意,禁止转载

本人在研究前端自动化时,开始深入学习设计模式,由此引发了对依赖注入的学习,站在巨人的肩膀上参考了很多文章,在这里对这些作者表示敬仰,并赋予相关博客链接。

前端需要知道的 依赖注入(Dependency Injection, DI)-IMWeb 黎清龙
JavaScript里的依赖注入
ralph_zhu-细数Javascript技术栈中的四种依赖注入
AngularJS中文官网-AngularJS入门教程05:XHR和依赖注入
依赖注入那些事儿

前言

熟悉AngularJS的同学很快就能联想到,在injector注入之前,我们在定义module时还可以调用config方法来配置随后会被注入的对象。典型的例子就是在使用路由时对$routeProvider的配置。
可是,你真的了解 依赖注入(Dependency Injection, DI) 吗?
本文将详细解释什么是依赖注入,并简单明了的解释属于前端的依赖注入

注意:本文专门为前端同学解释什么是依赖注入,文中例子也是js

1.什么是 依赖注入

1.1它是设计模式

首先,依赖注入是一个设计模式,因为它解决的是一类问题。这类问题是什么呢?这类问题和依赖有关系。

1.2依赖倒转原则

要知道依赖注入是解决什么问题,最好先了解一个原则:依赖倒转原则。

依赖倒转原则(Dependence Inversion Priciple, DIP)提倡:

. 高层模块不应该依赖低层模块。两个都应该依赖抽象
. 抽象不应该依赖细节,细节应该依赖抽象
. 针对接口编程,不要针对实现编程

要想讲明白这个设计模式得先给大家说一个现实场景的案例:主板和内存条

大家都知道内存条依赖主板,内存条坏了和主板无关,主板坏了也和内存条无关,可以把电脑理解成是大的软件系统,任何部件相互依赖,但又彼此独立,即这些部件就是电脑中封装的类或程序集,在电脑里这叫易插拔,在编程中这叫强内聚低耦合。

内存模块(高层模块)不依赖主板模块(低层模块),它们依赖的是被抽象的接口(模块依赖都应该依赖抽象),抽象不应该依赖细节,细节应该依赖抽象这句话说白了,就是要针对借口编程,不要对实现编程。无论主板、cpu、内存都是针对接口设计的,都是标准的接口,如果针对实现来设计,内存要对应到具体的厂商主板,内存坏了主板也得换。

内存和主板的依赖关系

这也就是说:在编程时,我们对系统进行模块化,它们之间有依赖,比如模块A依赖模块B
那么依据依赖倒转原则,模块A应该依赖模块B的接口,而不应该依赖模块B的实现。

模块A依赖模块B示例

虽然模块A只依赖接口编程,但在运行的时候,它还是需要有一个具体的模块来负责模块A需要的功能的,所以模块A在【运行时】是需要一个【真的】模块B,而不是它的接口。即模块A在【运行时】需要有一个接口的实现模块作为它的属性。

那么这个实现模块怎么来?它是怎么初始化,然后怎么传给模块A的?

解决这个问题的就是依赖注入

1.3前端的依赖注入

依赖注入更多的是后端的概念,对于前端来说,很少有抽象,更别说有接口了。但是,依赖注入却是一直都存在,只是许多人没有认出来而已。

比如用过vue的都应该知道main.js,其实他就是一个依赖注入,我们见过有这样一段代码。

import ElementUI from 'element-ui' // vue的ui组件-(饿了么-ui)element-ui
Vue.use(ElementUI)

其实就是我们的需要的模块依赖vue模块,main.js就是vue模块抽象出来的接口,这里使用Vue.use(),把我们需要的模块vue注入进来,然后我们就可以用它了。

这是个很普通的代码,太正常了,我们每天都会写这些代码。

其实依赖注入它只做两件事:

. 初始化被依赖的模块
. 注入到依赖模块中

这个时候应该知道了,import ElementUI from 'element-ui',初始化了被依赖的模块;而Vue.use(ElementUI)是把我们依赖的模块注入到依赖模块中。

我们不依赖element-ui的具体实现,我们只是使用他这个库的抽象接口而已,比如它的button组件。

1.4依赖注入的的作用

看了上面我们常用的引入是依赖注入,是不是有点吃惊?不用觉得这是什么很屌的是,关于这个我们仔细深究她的作用:

初始化被依赖的模块
注入到依赖模块中

我们为什么需要依赖注入呢?

1.初始化被依赖的模块

如果不通过依赖注入模式来初始化被依赖的模块,那么就要依赖模块自己去初始化了
那么问题来了:依赖模块就耦合了被依赖模块的初始化信息了

2. 注入到依赖模块中

被依赖模块已经被其他管理器初始化了,那么依赖模块要怎么获取这个模块呢?

有两种方式:

. 自己去问
. 别人主动给你

没用依赖注入模式的话是1,用了之后就是2

想想,你需要某个东西的时候,你去找别人要,你需要提供别人什么信息?最简单的就是那个东西叫什么,即你需要提供一个名称。
所以,方式1的问题是:依赖模块耦合了被依赖模块的【名称】还有那个【别人】
而方式2解决了这个问题,让依赖模块只依赖需要的模块的接口。

2.代码说明一切

让我们写下一些我们的依赖注入解决办法应该达到的目标:

  1. 我们首先得为模块依赖提供抽象的接口
  2. 下来应该能够注册依赖关系
  3. 在注册这个依赖关系后有地方存储它
  4. 存储后,我们应该把被依赖的模块注入依赖模块中
  5. 注入应该保持被传递函数的作用域
  6. 被传递的函数应该能够接受自定义参数,而不仅仅是依赖描述

基于Injector、dependencies和函数参数名的依赖注入

设想我们有1个模块Student,它依赖3个模块Notebook、Pencil、School

function Notebook() {}
Notebook.prototype.notebookName = function () {
  return 'this is a notebook'
}

function Pencil() {}
Pencil.prototype.printName = function () {
  return 'this is a pencil'
}

function School() {}
School.prototype.schoolName = function () {
  return '清华'
}

function Student() {}
Student.prototype.write = function (notebook, pencil, school) {
  if (!notebook || !pencil || !school) {
    throw new Error('Dependencies not provided!')
  }
  console.log('writing...')
  console.log(notebook)
  console.log(pencil)
  console.log(school)
  return '我拥有School、Pencil和Notebook'
}

我们需要在Student中用到,它依赖的3个模块Notebook、Pencil、School,记得依赖注入的功能:初始化被依赖的模块,注入到依赖模块中。

知道这些根据我们的目标,下面开始我们的injector接口,第一种方法:从方法中解析出参数的依赖注入

var injector = { // 依赖注入的抽象接口
  dependencies: {}, // 存储被依赖的模块
  register: function (key, value) { // 注册初始化被依赖的模块
    this.dependencies[key] = value
  },
  resolve: function (deps, func, scope) { // 注入到依赖的模块中,注入应该接受一个函数,并返回一个我们需要的函数
    var paramNames = this.getParamNames(func) // 取得参数名
    var params = []
    for (var i = 0; i < paramNames.length; i++) { // 通过参数名在dependencies中取出相应的依赖
      let d = paramNames[i]
      let depen = this.dependencies[d] || deps[i]
      if (depen) {
        params.push(depen)
      } else {
        throw new Error('缺失的依赖:' + d)
      }
    }
    // 注入依赖,执行,并返回一个我们需要的函数
    return func.apply(scope || {}, params) // 将func作用域中的this关键字绑定到bind对象上,bind对象可以为空
  },
  getParamNames: function (func) { // 获取方法的参数名字
    var paramNames = func.toString().match(/^function\s*[^\(]*\(\s*([^\)]*)\)/m)[1]
    paramNames = paramNames.replace(/ /g, '')
    paramNames = paramNames.split(',')
    return paramNames // Array
  }
}

这里我们使用register方法来注册初始化被依赖的模块,使用dependencies来存储被依赖的模块,resolve来进行注入,getParamNames来获取我们的传递方法的参数,注入依赖,执行,并返回一个我们需要的函数。

调用代码:

injector.register('notebook', new Notebook()) // 注册notebook
injector.register('pencil', new Pencil()) // 注册pencil
var school = new School()
var student = new Student()
// 以参数的形式传入school
var studentWrite = injector.resolve([, , school], student.write, student)
console.log(studentWrite) // "我拥有School、Pencil和Notebook"

执行结果:

执行结果

上面我们通过injector(注入器、注射器)向write方法提供它所需要的依赖。通过依赖注入,函数的执行和其所依赖对象的创建逻辑就被解耦开来了。这里我们初始化的方式有两种:一种是通过injector.register来注册,一种是直接使用injector.resolve中的deps来注册其依赖的。

这种方法我们是从方法中解析出参数她的参数得知他所依赖的模块。下面来说说另外一种方法:从方法中解析出参数及参数声明的依赖注入。

var injector = { // 依赖注入的抽象接口
  dependencies: {}, // 存储被依赖的模块
  isDeclare: false, // 是否为参数声明
  param: [], // 声明参数存储
  paramDeclare: function (param) { // 依赖注入参数声明
    if (Object.prototype.toString.call(param) !== '[object Array]') {
      try {
        throw new Error('接受的是一个数组,但是却得到一个' + typeof filterArray)
      } catch (e) {
        console.error(e)
      }
    }
    this.param = param.concat()
    console.log(this.param)
    this.isDeclare = true
  },
  register: function (key, value) { // 注册初始化被依赖的模块
    this.dependencies[key] = value
  },
  resolve: function (deps, func, scope) { // 注入到依赖的模块中,注入应该接受一个函数,并返回一个我们需要的函数
    console.log(deps)
    var paramNames = this.isDeclare ? this.param : this.getParamNames(func) // 取得参数名
    if (paramNames.length === 0 && this.isDeclare) {
      throw new Error('缺失的参数声明')
    }
    if (paramNames.length === 0 && !this.isDeclare) {
      throw new Error('该方法没有参数依赖')
    }
    var params = []
    for (var i = 0; i < paramNames.length; i++) { // 通过参数名在dependencies中取出相应的依赖
      let d = paramNames[i]
      let depen = this.dependencies[d] || deps[i]
      if (depen) {
        params.push(depen)
      } else {
        throw new Error('缺失的依赖:' + d)
      }
    }
    // 注入依赖,执行,并返回一个我们需要的函数
    return func.apply(scope || {}, params) // 将func作用域中的this关键字绑定到bind对象上,bind对象可以为空
  },
  getParamNames: function (func) { // 获取方法的参数名字
    var paramNames = func.toString().match(/^function\s*[^\(]*\(\s*([^\)]*)\)/m)[1]
    paramNames = paramNames.replace(/ /g, '')
    paramNames = paramNames.split(',')
    return paramNames // Array
  }
}

调用代码1:

injector.register('notebook', new Notebook()) // 注册notebook
injector.register('pencil', new Pencil()) // 注册pencil
var school = new School()
var student = new Student()
// 以参数的形式传入school
var studentWrite = injector.resolve([, , school], student.write, student)
console.log(studentWrite) // "我拥有School、Pencil和Notebook"

调用代码2:

injector.register('notebook', new Notebook())
injector.register('pencil', new Pencil())
injector.paramDeclare(['notebook', 'pencil', 'school']) // injector.paramDeclare-依赖注入参数声明
var school = new School()
var student = new Student()
var studentWrite = injector.resolve([, , school], student.write, student)
console.log(studentWrite) // "我拥有School、Pencil和Notebook"

结语

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

提示:后面还有精彩敬请期待,请大家关注我的专题:web前端。如有意见可以进行评论,每一条评论我都会认真对待。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容

  • 此文为本人学习guice的过程中,翻译的官方文档,如有不对的地方,欢迎指出。另外还有一些附件说明、吐槽、疑问点,持...
    李眼镜阅读 3,429评论 2 5
  • 版本:Angular 5.0.0-alpha 依赖注入是重要的应用设计模式。它使用得非常广泛,以至于几乎每个人都称...
    soojade阅读 2,974评论 0 3
  • 依赖注入(Dependency Injection) 依赖注入最大的特点就是:帮助我们开发出松散耦合(loose ...
    小李龍彪阅读 2,377评论 1 0
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,494评论 18 139
  • (一)Java部分 1、列举出JAVA中6个比较常用的包【天威诚信面试题】 【参考答案】 java.lang;ja...
    独云阅读 7,028评论 0 62