XingYun blog
  • JS基础

    • 图解js原型链
    • JS Event Loop
    • 对象的底层数据结构
    • 让你的JavaScript代码简单又高效
    • 函数参数按值传递
    • 判断数据类型
    • 浮点数精度问题和解决办法
    • 常用方法snippet
    • 实现Promise
    • 防抖和节流
    • 巧用sort排序
  • CSS && HTML

    • CSS也需要性能优化
    • class命名规范
    • em、px、rem、vh、vw 区别
    • CSS揭秘阅读笔记
  • 浏览器

    • 浏览器是如何渲染页面的
    • 重排和重绘
    • BOM浏览器对象模型
    • DOM事件
    • 浏览器存储
  • 数据结构

    • JS实现链表
    • JS实现栈与栈应用
    • JS实现常见排序
    • 哈夫曼编码
    • MD5算法
  • vue原理浅析

    • Vue虚拟dom与Diff算法
    • 前端打包文件的缓存机制
    • vue数组为什么不是响应式
    • v-for为什么不能用index做key
  • 前端工程化

    • 浏览器是如何渲染页面的
    • 前端打包需要gzip压缩吗
    • 前端打包文件的缓存机制
    • webpack loader和plugin
  • 轮子&&组件库

    • 实现水波浪进度球
  • 文字转语音mp3文件
  • 文件上传前后端实现
  • moment.js给定时间获取自然月、周的时间轴
  • 实现文件上传功能
  • 批量下载照片
  • leaflet改变坐标原点
  • 网络

    • 有了MAC地址 为什么还需要IP地址
    • 为什么IP地址老是变
    • 我们为什么需要IPV6
    • TCP与UDP
  • 计算机组成原理

    • ASCII、Unicode、UTF-8和UTF-16
  • VSCode

    • VSCode图片预览插件 Image preview
    • rsync:linux间的高效传输工具

XingYun

冲!
  • JS基础

    • 图解js原型链
    • JS Event Loop
    • 对象的底层数据结构
    • 让你的JavaScript代码简单又高效
    • 函数参数按值传递
    • 判断数据类型
    • 浮点数精度问题和解决办法
    • 常用方法snippet
    • 实现Promise
    • 防抖和节流
    • 巧用sort排序
  • CSS && HTML

    • CSS也需要性能优化
    • class命名规范
    • em、px、rem、vh、vw 区别
    • CSS揭秘阅读笔记
  • 浏览器

    • 浏览器是如何渲染页面的
    • 重排和重绘
    • BOM浏览器对象模型
    • DOM事件
    • 浏览器存储
  • 数据结构

    • JS实现链表
    • JS实现栈与栈应用
    • JS实现常见排序
    • 哈夫曼编码
    • MD5算法
  • vue原理浅析

    • Vue虚拟dom与Diff算法
    • 前端打包文件的缓存机制
    • vue数组为什么不是响应式
    • v-for为什么不能用index做key
  • 前端工程化

    • 浏览器是如何渲染页面的
    • 前端打包需要gzip压缩吗
    • 前端打包文件的缓存机制
    • webpack loader和plugin
  • 轮子&&组件库

    • 实现水波浪进度球
  • 文字转语音mp3文件
  • 文件上传前后端实现
  • moment.js给定时间获取自然月、周的时间轴
  • 实现文件上传功能
  • 批量下载照片
  • leaflet改变坐标原点
  • 网络

    • 有了MAC地址 为什么还需要IP地址
    • 为什么IP地址老是变
    • 我们为什么需要IPV6
    • TCP与UDP
  • 计算机组成原理

    • ASCII、Unicode、UTF-8和UTF-16
  • VSCode

    • VSCode图片预览插件 Image preview
    • rsync:linux间的高效传输工具
  • leaflet改变坐标原点
  • 补间动画gsap与tween
  • 文字转语音mp3文件
  • JavaScript引入
  • JavaScript高级程序设计阅读笔记
  • Javascript函数参数按值传递传递
  • JS防抖和节流
  • 手写JS常用方法
  • 手写Promise
  • JS Event Loop
  • 重排和重绘
  • em、px、rem、vh、vw 区别
  • css也需要性能优化
  • 图解 js 原型链
  • js函数参数按值传递
  • BOM浏览器对象模型
  • DOM
  • 事件
  • js对象数组sort按需排序
  • 文件上传功能技术选型和前后端实现
  • 前端图片处理
  • 让你的JavaScript代码简单又高效
  • BEM:class命名规范
  • 前端规范-CSS属性那么多(杂),怎么排序
  • TypeScript Tips
  • jsx
  • canvas基础
  • 前端日志
  • 浏览器存储
  • CSS世界阅读笔记
  • CSS揭秘阅读笔记
  • js变量命名常用规范词
  • 你不知道的JavaScript阅读笔记
  • js对象的底层数据结构
  • js判断数据类型
  • JS浮点数精度问题和解决办法
  • js作用域
  • js堆栈溢出和内存泄漏
  • 浏览器是如何渲染页面的
  • 疑难杂症和踩坑问题合集
  • 免费在线API收集
  • 原生JS实现Ajax请求
  • cookie、session、localStorage、sessionStorage的区别
  • Sass与Less
  • arrayBuffer、blob、file对象
  • TypeScript基础
  • 前端
