联系作者
联系作者

# 4.1 设计模式概论

# 4.1.1 代码与设计模式

# 🍅 我们写代码到底是在写什么?

我们写项目其实就是写模块然后设计它们之间的沟通,设计模式说白了就是帮助我们更好的设计模块, 更好的组织它们之间的沟通。

# 🍅 设计模式扮演的角色

  • 帮助我们组织模块

    通过一些设计模式,组织模块间的组成结构

  • 帮助我们设计沟通

    有的设计模式可以帮助我们设计模块间如何沟通

  • 提高代码质量

    通过设计模式,让代码更加优雅

# 🍅 设计原则

  1. 开闭原则

    我们的程序要对扩展开放,对修改关闭;我们的程序要给具体使用的时候扩展的接口,但是在具体使用的时候不能让其修改我们的源码, 也就是说我们不用修改源码就能扩展功能,像 vue,react 等都有扩展的接口。

  2. 单一职责原则

    我们的模块只做一件事情,模块的职责越单一越好。

  3. 依赖倒置原则

    我们的上层模块不要依赖与具体的下层模块,应该依赖于抽象

假如下面是一套点餐系统,我们在上层和下层之间加一个抽象层;下层如何变动都不会影响到上层,只需更改抽象层即可。

// 具体层
function Food1() {}
function Food2() {}
function Food3() {}

// 抽象层
function resturn(food) {
  var list = {
    food1: new Food1(),
    food2: new Food2(),
    food3: new Food3()
  }
  return list[food]
}

