JS 高级

coderljw 2024-10-13 JS 高级
  • JS 高级
  • 防抖节流
  • 柯里化
  • 组合函数
  • 函数式编程
  • 设计模式
大约 16 分钟

# 1. 防抖函数

  • 频繁触发事件中,若在 n 秒内事件再次被触发,清除前面的事件,重新计时。

    应用:搜索联想、窗口改变等。

const debounce = (fn, wait = 500) => {
  let timer
  return function(...args) {
    clearTimeout(timer)
    timer = setTimeout(() => fn.apply(this, args), wait)
  }
}
1
2
3
4
5
6
7

# 2. 节流函数

  • 频繁触发事件中,n 秒内只会执行一次。

    应用:鼠标点击、窗口滚动等。

const throttle = (fn, wait = 500) => {
  let inThrottle, timer, lastTime
  return function(...args) {
    if (!inThrottle) {
      fn.apply(this, args)
      lastTime = Date.now()
      inThrottle = true
    } else {
      clearTimeout(timer)
      timer = setTimeout(() => {
        if (Date.now() - lastTime >= wait) {
          fn.apply(this, args)
          lastTime = Date.now()
        }
      }, Math.max(wait - (Date.now() - lastTime), 0))
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 3. 惰性函数

  • 惰性函数表示函数执行的分支只会在函数第一次调用的时候执行。在第一次调用过程中,该函数会被覆盖为另一个按照合适方式执行的函数,这样任何对原函数的调用就不用再经过执行的分支了。

    应用:浏览器监听事件兼容等。

const time = () => {
  const timestamp = Date.now()
  time = _ => timestamp
  return time()
}

time() // => -28800000
time() // => -28800000
time() // => -28800000
1
2
3
4
5
6
7
8
9

# 4. 级联函数(链式函数)

  • 级联函数也叫链式函数,方法链一般适合对一个对象进行连续操作。一定程度上可以减少代码量,缺点是它占用了函数的返回值。

    jQuery 中运用了大量的链式写法。

function XueDao() {}
function setName(name) {
  this.name = name
  return this
}
function setAge(age) {
  this.age = age
  return this
}
function setWife(wife) {
  this.wife = wife
  return this
}

['setName', 'setAge', 'setWife'].map(key => (XueDao.prototype[key] = eval(key)))

const role = new XueDao()
role
  .setName('徐扶墙')
  .setAge(17)
  .setWife('姜姒') // => {name: '徐扶墙', age: 17, wife: '姜姒'}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 5. 偏应用函数

  • 固定一个函数的一些参数,然后产生另一个更小元的函数。偏函数严格来讲是一个减少函数参数个数的过程。

    • 理念:降低通用性、增加适用性。
    • 作用:参数复用、提前返回、延迟计算/运行。
const partial = (fn, ...presetArgs) => (...laterArgs) =>
  fn(...presetArgs, ...laterArgs)

const add = (x, y) => x + y

[770, 1550].map(partial(add, 7)) // => [777, 1557]
1
2
3
4
5
6

# 6. 柯理化函数

  • 柯里化是偏应用的一种特殊形式。把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果为新函数的技术。将一个多参数(higher-arity)函数拆解为一系列的单元链式函数。

    • 理念:降低通用性、增加适用性。
    • 作用:参数复用、提前返回、延迟计算/运行。
  • 松散柯里化(一次处理 {0, } 个参数)。

    const currying =
      (fn, arity = fn.length) =>
      (...args) =>
        args.length >= arity
          ? fn(...args)
          : currying(fn.bind(null, ...args), arity - args.length)
    
    const add = currying((...arg) => eval(arg.join('+')), 3)
    add()(2)(3, 5) // => 10
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
  • 严格柯里化(一次处理 {1, 1} 个参数)。

    const currying =
      (fn, arity = fn.length, prevArgs = []) =>
      nextArg => {
        const args = [...prevArgs, nextArg]
        return args.length >= arity ? fn(...args) : currying(fn, arity, args)
      }
    
    const add = currying((...arg) => eval(arg.join('+')), 3)
    add(2)(3, 4)(5) // => 10
    
    1
    2
    3
    4
    5
    6
    7
    8
    9

# 7. 反柯里化函数

  • 反柯里化,是一个泛型化的过程。它使得被反柯里化的函数,可以接收更多参数。目的是创建一个更普适性的函数,可以被不同的对象使用(有鸠占鹊巢的效果)。

    增加通用性。

// JavaScript 之父 Brendan Eich
Function.prototype.uncurrying = function() {
  var self = this
  return function() {
    var obj = Array.prototype.shift.call(arguments)
    return self.apply(obj, arguments)
  }
}

// 另一种实现方式
Function.prototype.uncurrying = function() {
  var self = this
  return function() {
    return Function.prototype.call.apply(self, arguments)
  }
}

['shift', 'push', 'forEach'].map(
  method => (Array[method] = Array.prototype[method].uncurrying())
)

const foo = {
  2: 2,
  3: 3,
  length: 2,
}

// 方法调用会使用/修改 length 哟!
Array.shift(foo) // foo: {2: 2, 3: 3, length: 1}
Array.push(foo, 5) // foo: {1: 5, 2: 2, 3: 3, length: 2}
// length 为 2,遍历截止到索引为 1
Array.forEach(foo, console.log) // 只打印一次:5 1 {1: 5, 2: 2, 3: 3, length: 2}
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

# 8. 组合函数 (opens new window)

  • 函数组合是指将多个函数按顺序执行,前一个函数的返回值作为下一个函数的参数,最终返回结果。这样做的好处是可以将复杂任务分割成多个子任务,然后通过组合函数再组合成复杂任务。

    应用:中间件等。

  • 同步通用组合函数(从右向左执行)。

    const compose =
      (...fns) =>
      (...args) =>
        fns.reduceRight((res, fn) => fn(res), ...args)
    
    const add = num => num + 7
    compose(add, add)(7) // => 21
    
    1
    2
    3
    4
    5
    6
    7
  • 异步通用组合函数(从右向左执行)。

    const composePromise =
      (...fns) =>
      (...args) =>
        fns.reduceRight(
          (seq, fn) => seq.then(res => fn(res)),
          Promise.resolve(fns.pop()(...args))
        )
    
    const foo = ms =>
      new Promise(resolve => setTimeout(_ => resolve(ms + 1000), ms))
    composePromise(foo, foo)(1000).then(console.log) // 约 3 秒后打印 3000
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
  • ES 提案了一种管道操作符操作 (opens new window)(从左向右执行)。

    const add = num => num + 7
    
    7 >| add >| add // => 21
    
    1
    2
    3

# 9. 面向切面编程(AOP)

  • AOP 是针对业务处理过程中的切面进行提取。对重复的非主业务代码进行抽离,降低耦合性,提高代码的复用性,提高开发效率。

    应用:错误处理,埋点,日志等。

Function.prototype.before = function(callback) {
  if (typeof callback !== 'function')
    throw new TypeError('callback is not a function')
  const self = this
  return function(...args) {
    callback.call(this, ...args)
    return self.call(this, ...args)
  }
}
Function.prototype.after = function(callback) {
  if (typeof callback !== 'function')
    throw new TypeError('callback is not a function')
  const self = this
  return function(...args) {
    const result = self.call(this, ...args)
    callback.call(this, ...args)
    return result
  }
}

function middle() {
  console.log('middle')
  return 'result'
}

middle.before(_ => console.log('before')).after(_ => console.log('after'))() // => 依次打印:'before' -> 'middle' -> 'after' -> '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
26

# 10. 单例模式

  • 保证一个类仅有一个实例,并提供一个访问它的全局访问点。

    应用:登录弹窗等。

const getSingle = fn => {
  let result
  return function(...args) {
    return result || (result = fn.apply(this, args))
  }
}

const createModal = function() {
  const modal = document.createElement('div')
  modal.innerHTML = '登录弹窗'
  modal.style.display = 'none'
  document.body.appendChild(modal)
  return modal
}

const createSingleModal = getSingle(createModal)

createSingleModal() === createSingleModal() // => true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 11. 策略模式

  • 定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。

    在 JavaScript 这种将函数作为一等对象的语言里,策略模式已经融入到了语言本身当中。我们经常用高阶函数来封装不同的行为,并且把它传递到另一个函数中。

const strategies = {
  S: salary => salary * 4,
  A: salary => salary * 3,
  B: salary => salary * 2,
}

const calculateBonus = (level, salary) => strategies[level](salary)

calculateBonus('S', 777) // => 3108
calculateBonus('A', 777) // => 2331
1
2
3
4
5
6
7
8
9
10

# 12. 代理模式

  • 代理模式是为一个对象提供一个代用品或占位符,以便控制对它的访问。

    应用:Vue 3.x 版本数据响应式等。

const foo = {
  name: '徐扶墙',
  age: 17,
  wife: ['姜姒', '裴南苇'],
  skill: {
    name: '天下第二',
  },
}

const proxy = new Proxy(foo, {
  get(target, key) {
    if (key === 'wife') console.log('风紧,扯呼!')
    return target[key]
  },
  set(target, key, value) {
    console.log('裆下有些忧郁啊!') // 设置 proxy.skill.name 的值时不会打印这句话
    return (target[key] = value)
  },
})

proxy.wife // '风紧,扯呼!'
proxy.name = '徐凤年' // '裆下有些忧郁啊!'
proxy.skill.name = '天下第一'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 13. 迭代器模式

  • 迭代器模式是指提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。

  • 迭代器模式是一种相对简单的模式,简单到很多时候我们都不认为它是一种设计模式。目前的绝大部分语言都内置了迭代器。

['jack', 'pony', 'coderljw'].map(i => console.log(i)) // 依次打印:'jack' -> 'pony' -> 'coderljw'
1

# 14. 发布订阅模式(观察者模式)

  • 发布订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。

    应用:DOM2 和 Vue 中的响应式原理、事件监听等。

  • 也可以先发布再订阅。例如微信离线消息,离线消息被保存在服务器中,接收人下次登录上线之后,可以重新收到这条消息。

  • 一部分人认为发布订阅模式与观察者模式是两种模式,认为发布订阅模式是观察者模式的解耦表达。设计模式表达的是程序设计思想,他们区分的发布订阅模式与观察者模式在思想上是一致的。

class Observer {
  message = {}
  // 监听事件
  on(type, cb) {
    if (type in this.message) this.message[type].push(cb)
    else this.message[type] = [cb]
  }
  // 触发事件
  emit(type, ...args) {
    if (type in this.message) this.message[type].forEach(cb => cb(...args))
  }
  // 取消事件中指定的订阅
  off(type, cb) {
    if (type in this.message)
      this.message[type] = this.message[type].filter(fn => fn !== cb)
  }
  // 取消事件
  offAll(type) {
    if (type in this.message) delete this.message[type]
  }
}

const event = new Observer()

const handlerClick1 = (...args) => console.log('click1', ...args)

event.on('click', handlerClick1)
event.on('click', (...args) => console.log('click2', ...args))
event.off('click', handlerClick1)
setTimeout(_ => event.emit('click', '徐扶墙', '南宫仆射'), 3000) // 3秒后打印:'click2 徐扶墙 南宫仆射'
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

# 15. 命令模式

  • 命令模式是最简单和优雅的模式之一,命令模式中的命令(command)指的是一个执行某些特定事情的指令。

    JavaScript 作为将函数作为一等对象的语言,跟策略模式一样,命令模式也早已融入到了 JavaScript 语言之中。

const bindClick = (button, func) => {
  button.onclick = func
}

const MenuBar = {
  refresh() {
    console.log('刷新菜单界面')
  },
}

const SubMenu = {
  add() {
    console.log('增加子菜单')
  },
  del() {
    console.log('删除子菜单')
  },
}

bindClick(button1, MenuBar.refresh)
bindClick(button2, SubMenu.add)
bindClick(button3, SubMenu.del)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 16. 组合模式

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

    应用:文件目录,DOM 文档树。

  • 组合模式应树形结构而生,所以组合模式的使用场景就是出现树形结构的地方。

    • 命令分发:只需要通过请求树的最顶层对象,便能对整棵树做统一的操作。在组合模式中增加和删除树的节点非常方便,并且符合开放-封闭原则。
    • 统一处理:统一对待树中的所有对象,忽略组合对象和叶对象的区别。
// 文件夹(树对象)
class Folder {
  files = []
  constructor(name) {
    this.name = name
  }
  add(file) {
    this.files.push(file)
  }
  scan() {
    console.log(`开始扫描文件夹:${this.name}`)
    this.files.forEach(file => file.scan())
  }
}

// 文件(叶对象)
class File {
  constructor(name) {
    this.name = name
  }
  add(file) {
    throw new Error('文件下面不能再添加文件')
  }
  scan() {
    console.log(`开始扫描文件:${this.name}`)
  }
}

const filmTV = new Folder('影视')
const matrixFolder = new Folder('Matrix')
const onePieceFolder = new Folder('ワンピース')

const matrix = new File('The Matrix.mp4')
const onePiece = new File('俺はルフィ!海賊王になる男だ!.mp4')

matrixFolder.add(matrix)
onePieceFolder.add(onePiece)
filmTV.add(matrixFolder)
filmTV.add(onePieceFolder)
filmTV.scan() // 依次打印:'开始扫描文件夹:影视' -> '开始扫描文件夹:Matrix' -> '开始扫描文件:The Matrix.mp4' -> '开始扫描文件夹:ワンピース' -> '开始扫描文件:俺はルフィ!海賊王になる男だ!.mp4'
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

# 17. 模板方法模式

  • 模板方法模式是一种只需使用继承就可以实现的非常简单的模式。

    优点:容易扩展和便于维护。

  • 在 JavaScript 中,我们很多时候都不需要依样画瓢地去实现一个模版方法模式,高阶函数是更好的选择。

class Beverage {
  boilWater() {
    console.log('把水煮沸')
  }
  // 以下为空方法,应该由子类重写
  brew() {}
  pourInCup() {}
  addCondiments() {}
  // 模板方法
  init() {
    ['boilWater', 'brew', 'pourInCup', 'addCondiments'].map(fn => this[fn]())
  }
}

class Coffee extends Beverage {
  brew() {
    console.log('用沸水冲泡咖啡')
  }
  pourInCup() {
    console.log('把咖啡倒进杯子')
  }
  addCondiments() {
    console.log('加糖和牛奶')
  }
}

const coffee = new Coffee()
coffee.init() // 依次打印:'把水煮沸' -> '用沸水冲泡咖啡' -> '把咖啡倒进杯子' -> '加糖和牛奶'

class Tea extends Beverage {
  brew() {
    console.log('用沸水浸泡茶叶')
  }
  pourInCup() {
    console.log('把茶倒进杯子')
  }
  addCondiments() {
    console.log('加柠檬')
  }
}

const tea = new Tea()
tea.init() // 依次打印:'把水煮沸' -> '用沸水浸泡茶叶' -> '把茶倒进杯子' -> '加柠檬'
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

# 18. 享元模式

  • 享元(flyweight)模式是一种用于性能优化的模式,“fly” 在这里是苍蝇的意思,意为蝇量级。享元模式的核心是运用共享技术来有效支持大量细粒度的对象。

    应用:B 站弹幕等。

  • 对象池是另外一种性能优化方案,它跟享元模式有一些相似之处,但没有分离内部状态和外部状态这个过程。

const FILMQUANTITY = 10000,
  DIVHEIGHT = 25,
  POOLQUANTITY = 20

const films = Array(FILMQUANTITY)
  .fill(0)
  .map((_, index) => ({
    name: `${Math.random()
      .toString(36)
      .substr(2, 10)} --- ${index}`,
    category: Math.random() > 0.5 ? 'SecondDimension' : 'ThirdDimension',
  }))

// film 享元对象
class FilmFlyweight {
  constructor(category) {
    // 定义内部状态
    this.category = category
  }
  // 设置外部状态
  setExternalState(state) {
    Object.entries(state).forEach(([key, value]) => this[key] = value)
  }
  getState() {
    const { name, category } = this
    return { name, category }
  }
}

// 创建享元对象(单例模式)
const createFilmFlyweight = (function() {
  const store = {}
  return function(category) {
    if (!store[category]) return (store[category] = new FilmFlyweight(category))
    return store[category]
  }
})()

// div 享元对象
class Div {
  node = document.createElement('div')
  // 设置外部状态
  setExternalState({ text, serialNumber }, onClick) {
    const { node } = this
    node.innerText = text
    node.style.position = 'absolute'
    node.style.top = `${serialNumber * DIVHEIGHT}px`
    node.onclick = onClick
  }
}

// 创建 div 享元对象
const createDiv = (function() {
  // 对象池
  const pool = []
  return function(container) {
    let div
    if (pool.length <= POOLQUANTITY) div = new Div()
    else div = pool.shift()

    pool.push(div)
    container.appendChild(div.node)
    return div
  }
})()

// 创建容器
const { outerContainer, innerContainer } = (function() {
  const outerContainer = document.createElement('div')
  const innerContainer = document.createElement('div')

  outerStyle = {
    width: '200px',
    height: '400px',
    maxHeight: '400px',
    border: '1px solid red',
    overflow: 'auto',
  }
  innerStyle = {
    position: 'relative',
    height: `${DIVHEIGHT * films.length}px`,
  }
  Object.entries(outerStyle).forEach(([key, value]) => outerContainer.style[key] = value)
  Object.entries(innerStyle).forEach(([key, value]) => innerContainer.style[key] = value)

  outerContainer.appendChild(innerContainer)
  document.body.appendChild(outerContainer)
  return { outerContainer, innerContainer }
})()

// 加载节点
function loadNode(start, end) {
  films.slice(start, end).forEach((filmData, index) => {
    const filmFlyweight = createFilmFlyweight(filmData.category)
    const div = createDiv(innerContainer)
    div.setExternalState(
      { text: filmData.name, serialNumber: start + index },
      () => {
        filmFlyweight.setExternalState({ name: filmData.name })
        console.log(filmFlyweight.getState())
      }
    )
  })
}

loadNode(0, POOLQUANTITY)
let current = 0
outerContainer.addEventListener('scroll', e => {
  const start = (outerContainer.scrollTop / DIVHEIGHT) | 0
  if (start !== current) {
    loadNode(start, start + POOLQUANTITY)
    current = start
  }
})
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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114

# 19. 职责链模式

  • 使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系,将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。

    应用:拦截器等。

const order500 = (type, pay, stock) => {
  if (type === 1 && pay) return '500 元定金预购,得到 100 优惠券'
  return 'nextSuccessor'
}
const order200 = (type, pay, stock) => {
  if (type === 2 && pay) return '200 元定金预购,得到 50 优惠券'
  return 'nextSuccessor'
}
const orderNormal = (type, pay, stock) => {
  if (stock > 0) return '普通购买,无优惠券'
  return '手机库存不足'
}

Function.prototype.after = function(callback) {
  if (typeof callback !== 'function')
    throw new TypeError('callback is not a function')
  const self = this
  return function(...args) {
    const result = self.call(this, ...args)
    if (result === 'nextSuccessor') return callback.call(this, ...args)
    return result
  }
}

const order = order500.after(order200).after(orderNormal)

order(1, true, 500) // => '500 元定金预购,得到 100 优惠券'
order(2, true, 500) // => '200 元定金预购,得到 50 优惠券'
order(1, false, 500) // => '普通购买,无优惠券'
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

# 20. 中介者模式

  • 中介者模式的作用就是解除对象与对象之间的紧耦合关系,使网状的多对多关系变成了相对简单的一对多关系。

  • 中介者模式和发布订阅模式非常相似,区别在于通信方式和结构上的差异。

    • 中介者模式中每个模块都可以发布消息(中介者本身也可以发布消息),而发布订阅模式中观察者只能被动的等待消息。
    • 中介者模式是星状结构,中介者是一个 ”控制点“,而发布订阅机制本身是一个 ”控制层”,意味着高层可以通过控制层操作下层模块(当然也可以通过代码实现下层控制高层)。
class Player {
  partners = [] // 队友列表
  enemies = [] // 敌人列表
  state = 'live' // 玩家状态
  constructor(name, teamColor) {
    this.name = name // 角色名字
    this.teamColor = teamColor // 队伍颜色
  }
  // 胜利
  win() {
    console.log('winner: ' + this.name)
  }
  // 失败
  lose() {
    console.log('loser: ' + this.name)
  }
  // 死亡
  die() {
    const { partners, enemies } = this
    this.state = 'dead'
    const allDead = partners.every(i => i.state === 'dead')

    if (allDead === true) {
      this.lose()
      partners.map(i => i.lose())
      enemies.map(i => i.win())
    }
  }
}

// 玩家列表
const players = []

// 创建玩家
const createPlayer = (name, teamColor) => {
  const newPlayer = new Player(name, teamColor)
  players.map(i => {
    if (i.teamColor === newPlayer.teamColor) {
      // 同队
      i.partners.push(newPlayer)
      newPlayer.partners.push(i)
    } else {
      // 敌队
      i.enemies.push(newPlayer)
      newPlayer.enemies.push(i)
    }
  })

  players.push(newPlayer)
  return newPlayer
}

// 红队
const player1 = createPlayer('Neo', 'red'),
  player2 = createPlayer('Trinity', 'red'),
  player3 = createPlayer('Morpheus', 'red'),
  player4 = createPlayer('Tank', 'red')

// 蓝队
const player5 = createPlayer('Smith', 'blue'),
  player6 = createPlayer('Oracle', 'blue'),
  player7 = createPlayer('Architect', 'blue'),
  player8 = createPlayer('Merovingian', 'blue')

player5.die()
player6.die()
player7.die()
player8.die()
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

# 21. 装饰者模式

  • 装饰者模式能够在不改变对象自身的基础上,在程序运行期间给对象动态地添加职责。跟继承相比,装饰者是一种更轻便灵活的做法,这是一种 “即用即付” 的方式。

    const hero = {
      buff() {
        console.log('蓝BUFF')
      },
    }
    const redDecorator = () => {
      console.log('红BUFF')
    }
    const dragonDecorator = () => {
      console.log('大龙BUFF')
    }
    const buff1 = hero.buff
    hero.buff = () => {
      buff1()
      redDecorator()
    }
    const buff2 = hero.buff
    hero.buff = () => {
      buff2()
      dragonDecorator()
    }
    hero.buff() // => 依次打印:蓝BUFF -> 红BUFF -> 大龙BUFF
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
  • ES 提案中的装饰器(ECMAScript 6 入门 - 阮一峰 (opens new window))。

    const blueDecorator = () => {
      console.log('蓝BUFF')
    }
    const redDecorator = () => {
      console.log('红BUFF')
    }
    const dragonDecorator = () => {
      console.log('大龙BUFF')
    }
    
    class Hero {
      @blueDecorator
      @redDecorator
      @dragonDecorator
      buff() {}
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16

# 22. 状态模式

  • 允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。

    应用:React、Vue 等框架中的组件。

  • 与策略模式之间的区别:策略模式中的各个策略类之间是平等又平行的,它们之间没有任何联系,所以客户必须熟知这些策略类的作用,以便客户可以随时主动切换算法。而在状态模式中,状态和状态对应的行为是早已被封装好的,状态之间的切换也早被规定完成,“改变行为” 这件事情发生在状态模式内部。对客户来说,并不需要了解这些细节,这正是状态模式的作用所在。

const delegate = (client, delegation) => {
  return {
    buttonWasPressed() {
      // 将客户的操作委托给 delegation 对象
      return delegation.buttonWasPressed.apply(client, arguments)
    },
  }
}
const FSM = {
  off: {
    buttonWasPressed() {
      console.log('关灯')
      this.button.innerHTML = '下一次按我是开灯'
      this.currentState = this.onState
    },
  },
  on: {
    buttonWasPressed() {
      console.log('开灯')
      this.button.innerHTML = '下一次按我是关灯'
      this.currentState = this.offState
    },
  },
}
class Light {
  button
  constructor() {
    this.offState = delegate(this, FSM.off)
    this.onState = delegate(this, FSM.on)
    this.currentState = this.offState // 设置初始状态为关闭状态
  }
  init() {
    const button = document.createElement('button')
    button.innerHTML = '已关灯'
    this.button = document.body.appendChild(button)
    this.button.onclick = _ => this.currentState.buttonWasPressed()
  }
}

const light = new Light()
light.init()
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

# 23. 适配器模式

  • 适配器模式是一对相对简单的模式。作用是解决两个软件实体间的接口不兼容的问题。使用适配器模式之后,原本由于接口不兼容而不能工作的两个软件实体可以一起工作。

    应用:Vue 中的 computed 等。

  • 如果现有的接口已经能够正常工作,那我们就永远不会用上适配器模式。适配器模式是一种 “亡羊补牢” 的模式,没有人会在程序的设计之初就使用它。

const googleMap = {
  show() {
    console.log('开始渲染谷歌地图')
  },
}

const baiduMap = {
  display() {
    console.log('开始渲染百度地图')
  },
}

// 适配 renderMap
const baiduMapAdapter = {
  show() {
    return baiduMap.display()
  },
}

const renderMap = map => map?.show?.()

renderMap(googleMap)
renderMap(baiduMapAdapter)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
以父之名
周杰伦.mp3