XingYun
2022-06-29
目录

Javascript设计模式

参考 [[ JavaScript 设计模式与开发实践 ]]

# 1.单例模式

单例模式的定义是: 保证一个类仅有一个实例,并提供一个访问它的全局访问点。

单例模式是一种常用的模式,有一些对象我们往往只需要一个,比如线程池、全局缓存、浏 览器中的 window 对象等。在 JavaScript 开发中,单例模式的用途同样非常广泛。试想一下,当我 们单击登录按钮的时候,页面中会出现一个登录浮窗,而这个登录浮窗是唯一的,无论单击多少 次登录按钮,这个浮窗都只会被创建一次,那么这个登录浮窗就适合用单例模式来创建。

核心点 : 确保只有一个实例,并提供全局访问。

# 1.1 场景

使用 CreateDiv 单例类,它的作用是负责在页 面中创建唯一的 div 节点

var CreateDiv = function(html) {
  if (instance) {
    return instance
  }
  this.html = html
  this.init()
  return (instance = this)
}

var CreateDiv = function(html) {
  this.html = html
  this.init()
}
CreateDiv.prototype.init = function() {
  var div = document.createElement('div')
  div.innerHTML = this.html
  document.body.appendChild(div)
}

var ProxySingletonCreateDiv = (function() {
  var instance
  return function(html) {
    if (!instance) {
      instance = new CreateDiv(html)
    }
    return instance
  }
})()
var a = new ProxySingletonCreateDiv('sven1')
var b = new ProxySingletonCreateDiv('sven2')

alert(a === b) //true
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

通过引入代理类的方式,我们同样完成了一个单例模式的编写, 我们 把负责管理单例的逻辑移到了代理类 proxySingletonCreateDiv 中。这样一来,CreateDiv 就变成了 一个普通的类,它跟 proxySingletonCreateDiv 组合起来可以达到单例模式的效果。

# 1.2 通用的惰性单例

惰性单例指的是在需要的时候才创建对象实例。惰性单例是单例模式的重点,这种技术在实 际开发中非常有用,有用的程度可能超出了我们的想象,实际上在本章开头就使用过这种技术, 11 instance 实例对象总是在我们调用 Singleton.getInstance 的时候才被创建,而不是在页面加载好 的时候就创建

Singleton.getInstance = (function() {
  var instance = null
  return function(name) {
    if (!instance) {
      instance = new Singleton(name)
    }
    return instance
  }
})()
1
2
3
4
5
6
7
8
9

在 getSinge 函数中,实际上也提到了闭包和高阶函数的概念。单例模式是一种简单但非常实 用的模式,特别是惰性单例技术,在合适的时候才创建对象,并且只创建唯一的一个。更奇妙的 是,创建对象和管理单例的职责被分布在两个不同的方法中,这两个方法组合起来才具有单例模 式的威力。

# 1.3 策略模式的优缺点

优点

  • 策略模式利用组合、委托和多态等技术和思想,可以有效地避免多重条件选择语句。
  • 策略模式提供了对开放—封闭原则的完美支持,将算法封装在独立的 strategy 中,使得它们易于切换,易于理解,易于扩展。
  • 策略模式中的算法也可以复用在系统的其他地方,从而避免许多重复的复制粘贴工作。
  • 在策略模式中利用组合和委托来让 Context 拥有执行算法的能力, 这也是继承的一种更轻便的替代方案