// 上层
function order(food) {
  return resturn(food)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  1. 接口隔离原则

我们的接口要细化,功能要单一,一个接口不要调用太多方法,使其能力单一,听起来像单一职责原则;但是 2 者的关注点不同, 单一职责原则主要关注于模块本身,接口隔离原则关注于接口;我们尽量细化接口,每个接口做的事情尽量单一化。

  1. 迪米特法则

我们让 2 个对象之间产生沟通,我们最好让 2 个对象之间知道的越少越好,没必要 2 者之间非常的了解;我们的中介者模式是一个很好体现迪米特法则的设计模式,中介者模式让 2 个对象之间没必要直接的沟通,如果直接沟通需要了解 2 者之间的 api 和彼此的调用方式,这个时候我们可以采用一个中介者来转达我们的需求,而不用彼此知道

  1. 里氏替换原则

它主要关注于继承,它的意义是任何使用父类的地方都可以用子类去替换,直白的说我们子类继承父类的时候,我们的子类必须完全保证继承父类的属性和方法,这样的话父类使用的地方,子类可以进行替换

后面学习到设计模式都是在体现这些设计原则

# 4.1.2 设计模式的分类

  1. 创建型

这些设计模式可以帮助我们优雅地创建对象

  1. 结构型

帮助我们优雅地设计代码结构

  1. 行为型

模块之间行为的模式总结,帮助我们组织模块行为

  1. 技巧型

一些帮助我们优化代码的技巧

# 🍅 创建型

  • 工厂模式-大量创建对象

  • 单例模式-全局只能有我一个

  • 建造者模式-精细化组合对象

  • 原型模式-javaScript 的灵魂

# 🍅 结构型

  • 外观模式-给你的一个套餐

  • 适配器模式-用适配代替更改

  • 装饰者模式-更优雅地扩展需求

  • 享元模式-共享来减少数量

  • 桥接模式-独立出来,然后再对接过去

# 🍅 行为型

  • 观察者模式-我作为第三方转发

  • 状态模式-用状态代替判断

  • 策略模式-算法工厂

  • 职责链模式-像生产线一样组织模块

  • 命令模式-用命令去解耦

  • 迭代器模式-告别 for 循环

# 🍅 技巧模式

  • 链模式-链式调用

  • 委托模式-让别人代替你收快递

  • 数据访问模式-一个方便的数据管理器

  • 惰性模式-我要搞机器学习(第一次执行完后把状态记录下来)

  • 等待者模式-等你们都回来再吃饭

# 4.2 封装对象

创建型设计模式到底是怎么样使用的,利用创建型设计模式更好的封装代码更好的创建对象

封装的目的?

  • 定义的变量不会污染到外部

  • 能够作为一个模块调用

  • 遵循开闭原则

什么是好的封装?

  • 变量外部不可见

  • 调用接口使用

  • 留出扩展接口

# 4.2.1 封装对象时的设计模式

# 🍅 创建一个对象的模式

  • 工厂模式

    目的:方便我们大量创建对象

    应用场景:当某一个对象需要经常创建的时候

  • 建造者模式

    目的:需要组合出一个全局对象

    应用场景:当要创建单个、庞大的组合对象时

# 🍅 保障对象全局只有一个

  • 单例模式

    目的:需要确保全局只有一个对象

    应用场景:为了避免重复新建,避免多个对象存在相互干扰

# 4.2.2 基本结构

# 🍅 工厂模式的基本结构

function Factory (type) {
    switch (type) {
        case 'type1'
        return new Type1()
        case 'type2'
        return new Type2()
        case 'type3'
        return new Type3()
    }
}
1
2
3
4
5
6
7
8
9
10

工厂模式就是写一个方法,只需调用这个方法,就能拿到你要的对象

# 🍅 建造者模式的基本结构

//  模块1
function Mode1() {}
// 模块2
function Mode2() {}
// 最终使用的类
function Final() {
  this.mode1 = new Model()
  this.mode2 = new Mode2()
}
1
2
3
4
5
6
7
8
9

把一个复杂的类各个部分,拆分成独立的类,然后再最终类里组合到一块,final 为最终给出去的类

# 🍅 单例模式的基本结构

单例模式的做法不是很固定,我们更重要的记住是保证全局只有一个对象的思想

// 作为单例实例化的对象
let SingLeton = function(name) {
  this.name = name
}
/*
在SingLeton挂在一个getInstance方法,只能通过getInstance方法来获取
SingLeton的实力化对象
*/
SingLeton.getInstance = function(name) {
  if (this.instance) {
    return this.instance
  }
  return (this.instance = new SingLeton(name))
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

通过定义一个方法,使用时只允许通过此方法拿到存在内部的同一实例化对象

# 4.2.3 应用示例

# 🍅 工厂模式的示例

实现一个多彩的弹窗?

需求:弹窗有多种,它们之间存在内容和颜色上的差异

;(function(ROOT) {
  // 消息弹性
  function infoPop() {}
  // 确认弹窗
  function confirmPop() {}
  // 取消弹窗
  function cancelPop() {}
  // 弹窗工厂
  function pop(type, content, color) {
    switch (type) {
      case 'infoPop':
        return new infoPop(content, color)
      case 'confirmPop':
        return new confirmPop(content, color)
      case 'confircancelPopmPop':
        return new cancelPop(content, color)
    }
  }
  ROOT.pop = pop
})(window)
// 根据传入不同的参数来,来弹出不同的弹窗
pop('infoPop', '开始', 'white')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

上面这种写法有个弊端就是不能 new pop,当用户用 new 关键词就不适用了

;(function(ROOT) {
  function pop(type, content, color) {
    if (this instanceof pop) {
      var s = this[type](content, color)
      return
    } else {
      return new pop(type, content, color)
    }
  }
  pop.prototype.infoPop = function() {}
  pop.prototype.confirmPop = function() {}
  pop.prototype.cancelPop = function() {}
  ROOT.pop = pop
})(window)

pop('infoPop', '开始', 'white')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

jQuery 源码示例

需求:jQuery 需要操作 dom,每个 dom 都是一个 jq 对象

/*
 seletor 选择器
 context 上下文
*/
;(function(ROOT) {
  var jQuery = function() {
    //之所以不new jQuery本身,避免递归造成无限循环,我们用的Query.fn上的init方法代替
    return new jQuery.fn.init(seletor, context)
  }
  // JQuery各种操作都会挂载到prototype上
  JQuery.fn = JQuery.prototype = {
    init: function() {}
  }
  jQuery.fn.init.prototype = jQuery.fn
  // extend方法把方法拷贝extend对象上
  jQuery.extend = jQuery.fn.extend = function() {}

  jQuery.extend({})

  ROOT.$ = ROOT.jQuery = jQuery
})(window)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

总结:工厂模式就是要把我们要暴露的对象,真正要实例化的对象先封装到函数的内部,然后我们只暴露一个工厂方法,使用者通过这个工厂方法 来获取我们实例话的对象,它的优势方便我们大量的创建对象。

# 🍅 建造者模式的示例

编写一个编辑器插件

需求:有一个编辑器插件,初始化的时候需要配置大量参数,而且内部功能很多

// 最终类
function Editor() {
  this.intt = new initHTML()
  this.fontControll = new fontControll()
  this.stateControll = new stateControll()
}

// 初始化html的类,最终渲染成dom
function initHTML() {}
// 初始化控制样式的方法
initHTML.prototype.initStyle = function() {}
// 初始化渲染成dom的方法
initHTML.prototype.renderDom = function() {}

// 改变字体颜色大小的类
function fontControll() {}
// 控制颜色的方法
fontControll.prototype.changeColor = function() {}
// 控制字体大小的方法
fontControll.prototype.changeFontsize = function() {}

// 改变状态类,为了前进后退
function stateControll() {
  this.state = [] //存储状态
  this.nowstate = 0 //状态指针
}
//保存状态的方法
stateControll.prototype.saveState = function() {}
//回滚状态的方法
stateControll.prototype.saveBack = function() {
  var state = this.state[this.nowstate - 1]
  this.fontControll.changeColor(state.color)
  this.fontControll.changeFontsize(state.color)
}
//前进状态的方法
stateControll.prototype.saveGo = function() {}

window.Editor = Editor
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

建造者模式是把它的模块抽离出独立的类,然后在组合起来

vue 初始化

需求:vue 内部众多模块,而且过程复杂,vue 类也可以看做是一个建造者模式

function Vue(options){
    // 只允许用户用new操作符,如果直接调用就抛出警告
    if(!(this instanceof Vue)){
        console.warn('Vue is a constructor and should be called with the `new` keyword')
    }
    // 初始化配置项
    this._init(options)
}

initMixin(Vue) // 初始化系统混入到Vue中去
stateMixin(Vue) // 状态系统混入到Vue中去
eventsMixin(Vue) // 事件系统的混入
lifecycleMixin(Vue) // 生命周期混入
renderMixin(Vue)  // 渲染混入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

通过这些方法和我们上一个案例把模块独立成一个类,把这些类放到暴露出的类里面是一个道理; 只不过这里改成方法,vue 中所有的功能都是独立开发,通过这一系列的混入将其混入进去

# 🍅 单例模式的示例

写一个数据存储对象

需求:项目中有一个全局的数据存储者,这个存储者只能有一个 ,不然会需要进行同步,增加复杂度

function store() {
  this.state = {}
  if (store.install) {
    return store.install
  }
  store.install = this
}

store.install = null
var s1 = new store()
var s2 = new store()
s1.state.a = 1
console.log(s1, s2) // store { state: { a: 1 } } store { state: { a: 1 } }
1
2
3
4
5
6
7
8
9
10
11
12
13

vue-router

需求:vue-router 必须 保障全局有且只有一个,否则的话会错乱

let _Vue
function install(Vue) {
  if (install.installed && _Vue === vue) return
  install.installed = true
  _Vue = vue
}
1
2
3
4
5
6

# 4.2.4 总结

工厂模式:如果你写的模块,需要大量创建类似的对象

建造者模式:需要创建一个需要大量参数,且内部模块庞大

单例模式:防止重复注册,防止有多个对象互相干扰

# 4.3 提高复用性

什么是好的复用?

  • 对象可以再重复使用,不用修改
  • 重复代码少
  • 模块功能单一

# 4.3.1 提高复用性的设计模式

# 🍅 减少代码数量,高效复用代码

  • 桥接模式

    目的:通过桥接代替耦合

    应用场景:减少模块之间的耦合

  • 享元模式

    目的:减少对象/代码数量

    应用场景:当代码中创建了大量类似对象和类似的代码块

# 🍅 创建高可复用性的代码

  • 模版方法模式

    目的:定义一系列操作的骨架,简化后面类似操作的内容

    应用场景:当项目中出现很多类似操作内容

# 4.3.2 基本结构

# 🍅 桥接模式

// 有三种形状,每种形状都有3种颜色
function rect(color) {
  //矩形
  showcolor(color)
}
function circle() {
  // 圆形
  showcolor(clor)
}
function delta(color) {
  // 三角形
  showcolor(clor)
}

new circle('red')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  • 对于 3 种形状、每种形状有 3 种颜色的需求,可以不用创建 9 种不同颜色的不同形状
  • 这个模式把重复的方法抽取出来,然后在桥接出去

这个模式跟我们的建造者模式很类似拆分再组合,建造者模式的核心是如何去构造对象;而我们桥接模式是如何简化我们的代码,提高我们的可复用性,一个关注的是功能一个关注的是创建,这是它们的区别。

# 🍅 享元模式

    // 有一百种不同文字的弹窗,每种弹窗行为相同,但是文字和样式不同,我们没必要新间一百个弹窗对象
    function Pop(){
    }
    // 保留同样的行为
    Pop.prototype.action=function(){}
    //显示
    Pop.prototype.show=function(){}
    // 提取出每个弹窗不同的部分作为一个外部数组
    var popArr=[
        {text:"window1",style:[400,400]}
        {text:"window2",style:[400,200]}
    ]

    var poper=new Pop()

    for(var i=0;i<100;i++){
        poper.show(popArr[i])
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  • 只需一个类,不需要 new 一百次弹窗
  • 这个类只保留所有弹窗共有的,每个弹窗不同的部分留作为一个公共享元

# 🍅 模版方法模式

// 编写导航组件,有的带消息提示,有的竖着,有的横者
function baseNav() {
  // 基础类,此处定下基本骨架
}

baseNav.prototype.action = function(fn) {
  // 特异性的处理,留一个回调等待具体实现
}
1
2
3
4
5
6
7
8
  • 导航组件多种多样,可能后面还会新增类型,那么我们不妨写一个基础的组件库,然后具体的实现,延迟到具体的使用时

# 4.3.3 应用示例

# 🍅 桥接模式的示例

创建不同的选中效果

需求:有一组菜单,上面每种选项都有不同的选中效果

//  一般做法
//menu1,menu2,menu3
function menuItem(word) {
  this.word = ''
  this.dom = document.createElement('div')
  this.dom.innerHTML = this.word
}
var menu1 = new menuItem('menu1')
var menu2 = new menuItem('menu2')
var menu3 = new menuItem('menu3')
menu1.onmouseover = function() {
  menu1.style.color = 'red'
}
menu2.onmouseover = function() {
  menu1.style.color = 'green'
}
menu3.onmouseover = function() {
  menu1.style.color = 'blue'
}
menu1.onmouseout = function() {
  menu1.style.color = 'white'
}
menu2.onmouseout = function() {
  menu1.style.color = 'white'
}
menu3.onmouseout = function() {
  menu1.style.color = 'white'
}

// 桥接模式做法
function menuItem(word, color) {
  this.word = word
  this.color = color
  this.dom = document.createElement('div')
  this.dom.innerHTML = this.word
  document.getElementById('app').appendChild(this.dom)
}

menuItem.prototype.bind = function() {
  var self = this
  this.dom.onmouseover = function() {
    console.log(self.color)
    this.style.color = self.color.colorOver
  }
  this.dom.onmouseout = function() {
    this.style.color = self.color.colorOut
  }
}
function menuColor(colorover, colorout) {
  this.colorOver = colorover
  this.colorOut = colorout
}

var data = [
  { word: 'menu1', color: ['red', 'black'] },
  { word: 'menu2', color: ['green', 'black'] },
  { word: 'menu3', color: ['blue', 'black'] }
]
for (var i = 0; i < data.length; i++) {
  new menuItem(
    data[i].word,
    new menuColor(data[i].color[0], data[i].color[1])
  ).bind()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64

Express 中创建 get 等方法

需求:express 中 get,post 等方法,有七八个,如何快速地创建。

var methods = ['get', 'post', 'delete', 'put']
methods.forEach(function(method) {
  app[method] = function() {
    //利用桥接过来的route来完成post,get等请求
    route[method].apply(route, slice.call(arguments, 1))
  }
})
1
2
3
4
5
6
7

# 🍅 享元模式的示例

文件上传

需求:项目中有一个文件上传功能,该功能可以上传多个文件

一般做法

//文件上传
function uploader(fileType, file) {
  this.fileType = fileType
  this.file = file
}
uploader.prototype.init = function() {
  //初始化文件上传的html
}
uploader.prototype.delete = function() {
  //删除掉该html
}
uploader.prototype.uploading = function() {
  //上传
}

new uploader('img', fileob1)
new uploader('txt', fileob2)
new uploader('img', fileob3)
new uploader('word', fileob4)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

享元模式

//fileType,file
function uploader() {}
uploader.prototype.init = function() {
  //初始化文件上传的html
}
uploader.prototype.delete = function() {
  //删除掉该html
}
uploader.prototype.uploading = function(filetype, file) {}

var fileob1, fileob2, fileob3, fileob4
var data = [
  {
    type: 'img',
    file: fileob1
  },
  {
    type: 'txt',
    file: fileob2
  },
  {
    type: 'img',
    file: fileob3
  },
  {
    type: 'word',
    file: fileob4
  }
]
var uploader = new uploader()
for (var i = 0; i < data.length; i++) {
  uploader.uploading(data[i].type, data[i].file)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

jQuery 的 extend

需求:extends 方法,需要判断参数数量来进行不同的操作

// 一般做法
var jQuery = {}
jQuery.fn = {}
jQuery.extend = jQuery.fn.extend = function() {
  if (arguments.length == 1) {
    for (var item in arguments[0]) {
      this[item] = arguments[0][item]
    }
  } else if (arguments.length == 2) {
    for (var item in arguments[1]) {
      arguments[0][item] = arguments[1][item]
    }
    return arguments[0]
  }
}

// 享元做法,保留一个公共的for循环
jQuery.extend = jQuery.fn.extend = function() {
  var target = arguments[0]
  var source
  if (arguments.length == 1) {
    target = this
    source = arguments[0]
  } else if (arguments.length == 2) {
    target = arguments[0]
    source = arguments[1]
  }
  for (var item in source) {
    target[item] = source[item]
  }
  return target
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

# 🍅 模版方法模式的示例

编写一个弹窗组件

需求:项目有一系列弹窗,每个弹窗的行为、大小、文字不同

function basePop(word, size) {
  this.word = word
  this.size = size
  this.dom = null
}
basePop.prototype.init = function() {
  var div = document.createElement('div')
  div.innerHTML = this.word
  div.style.width = this.size.width + 'px'
  div.style.height = this.size.height + 'px'
  this.dom = div
}
basePop.prototype.hidden = function() {
  //定义基础操作
  this.dom.style.display = 'none'
}
basePop.prototype.confirm = function() {
  //定义基础操作
  this.dom.style.display = 'none'
}
function ajaxPop(word, size) {
  basePop.call(this, word, size)
}
ajaxPop.prototype = new basePop()
var hidden = ajaxPop.prototype.hidden
ajaxPop.prototype.hidden = function() {
  hidden.call(this)
  console.log(1)
}
var confirm = ajaxPop.prototype.confirm
ajaxPop.prototype.confirm = function() {
  confirm.call(this)
  console.log(1)
}
var pop = new ajaxPop('sendmes', { width: 100, height: 300 })
pop.init()
pop.confirm()

var axios = {
  get: function() {
    return Promise.resolve()
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

上面这个就是我们面向对象中的继承,模版模式不一定非要通过继承方式来完成,它强调先定义后面进行不同维度的操作的基本行为;

然后在这个基本行为上有扩展的空间,这就是我们模版方法的目的。

封装一个算法计算器

需求:现在我们有一系列自己的算法,但是这个算法常在不同的地方需要增加一些不同的操作

//算法计算器

function counter() {
  this.beforeCounter = []
  this.afterCounter = []
}

//然后我们把具体的不同部分留到具体使用的时候去扩展
//所以我们定义两个方法来扩展
counter.prototype.addBefore = function(fn) {
  this.beforeCounter.push(fn)
}
counter.prototype.addAfter = function(fn) {
  this.afterCounter.push(fn)
}

//最终计算方法
counter.prototype.count = function(num) {
  //结果变量
  var _resultnum = num
  //算法队列数组组装
  var _arr = [baseCount]
  _arr = this.beforeCounter.concat(_arr)
  _arr = _arr.concat(this.afterCounter)
  //不同部分的相同算法骨架
  function baseCount(num) {
    num += 4
    num *= 2
    return num
  }
  //循环执行算法队列
  while (_arr.length > 0) {
    _resultnum = _arr.shift()(_resultnum)
  }
  return _resultnum
}
//使用
var countObject = new counter()
countObject.addBefore(function(num) {
  num--
  return num
})
countObject.addAfter(function(num) {
  num *= 2
  return num
})
countObject.count(10)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

这个应用了组合实现模版模式

javascript 的组合与继承

  • 组合(推荐)

    1. javascript 最初没有专门的继承,所以最初 javascript 推崇函数式的编程,然后进行统一组合桥接到一起
    2. 桥接模式可以看出组合的一种体现,组合的好处是耦合低,方便方法复用,方便扩展
  • 继承

    1. es6 出现 class 与 extend,继承的方式多种多样,但是都是各有弊端
    2. 模版方法模式可以看出继承的一种体现,继承的好处是可以自动获得父类的内容与接口,方便统一化

# 4.3.4 总结

  • 桥接模式

通过独立方法间接的桥接来形成整体功能,这样每个方法都可以被高度复用

  • 享元模式

提取出公有部分与私有部分,私有部分作为外部数据传入,从而减少对象数量

  • 模版方法模式

当一个功能朝着多样化发展,不妨先定义一个基础的,把具体实现延迟到后面

# 4.4 提高可扩展性(1)

提高可扩展性的目的

  • 面对需求变更,方便更该需求
  • 减少代码修改的难度

什么是好的扩展

  • 需求的变更,不需要重写
  • 代码修改不会引起大规模变动
  • 方便加入新模块

# 4.4.1 提高可扩展性的设计模式

# 🍅 更好的更改代码

  • 适配器模式(接口)

适配器模式的目的:通过写一个适配器,来代替替换

适配器模式的应用场景:面临接口不通用的问题

  • 装饰者模式(方法作用)

装饰者模式的目的:不重写方法的扩展方法

装饰者模式的应用场景:放一个方法需要扩展,但是又不好去修改方法

# 🍅 解耦你得方法与调用

  • 命令模式

命令模式的目的:解耦实现和调用,让双方互不干扰

命令模式的应用场景:调用的命令充满不确性

# 4.4.2 基本结构

# 🍅 适配器模式的基本结构

var log = function() {
  return winodw.console.log
}
1
2
3

想用 log 来代替 console.log

# 🍅 装饰者模式的基本结构

// 在一个他人写好的模版a内部调用b,模块为他人写好,不能修改,如何扩展b方法?
var a = {
  b: function() {}
}
function myb() {
  a.b()
  // 要扩展的方法
}
1
2
3
4
5
6
7
8

我们新建一个自己的方法,在其内部调用 b 方法,并且再执行自己的方法,这样可以在不修改原方法的情况下扩展方法

# 🍅 命令模式的基本结构

  var command=(function(){
      // action中定义了各种方法
      var action={}
      // excure可以调用action方法
      return function excure()
  })()
  // command只需输入命令就可以调用action里的方法
1
2
3
4
5
6
7

# 4.4.3 应用示例

# 🍅 适配器模式示例

框架的变更

需求:目前项目使用的 A 框架,现在改成了 B,2 个框架与十分类似,但是有少数几个方法不同

// A框架调用的方式
A.c()
// 假如我们项目中换成了jQuey,我们不想全部去替换A方法,就用适配器的方法
A.c = function() {
  return $.on.apply(this.arguments)
}
1
2
3
4
5
6

参数适配

需求:为了避免参数不适配产生问题,很多框架会有一个参数适配操作

// 给参数适配,没传值给默认值
function f1() {
  var _defalut = {
    name: '',
    color: ''
  }
  for (var item in _defalut) {
    _defalut[item] = config[item] || _defalut[item]
  }
  return _defalut
}
1
2
3
4
5
6
7
8
9
10
11

# 🍅 装饰者模式示例

扩展你的已有事件绑定

需求:现在项目改造,需要给 input 标签已经有的事件增加一些操作

var decorator = function(dom, fn) {
  if ((typeof dom.onclick = 'function')) {
    var _old = dom.onclick
    dom.onclick = function() {
      _old()
      fn()
    }
  }
}
decorator(document.getElementById('dom1'), function() {
  // 自己的操作
})
1
2
3
4
5
6
7
8
9
10
11
12

Vue 的数组监听

需求:vue 中利用 defineProperty 可以监听对象,那么数组怎么办

var arrayProto = Array.prototype
var arrayMethods = Object.create(arrayProto)
var methodsToPatch = [
  'push',
  'pop',
  'unshift',
  'shift',
  'splice',
  'resverse',
  'sort'
]

methodsToPatch.forEach(method => {
  var original = arrayMethods[method]
  object.defineProperty(arrayMethods, method, {
    value(...args) {
      const result = original.apply(this, args)
      dep.notify()
      return result
    }
  })
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

装饰者模式,拿到老方法,调用老方法,组成新方法

# 🍅 命令模式示例

需求:封装一系列的 canvas 绘图命令

var myCanvas = function() {}
myCanvas.prototype.drawCircle = function() {}
myCanvas.prototype.drawRect = function() {}

var canvasComand = (function() {
  var action = {
    drawCircle: function(config) {},
    drawRect: function(config) {}
  }
  return function excute(commander) {
    commander.forEach(item => {
      action[item.command](item.config)
    })
  }
})()

myCanvas([{ command: 'drawReact', config: {} }])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  1. 用户只管输入他要的命令,不用关心 api

  2. 命令和实现解耦,无论命令发生变动,还是实现发生变动,都不会彼此影响

绘制随数量图片

需求:需要做一个画廊,图片数量和排列顺序随机

var createImg = (function() {
  var action = {
    create: function(obj) {
      var htmlArr = []
      var _htmlstring = ''
      var _htmlTemplate =
        "<div><img src='{{img-url}}' /></div><h2>{{title}}</h2>"
      var displayWay = {
        normal: function(arr) {
          return arr
        },
        reverse: function(arr) {
          return arr.reverse
        }
      }

      obj.imgArr.forEach(img => {
        var _html
        _html = _htmlTemplate
          .replace('{{img-url}}', img.img)
          .replace('{{title}}', img.title)
        htmlArr.push(_html)
      })
      htmlArr = displayWay[obj.type](htmlArr)
      _htmlstring = htmlArr.join('')
      return '<div>' + _htmlstring + '</div>'
    },
    display: function(obj) {
      var _html = this.create(obj)
      obj.target.innerHTML = _html
    }
  }

  return function excute(obj) {
    var _default = {
      imgArr: [{ img: 'xxxx', title: 'default title' }],
      type: 'normal',
      target: document.body
    }
    for (var item in _default) {
      _default[item] = obj[item] || _default[item]
    }
    action.display(_default)
  }
})()
createImg({
  imgArr: [
    { img: 'xxxx', title: 'default title1' },
    { img: 'xxxx', title: 'default title2' }
  ],
  type: 'normal'
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

数据-> excute 命令解析层 -> 调用 api

# 4.4.4 总结

  • 适配器模式

当面临两个新老模块间接口 api 不匹配,可以用适配来转化 api

  • 装饰者模式

当老的方法,不方便去直接修改,可以通装饰者来增加功能

  • 命令模式

解耦实现与具体命令,让实现端和命令端扩展的都更轻松

# 4.5 提高可扩展性(2)

提高整体项目可扩展性的核心

  • 低耦合

  • 良好的组织沟通方式

# 4.5.1 提高可扩展性的设计模式

# 🍅 应对需求上的变更

  • 观察者模式(事件绑定是典型的观察者模式,比如 dom 上监视点击了事件,点击事件触发以后就去做这个点击事件)

观察者模式的目的:减少对象间的耦合,来提高可扩展性

观察者模式的应用场景:当两个模块直接沟通会增加它们的耦合性时

  • 职责链模式

职责链模式的目的:为了避免请求发送者与多个请求处理者耦合在一起,形成一个链条

组合模式的应用场景:把操作分隔成一系列模块,每个模块只处理自己的事情

# 🍅 应对需求上的变更

访问者模式的目的:解耦数据结构与数据操作

访问者模式的应用场景:数据结构不希望与操作有关联

# 4.5.2 基本结构

# 🍅 观察者的基本结构

function observe {
  this.message={}
}

observe.prototype.regist=function(type,fn) {
  this.message[type]=fn
}

observe.prototype.fire=function(type){
  this.message[type]()
}

observe.prototype.remove=function(type){
  this.message[type]=null
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  • 定义一个中转观察者,两个模块之间不直接沟通,而是通过观察者,一般使用与不方便直接沟通,或者异步操作

# 🍅 职责链模式的基本结构

function mode1() {}

function mode2() {}

function mode3() {}

_result = mode1(_result)
_result = mode2(_result)
_result = mode3(_result)
1
2
3
4
5
6
7
8
9
  • 把要做的事情组织为一条有序的链条,通过再这条链条传递消息来完成功能,适用于不设计到赋值异步操作

# 🍅 访问者模式的基本结构

var data = []

var handler = function() {}

handler.prototype.get = function() {}

var vistor = function(handler, data) {
  handler.get(data)
}
1
2
3
4
5
6
7
8
9
  • 通过定义一个访问者,代替直接访问对象,来减少两个对象之间的耦合

# 4.5.3 应用示例

# 🍅 观察者模式示例

多人合作的问题

需求:现在假设 A 工程师写了首页模块,然后 B 工程师写了评论模块。现在要把评论展示在首页

function observe {
  this.message={}
}

observe.prototype.regist=function(type,fn) {
  this.message[type]=fn
}

observe.prototype.fire=function(type){
  this.message[type]()
}

function comment () {
  var self=this;
  this.commentList=[
    {
      type:'hot',
      content:'xxxx'
    }
  ];
// 注册事件
observeOb.regist('gotHot',function(){
  var _arr=[];
  self.commentList.forEach((item)=>{
    if(item.type==='hot'){
      _arr.push(item)
    }
  })
  return _arr
})

}
// 调用事件
var _arr=observeOb.fire('gotHot')

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

一个转盘

需求:有一个转盘应用,每转一圈,速度加快

function observe {
  this.message={}
}

observe.prototype.regist=function(type,fn) {
  this.message[type]=fn
}

observe.prototype.fire=function(type){
  this.message[type]()
}

var observeOb=new observe()

// 初始化html=> 最终结果选定 => 运动结果 => 运动控制

var _domArr=[]

function htmlInit (target) {
  for(let i =0;i<9;i++){
    var _div=document.createElement('div')
    _div.innerHTML=i
    _div.setAttribute('class','item')
    target.appendChild(_div)
    _domArr.push(_div)
  }
}

function getFinal(){
  var _num=Math.random()*10+40
  return Math.floor(_num,0)
}
// 运动模块
function mover (moveConfig){
  var nowIn=0;
  var removeNum=9;
  var timer=setInterval(()=>{
    if(nowIn!=0){
      removeNum=nowIn-1
    }
    _domArr[removeNum].setAttribute('class','item')
    _domArr[nowIn].setAttribute('class','item item-on')
    nowIn++
    if(nowIn==moveConfig.moveTime){
      clearInterval(timer)
      if(moveConfig.moveTime==10){
        observeOb.fire('finish')
      }
    }
  },moveConfig.speed)
}

function moveControll () {
  var final=getFinal()
  var _circle=Math.floor(final/10,0)
  var stopNum=final%10
  var _speed=2000
  var _runCircle=0
  mover({
    moveTime:10,
    speed:_speed
  })
  observeOb.regist('finish',fucntion(){
    var _time=0;
    _speed-=50;
    _runCircle++;
    if(_runCircle<=_circle){
      _time=0
    }else {
      _time=stopNum
    }
    mover({
      moveTime:_time,
      speed:_speed
    })
  })
}

htmlInit(document.getElementById('app'))
moveControll()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80

# 🍅 职责链模式示例

Axios 的拦截器

需求:axios 拦截器的设计,大家可以看成一个用给职责链的思想去处理请求

function axios() {
  this.interceptors = {
    request: new interceptorsManner(),
    response: new interceptorsManner()
  }
  axios.prototype.request = function() {
    var chain = [dispathReuest, undefined]
    var promise = Promise.resolve(config)
    this.interceptors.request.handlers.forEach(interceptor => {
      chain.unshift(interceptor.fulfilled, interceptor.injected)
    })
    this.interceptors.response.handlers.forEach(interceptor => {
      chain.shift(interceptor.fulfilled, interceptor.injected)
    })
    while (chain.length) {
      promise = promise.then(chain.shift(), chain.shift())
    }
  }
}

function interceptorsManner() {
  this.handlers = []
}

interceptorsManner.prototype.use = function(fulfilled, rejected) {
  this.handlers.push({
    fulfilled: fulfilled,
    rejected: rejected
  })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

利用职责链组织一个表单验证

需求:有一个表单,需要前后台校验,后台校验

// 表单事件绑定->表单前端验证->表单后端验证
// 思想:把你要做的事情拆分为模块,模块之间只做自己模块的事情

input.onblur = function() {
  var _value = input.value
  var _arr = [font, middle, back, fontAgain]
  async function test() {
    var _result = _value
    while (_arr.length > 0) {
      _result = await _arr.shift()(_result)
    }
    return _result
  }
  test().then(res => {
    console.log(res)
  })
}

function font(result) {}

function middle(result) {}

function back(result) {}

function fontAgain(result) {}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

# 🍅 访问者模式示例

不同角色访问数据

需求:假设有一个公司的财务报表,财务关心支出和收入,老板关心盈利

function report() {
  this.income = ''
  this.cost = ''
  this.profit = ''
}

function boss() {}

boss.prototype.get = function(data) {}

function account() {}

account.prototype.get = function(num1, num2) {}

function vistor(data, man) {
  var handle = {
    boss: function(data) {
      man.get(data.profit)
    },
    account: function(data) {
      man.get(data.income, data.cost)
    }
  }
  handle[man.constructor.name](data)
}

vistor(new report(), new account())
vistor(new report(), new boss())

// 设计的数据结构操作难以去访问具体的数据结构的时候
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

表格操作

需求:一个可以新增,删除的表格

  function table () { }

  table.prototype.show=function () {

  }

  table.prototype.delete=function () {
    vistor(this.tableData,'delete',id)
  }

  table.prototype.add=function () {

  }

  var tableData=[
    {
      id:1,
      name:'xxx',
      prize:'xxx'
    }
  ]

  function vistor (table,data,handle) {
    var handleOb={
      delete:function(id){

      },
      add:funtion(id,name,price){

      }
    }
    var arg=Array.prototype.splice(arguments);
    arg.splice(0,3);
    handleOb[handle].apple(this,arg)
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

# 4.5.4 总结

  • 观察者模式

适用于不适合直接沟通的模块之间的组织

  • 职责链模式

组织同步模块,把要做的事情划分为模块,要做的事情一次传递

  • 访问者模式

解耦数据操作与数据结构

# 4.6 提高代码质量

提高代码质量

  • 高质量的代码,方便后续的一切操作
  • 方便他人阅读

什么是代码质量

  1. 代码整洁
  2. 结构规整,没有漫长的结构
  3. 阅读好理解

# 4.6.1 优化代码结构

  • 策略模式/状态模式

策略/状态模式的目的:优化 if-else 分支

策略/状态模式的应用场景:当代码 if-else 分支过多时

  • 外观模式

外观模式的目的:通过为多个复杂的子系统提供一个一致的接口

外观模式的应用场景:当完成一个操作时,需要操作多个子系统,不如提供一个更高级的

# 4.6.2 优化你的代码操作

  • 迭代器模式

迭代器者模式的目的:不访问内部的情况下,方便的遍历数据

迭代器模式的应用场景:当我们需要对某个对象进行操作,但是又不能暴露内部

  • 备忘录模式

备忘录模式的目的:记录状态,方便回滚

备忘录模式的应用场景:系统状态多样,为了保证状态的回滚方便,记录状态

# 4.6.3 基本结构

# 🍅 策略模式的基本结构

假设要编写一个计算器,有加减乘除,我们把一层一层的 if 判断,变成下面的形式

  function Strategy (type,a,b) {
    var Strategyer={
      add:function (a,b){
        return a+b
      }
      minus:function(a,b){
        return a-b
      }
      division:function (a,b){
        return a/b
      }
    }
    return Strategyer[type](a,b)
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 🍅 状态模式的基本结构(加了状态的策略模式)

为了减少 if-else 结构,将判断变成对象内部的一个状态,通过对象内部的状态改变,让其拥有不同的行为

function stateFactor(state) {
  var stateObject = {
    _status: '',
    state: {
      state1: function() {},
      state2: function() {}
    },
    run: function() {
      return this.state[this._status]
    }
  }
  stateObject._status = state
  return stateObject
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 🍅 外观模式的基本结构

我们组织方法模块时可以细化多个接口,但是我们给别人使用时,要合为一个接口就像你可以直接去餐厅去点套餐

// 模块1
function Model1() {}
// 模块2
function Model2() {}
// 功能由Model1获取Model2得结果来完成

function use() {
  Model2(Model1())
}
1
2
3
4
5
6
7
8
9

# 🍅 迭代器模式的基本结构

在不暴露对象内部结构的同时,可以顺序的访问对象内部,可以帮助我们简化循环,简化数据操作

function Iterator(item) {
  this.item = item
}
Iterator.prototype.dealEach = function(fn) {
  for (var i = 0; i < this.item.length; i++) {
    fn(this.item[i], i)
  }
}
1
2
3
4
5
6
7
8

# 🍅 备忘录模式的基本结构

记录对象内部的状态,当有需要时回滚到之前的状态或者方便对象使用

function Memento() {
  var cache = {}
  return function(cacheName) {
    if (cache[cacheName]) {
      // 有缓存的操作
    } else {
      // 没有缓存的操作
    }
  }
}
var MementtoFn = Memento()
MementtoFn('xxxcx')
1
2
3
4
5
6
7
8
9
10
11
12

# 4.6.4 应用示例

# 🍅 策略/状态模式的示例

动态的内容

需求:项目有一个动态的内容,根据用户权限的不同显示不同的内容

// 没有用策略的模式的情况
function showPart1() {
  console.log(1)
}
function showPart2() {
  console.log(2)
}
function showPart3() {
  console.log(3)
}
axios.get('xxx').then(res => {
  if (res == 'boss') {
    showPart1()
    showPart2()
    showPart3()
  } else if (res == 'manner') {
    showPart1()
    showPart2()
  } else if (res == 'staff') {
    showPart3()
  }
})

// 用策略模式的情况
function showControl() {
  this.status = ''
  this.power = {
    boss: function() {
      showPart1()
      showPart2()
      showPart3()
    },
    manner: function() {
      showPart1()
      showPart2()
    },
    staff: function() {
      showPart3()
    }
  }
}
showControl.prototype.show = function() {
  var self = this
  axios.get('xxx').then(res => {
    self.status = res
    self.power[self.status]()
  })
}
new showControl().show()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

复合运动

需求:有一个小球,可以控制它左右移动,或则左前,右前等方式移动

function moveLeft() {
  console.log('left')
}
function moveRight() {
  console.log('RigmoveRight')
}
function moveTop() {
  console.log('Top')
}
function moveBottom() {
  console.log('bomoveBottom')
}

// 没有用状态模式的情况
function mover() {
  if (arguments.length == 1) {
    if (arguments[0] == 'left') {
      moveLeft()
    } else if (arguments[0] == 'right') {
      moveRight()
    } else if (arguments[0] == 'top') {
      moveTop()
    } else if (arguments[0] == 'bottom') {
      moveBottom()
    }
  } else {
    if (arguments[0] == 'left' && arguments[1] == 'top') {
      moveLeft()
      moveTop()
    } else if (arguments[0] == 'right' && arguments[1] == 'bottom') {
      moveRight()
      moveBottom()
    }
  }
}

// 用状态模式的情况
function mover() {
  this.status = []
  this.actionHandle = {
    left: moveLeft,
    right: moveRight,
    top: moveTop,
    bottom: moveBottom
  }
}
mover.prototype.run = function() {
  this.status = Array.prototype.slice.call(arguments)
  this.status.forEach(action => {
    this.actionHandle[action]()
  })
}
new mover().run('left', 'right')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

# 🍅 外观模式的示例

插件封装的规律

需求:插件基本都会给最终使用提供一个高级接口

// 划分功能,给使用者一个统一的接口
function tab() {
  this.dom = null
}
tab.prototype.initHTML = function() {}
tab.prototype.changeTab = function() {}
tab.prototype.eventBind = function() {
  var self = this
  this.dom.onclick = function() {
    self.changeTab()
  }
}
tab.prototype.init = function(config) {
  this.initHTML(config)
  this.eventBind()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

封装成方法的思想

需求:在兼容时代,我们会常常需要检测能力,不妨作为一个统一接口

//dom支持检测
function addEvent(dom, type, fn) {
  if (dom.addEventListener) {
    dom.addEventListener(type, fn, false)
  } else if (dom.attachEvent) {
    dom.attachEvent('on' + type, fn)
  } else {
    dom['on' + type] = fn
  }
}
1
2
3
4
5
6
7
8
9
10

# 🍅 迭代器模式的示例

构建一个自己的 forEach

需求:forEach 方法其实是一个典型的迭代器方法

// 对数组和对象进行迭代
//forEach
function Iterator(data) {
  this.data = data
}
Iterator.prototype.dealEach = function(fn) {
  if (this.data instanceof Array) {
    for (var i = 0; i < this.data.length; i++) {
      fn(this.data[i], i)
    }
  } else {
    for (var item in this.data) {
      fn(this.data[item], item)
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

给你的项目数据添加迭代器

需求:项目经常对于后端数据进行遍历操作,不如封装一个迭代器,遍历的更方便

//数据迭代器
var data = [{ num: 1 }, { num: 2 }, { num: 3 }]
function i(data) {
  function Iterator(data) {
    this.data = data
  }
  Iterator.prototype.getHasSomenum = function(handler, num) {
    var _arr = []
    var handleFn
    if (typeof handler == 'function') {
      handleFn = handler
    } else {
      handleFn = function(item) {
        if (item[handler] == num) {
          return item
        }
      }
    }
    for (var i = 0; i < this.data.length; i++) {
      var _result = handleFn.call(this, this.data[i])
      if (_result) {
        _arr.push(_result)
      }
    }
    return _arr
  }
  return new Iterator(data)
}
i(data).getHasSomenum('num', 1)
// 自定义的筛选方法
i(data).getHasSomenum(function(item) {
  if (item.num - 1 == 2) {
    return item
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

# 🍅 备忘录模式的示例

文章页的缓存

需求:项目有一个文章页需求,现在进行优化,如果上一篇已经读取过了,则不进行请求,否则请求文章数据

//缓存
function pager() {
  var cache = {}
  return function(pageName) {
    if (cache[pageName]) {
      return cache[pageName]
    } else {
      axios.get(pageName).then(res => {
        cache[pageName] = res
      })
    }
  }
}
var getpage = pager()
getpage('pageone')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

前进后退功能

需求:开发一个可移动的 div,拥有前进后退功能能回滚到之前的位置

//前进后退
function moveDiv() {
  this.stateList = [] // 缓存之前的状态
  this.nowState = 0 // 状态指针,指向当前状态在哪个
}
/**
 * @param {string} type 往哪个方向移动
 * @param {Number} num  移动多远
 */
moveDiv.prototype.move = function(type, num) {
  changeDiv(type, num) //假设移动位置的函数
  this.stateList.push({
    type: type,
    num: num
  }) // 添加状态
  this.nowState = this.stateList.length - 1 // 设置当前指针
}
moveDiv.prototype.go = function() {
  //前进
  var _state
  if (this.nowState < this.stateList.length - 1) {
    //当前指针小于数组最后一位,说明能前进
    this.nowState++
    _state = this.stateList[this.nowState]
    changeDiv(_state.type, _state.num)
  }
}
moveDiv.prototype.back = function() {
  //后退
  var _state
  if (this.nowState >= 0) {
    this.nowState--
    _state = this.stateList[this.nowState]
    changeDiv(_state.type, _state.num)
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

# 4.6.5 本章总结

  • 策略/状态模式

帮助我们优化 if-else 结构

  • 外观模式

一种套餐化接口的思想,提醒我们封装常用方法

  • 迭代器模式

帮助我们更好的遍历数据

  • 备忘录模式

帮我们缓存以及回到过去的状态

# 4.7 总结

提示

我们的设计模式,要记住其思想,不用记住其结构,结构不是固定;我们通过设计模式主要是提高我们代码的质量

最新更新时间: 2022-2-11 3:16:26 ├F10: PM┤

© 2024 web全栈体系. All Rights Reserved.(所有文章未经授权禁止转载、摘编、复制或建立镜像,如有违反,追究法律责任。)

豫ICP备19041317号-1