缺点

使用策略模式,必须了解所有的 strategy,必须了解各个 strategy 之间的不同点, 这样才能选择一个合适的 strategy。比如,我们要选择一种合适的旅游出行路线,必须先了解选择飞机、火车、自行车等方案的细节。此时 strategy 要向客户暴露它的所有实现,这是违反最少知识原则的

# 2. 策略模式

策略模式:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换

# 2.1 场景

表单校验

# 2.2 普通实现

var registerForm = document.getElementById('registerForm')
registerForm.onsubmit = function() {
  if (registerForm.userName.value === '') {
    alert('用户名不能为空')
    return false
  }
  if (registerForm.password.value.length < 6) {
    alert('密码长度不能少于 6 位')
    return false
  }
  if (!/(^1[3|5|8][0-9]{9}$)/.test(registerForm.phoneNumber.value)) {
    alert('手机号码格式不正确')
    return false
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 2.3 策略模式实现

var strategies = {
    isNonEmpty: function( value, errorMsg ){
        if ( value === '' ){
            return errorMsg ;
        }
    },
    minLength: function( value, length, errorMsg ){
        if ( value.length < length ){
            return errorMsg;
        }
    },
    isMobile: function( value, errorMsg ){ // 手机号码格式
        if ( !/(^1[3|5|8][0-9]{9}$)/.test( value ) ){
            return errorMsg;
        }
    }
};
var Validator = function(){
    this.cache = []; // 保存校验规则
};
Validator.prototype.add = function(
    var ary = rule.split( ':' );
    this.cache.push(function(){ //
        var strategy = ary.shift();
        ary.unshift( dom.value );
        ary.push( errorMsg ); //
        return strategies[strategy].apply(dom, ary);
    });
};
Validator.prototype.start = function(){
    for ( var i = 0, validatorFunc; validatorFunc = this.cache[ i++ ]; ){
        var msg = validatorFunc(); // 开始校验,并取得校验后的返回信息
        if ( msg ){ // 如果有确切的返回值,说明校验没有通过
              return msg;
        }
    }
};
var validataFunc = function(){
    var validator = new Validator(); // 创建一个 validator 对象
    /***************添加一些校验规则****************/
    validator.add( registerForm.userName, 'isNonEmpty', '用户名不能为空' );
    validator.add( registerForm.password, 'minLength:6', '密码长度不能少于 6位');
    validator.add( registerForm.phoneNumber, 'isMobile', '手机号码格式不正确' );
    var errorMsg = validator.start(); // 获得校验结果
    return errorMsg; // 返回校验结果
}
var registerForm = document.getElementById( 'registerForm' ); registerForm.onsubmit = function(){
    var errorMsg = validataFunc(); // 如果 errorMsg 有确切的返回值,说明未通过校验
    if ( errorMsg ){
        alert ( errorMsg );
        return false; // 阻止表单提交
    }
};
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

# 2.4 场景 2

奖金计算,绩效为 S 的人年 终奖有 4 倍工资,绩效为 A 的人年终奖有 3 倍工资,而绩效为 B 的人年终奖是 2 倍工资

# 2.5 普通实现

var calculateBonus = function(performanceLevel, salary) {
  if (performanceLevel === 'S') {
    return salary * 4
  }
  if (performanceLevel === 'A') {
    return salary * 3
  }
  if (performanceLevel === 'B') {
    return salary * 2
  }
}
calculateBonus('B', 20000) // 输出:40000
calculateBonus('S', 6000) // 输出:24000
1
2
3
4
5
6
7
8
9
10
11
12
13

# 2.6 使用策略模式

var strategies = {
  S: function(salary) {
    return salary * 4
  },
  A: function(salary) {
    return salary * 3
  },
  B: function(salary) {
    return salary * 2
  }
}
var calculateBonus = function(level, salary) {
  return strategies[level](salary)
}
console.log(calculateBonus('S', 20000)) // 输出:80000
console.log(calculateBonus('A', 10000)) // 输出:30000
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 3. 职责链模式

职责链模式的定义是: 使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系,将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。
职责链模式的名字非常形象,一系列可能会处理请求的对象被连接成一条链,请求在这些对 象之间依次传递,直到遇到一个可以处理它的对象,我们把这些对象称为链中的节点

# 3.1 场景

假设我们负责一个售卖手机的电商网站,经过分别交纳 500 元定金和 200 元定金的两轮预定后(订单已在此时生成),现在已经到了正式购买的阶段。 公司针对支付过定金的用户有一定的优惠政策。在正式购买后,已经支付过 500 元定金的用 户会收到 100 元的商城优惠券,200 元定金的用户可以收到 50 元的优惠券,而之前没有支付定金的用户只能进入普通购买模式,也就是没有优惠券,且在库存有限的情况下不一定保证能买到。

# 3.2 普通实现

if else 嵌套

var order = function(orderType, pay, stock) {
  if (orderType === 1) {
    // 500 元定金购买模式
    if (pay === true) {
      // 已支付定金
      console.log('500 元定金预购, 得到 100 优惠券')
    } else {
      // 未支付定金,降级到普通购买模式
      if (stock > 0) {
        // 用于普通购买的手机还有库存
        console.log('普通购买, 无优惠券')
      } else {
        console.log('手机库存不足')
      }
    }
  } else if (orderType === 2) {
    if (pay === true) {
      // 200 元定金购买模式
      console.log('200 元定金预购, 得到 50 优惠券')
    } else {
      if (stock > 0) {
        console.log('普通购买, 无优惠券')
      } else {
        console.log('手机库存不足')
      }
    }
  } else if (orderType === 3) {
    if (stock > 0) {
      console.log('普通购买, 无优惠券')
    } else {
      console.log('手机库存不足')
    }
  }
}
order(1, true, 500) // 输出: 500 元定金预购, 得到 100 优惠券
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

可以看到虽然功能实现了,但代码很乱, 很难读懂

# 3.3 使用职责链模式实现

现在我们采用职责链模式重构这段代码,先把 500 元订单、200 元订单以及普通购买分成 3 个函数。

接下来把 orderType、pay、stock 这 3 个字段当作参数传递给 500 元订单函数,如果该函数不符合处理条件,则把这个请求传递给后面的 200 元订单函数,如果 200 元订单函数依然不能处理该请求,则继续传递请求给普通购买函数。

var order500 = function(orderType, pay, stock) {
  if (orderType === 1 && pay === true) {
    console.log('500 元定金预购,得到 100 优惠券')
  } else {
    return 'nextSuccessor' // 我不知道下一个节点是谁,反正把请求往后面传递
  }
}
var order200 = function(orderType, pay, stock) {
  if (orderType === 2 && pay === true) {
    console.log('200 元定金预购,得到 50 优惠券')
  } else {
    return 'nextSuccessor' // 我不知道下一个节点是谁,反正把请求往后面传递
  }
}
var orderNormal = function(orderType, pay, stock) {
  if (stock > 0) {
    console.log('普通购买,无优惠券')
  } else {
    console.log('手机库存不足')
  }
}

// Chain.prototype.setNextSuccessor 指定在链中的下一个节点
// Chain.prototype.passRequest 传递请求给某个节点
var Chain = function(fn) {
  this.fn = fn
  this.successor = null
}
Chain.prototype.setNextSuccessor = function(successor) {
  return (this.successor = successor)
}
Chain.prototype.passRequest = function() {
  var ret = this.fn.apply(this, arguments)
  if (ret === 'nextSuccessor') {
    return (
      this.successor &&
      this.successor.passRequest.apply(this.successor, arguments)
    )
  }
  return ret
}
var chainOrder500 = new Chain(order500)
var chainOrder200 = new Chain(order200)
var chainOrderNormal = new Chain(orderNormal)

chainOrder500.setNextSuccessor(chainOrder200)
chainOrder200.setNextSuccessor(chainOrderNormal)

chainOrder500.passRequest(1, true, 500) // 输出:500 元定金预购,得到 100 优惠券
chainOrder500.passRequest(2, true, 500) // 输出:200 元定金预购,得到 50 优惠券
chainOrder500.passRequest(3, true, 500) // 输出:普通购买,无优惠券
chainOrder500.passRequest(1, false, 0) // 输出:手机库存不足
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

通过改进,我们可以自由灵活地增加、移除和修改链中的节点顺序,假如某天网站运营人员 又想出了支持 300 元定金购买,那我们就在该链中增加一个节点即可

var order300 = function() {
  // 具体实现略
}
chainOrder300 = new Chain(order300)
chainOrder500.setNextSuccessor(chainOrder300)
chainOrder300.setNextSuccessor(chainOrder200)
1
2
3
4
5
6

# 3.4 异步的职责链

在现实开发中,我们经常会遇到一些异步的问题,比如我们要在 节点函数中发起一个 ajax 异步请求,异步请求返回的结果才能决定是否继续在职责链中 passRequest。 这时候让节点函数同步返回"nextSuccessor"已经没有意义了,所以要给 Chain 类再增加一个原型方法 Chain.prototype.next,表示手动传递请求给职责链中的下一个节点

Chain.prototype.next = function() {
  return (
    this.successor &&
    this.successor.passRequest.apply(this.successor, arguments)
  )
}
/* 异步职责链 */
var fn1 = new Chain(function() {
  console.log(1)
  return 'nextSuccessor'
})
var fn2 = new Chain(function() {
  console.log(2)
  var self = this
  setTimeout(function() {
    self.next()
  }, 1000)
})
var fn3 = new Chain(function() {
  console.log(3)
})
fn1.setNextSuccessor(fn2).setNextSuccessor(fn3)
fn1.passRequest()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

现在我们得到了一个特殊的链条,请求在链中的节点里传递,但节点有权利决定什么时候把 请求交给下一个节点。可以想象,异步的职责链加上命令模式(把 ajax 请求封装成命令对象),我们可以很方便地创建一个异步 ajax 队列库。

# 3.5 用 AOP 实现职责链

Function.prototype.after = function(fn) {
  var self = this
  return function() {
    var ret = self.apply(this, arguments)
    if (ret === 'nextSuccessor') {
      return fn.apply(this, arguments)
    }
    return ret
  }
}
var order = order500yuan.after(order200yuan).after(orderNormal)
order(1, true, 500) // 输出:500 元定金预购,得到 100 优惠券
order(2, true, 500) // 输出:200 元定金预购,得到 50 优惠券
order(1, false, 500) // 输出:普通购买,无优惠券
order(2, false, 0) // 手机库存不足
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

用 AOP 来实现职责链既简单又巧妙,但这种把函数叠在一起的方式,同时也叠加了函数的 作用域,如果链条太长的话,也会对性能有较大的影响

# 4. 代理模式

# 4.1 场景 1

假设当 A 在心情好的时候收到花,小明表白成功的几率有 60%,而当 A 在心情差的时候收到花,小明表白的成功率无限趋近于 0。

小明跟 A 刚刚认识两天,还无法辨别 A 什么时候心情好。如果不合时宜地把花送给 A,花 被直接扔掉的可能性很大,这束花可是小明吃了 7 天泡面换来的。

但是 A 的朋友 B 却很了解 A,所以小明只管把花交给 B,B 会监听 A 的心情变化,然后选 择 A 心情好的时候把花转交给 A

# 4.2 代码实现:

var Flower = function() {}
var xiaoming = {
  sendFlower: function(target) {
    var flower = new Flower()
    target.receiveFlower(flower)
  }
}
var B = {
  receiveFlower: function(flower) {
    A.listenGoodMood(function() {
      var flower = new Flower()
      A.receiveFlower(flower)
    })
  }
}
var A = {
  receiveFlower: function(flower) {
    // 监听 A 的好心情
    console.log('收到花 ' + flower)
  },
  listenGoodMood: function(fn) {
    setTimeout(function() {
      // 假设 10 秒之后 A 的心情变好
      fn()
    }, 10000)
  }
}
xiaoming.sendFlower(B)
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

由上面的例子可以引出两种代理模式的用处

  • 保护代理: 代理 B 可以帮助 A 过滤掉一些请求,比如送花的人中年龄太大的或者没有宝马的,这种请求就可以直接在代理 B 处被拒绝掉

  • 虚拟代理: 假设现实中的花价格不菲,导致在程序世界里,new Flower 也是一个代价昂贵的操作, 那么我们可以把 new Flower 的操作交给代理 B 去执行,代理 B 会选择在 A 心情好时再执行 new Flower

# 4.3 虚拟代理实现图片预加载

var myImage = (function() {
  var imgNode = document.createElement('img')
  document.body.appendChild(imgNode)
  return {
    setSrc: function(src) {
      imgNode.src = src
    }
  }
})()
var proxyImage = (function() {
  var img = new Image()
  img.onload = function() {
    myImage.setSrc(this.src)
  }
  return {
    setSrc: function(src) {
      myImage.setSrc('file:// /C:/Users/svenzeng/Desktop/loading.gif')
      img.src = src
    }
  }
})()
proxyImage.setSrc('http://imgcache.qq.com/music/photo/k/000GGDys0yA0Nk.jpg')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 4.4 合并 HTTP 请求

虚拟代理合并 HTTP 请求, 假设我们在做一个文件同步的功能,当我们选中一个 checkbox 的时候,它对应的文件就会被同 步到另外一台备用服务器上面。当一次选中过多时,会产生频繁的网络请求。

将带来很大的开销。可以通过一个代理函数 proxySynchronousFile 来收集一段时间之内的请求, 最后一次性发送给服务器

var synchronousFile = function(id) {
  console.log('开始同步文件,id 为: ' + id)
}
var proxySynchronousFile = (function() {
  var cache = [], // 保存一段时间内需要同步的 ID
    timer // 定时器
  return function(id) {
    cache.push(id)
    if (timer) {
      // 保证不会覆盖已经启动的定时器
      return
    }
    timer = setTimeout(function() {
      synchronousFile(cache.join(','))
      clearTimeout(timer) // 清空定时器
      timer = null
      cache.length = 0 // 清空 ID 集合
    }, 2000)
  } // 2 秒后向本体发送需要同步的 ID 集合
})()

var checkbox = document.getElementsByTagName('input')
for (var i = 0, c; (c = checkbox[i++]); ) {
  c.onclick = function() {
    if (this.checked === true) {
      proxySynchronousFile(this.id)
    }
  }
}
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

# 5. 组合模式

组合模式将对象组合成树形结构,以表示“部分-整体”的层次结构。 除了用来表示树形结构之外,组合模式的另一个好处是通过对象的多态性表现,使得用户对单个对象和组合对象的使用具有一致性

以命令模式中的宏命令代码为例,宏命令对象包含了一组具体的子命令对象,不管是宏命令对象,还是子命令对象,都有一个 execute 方法负责执行命令。宏命令中包含了一组子命令,它们组成了一个树形结构,这里是一棵结构非常简单的树

在组合模式中,请求在树中传递的过程总是遵循一种逻辑。请求从树最顶端的对象往下传递,如果当前处理请求的对象是叶对象(普通子命令),叶对象自身会对请求作出相应的处理;如果当前处理请求的对象是组合对象(宏命令), 组合对象则会遍历它属下的子节点,将请求继续传递给这些子节点。

# 5.1 组合模式下的宏命令

目前的万能遥控器,包含了关门、开电脑、登录 QQ 这 3 个命令。现在我们需要一个“超级万能遥控器”,可以控制家里所有的电器,这个遥控器拥有以下功能

  • 打开空调
  • 打开电视和音响
  • 关门、开电脑、登录 QQ
var MacroCommand = function() {
  return {
    commandsList: [],
    add: function(command) {
      this.commandsList.push(command)
    },
    execute: function() {
      for (var i = 0, command; (command = this.commandsList[i++]); ) {
        command.execute()
      }
    }
  }
}
var openAcCommand = {
  execute: function() {
    console.log('打开空调')
  }
}
/*家里的电视和音响是连接在一起的,所以可以用一个宏命令来组合打开电视和打开音响的命令*/
var openTvCommand = {
  execute: function() {
    console.log('打开电视')
  }
}
var openSoundCommand = {
  execute: function() {
    console.log('打开音响')
  }
}
var macroCommand1 = MacroCommand()
macroCommand1.add(openTvCommand)
macroCommand1.add(openSoundCommand)
/*关门、打开电脑和打登录 QQ 的命令*/
var closeDoorCommand = {
  execute: function() {
    console.log('关门')
  }
}
var openPcCommand = {
  execute: function() {
    console.log('开电脑')
  }
}
var openQQCommand = {
  execute: function() {
    console.log('登录 QQ')
  }
}
var macroCommand2 = MacroCommand()
macroCommand2.add(closeDoorCommand)
macroCommand2.add(openPcCommand)
macroCommand2.add(openQQCommand)

/*现在把所有的命令组合成一个“超级命令”*/
var macroCommand = MacroCommand()
macroCommand.add(openAcCommand)
macroCommand.add(macroCommand1)
macroCommand.add(macroCommand2)

/*最后给遥控器绑定“超级命令”*/
var setCommand = (function(command) {
  document.getElementById('button').onclick = function() {
    command.execute()
  }
})(macroCommand)
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

从这个例子中可以看到,基本对象可以被组合成更复杂的组合对象,组合对象又可以被组合, 这样不断递归下去,这棵树的结构可以支持任意多的复杂度。

在树最终被构造完成之后,让整颗 树最终运转起来的步骤非常简单,只需要调用最上层对象的 execute 方法。每当对最上层的对象 进行一次请求时,实际上是在对整个树进行深度优先的搜索

而创建组合对象的程序员并不关心这些内在的细节,往这棵树里面添加一些新的节点对象是非常容易的事情。

# 5.2 应用场景 —— 扫描文件夹

文件夹和文件之间的关系,非常适合用组合模式来描述。文件夹里既可以包含文件,又可以 包含其他文件夹,最终可能组合成一棵树

当使用用杀毒软件扫描该文件夹时,往往不会关心里面有多少文件和子文件夹,组合模式使得我们只需要操作最外层的文件夹进行扫描。

/* Folder */
var Folder = function( name ){
    this.name = name;
    this.files = [];
};
Folder.prototype.add= function( file ){
    this.files.push(file );
};
Folder.prototype.scan = function(){
    console.log( '开始扫描文件夹: ' + this.name );
    for ( var i = 0, file, files = this.files; file = files[ i++ ]; ){
        files file.scan();
    }
};
/*File*/
var File = function( name ){
    this.name = name;
};
File.prototype.add = function(){
    throw new Error( '文件下面不能再添加文件' );
};
File.prototype.scan = function(){
    console.log( '开始扫描文件: ' + this.name );
};
/*创建一些文件夹和文件对象, 并且让它们组合成一棵树,这棵树就是我们 F 盘里的 现有文件目录结构*/
var folder = new Folder( '学习资料' );
var folder1 = new Folder( 'JavaScript' );
var folder2 = new Folder ( 'jQuery' );
var file1 = new File( 'JavaScript 设计模式与开发实践' );
var file2 = new File( '精通 jQuery' );
var file3 = new File('重构与模式' );
folder1.add( file1 );
folder2.add( file2 );
folder.add( folder1 );
folder.add( folder2 );
folder.add( file3 );

/*现在的需求是把移动硬盘里的文件和文件夹都复制到这棵树中,假设我们已经得到了这些文件对象*/
var folder3 = new Folder( 'Nodejs' );
var file4 = new File( '深入浅出 Node.js' );
folder3.add( file4 );
var file5 = new File( 'JavaScript 语言精髓与编程实践' );

/*接下来就是把这些文件都添加到原有的树中*/
folder.add( folder3 );
folder.add( file5 );
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

组合模式可以让我们使用树形方式创 建对象的结构。我们可以把相同的操作应用在组合对象和单个对象上。在大多数情况下,我们都 可以忽略掉组合对象和单个对象之间的差别,从而用一致的方式来处理它们。

然而,组合模式并不是完美的,它可能会产生一个这样的系统:系统中的每个对象看起来都 与其他对象差不多。它们的区别只有在运行的时候会才会显现出来,这会使代码难以理解。此外,如果通过组合模式创建了太多的对象,那么这些对象可能会让系统负担不起。

上次更新: 2023/04/05, 09:41:10
最近更新
01
JavaScript-test
07-20
02
二维码的原理
07-20
03
利用ChatGPT优化代码
07-20
更多文章>
Theme by Vdoing | Copyright © 2021-2023 XingYun | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式