venaissance / myBlog

💡 致力于提升技术学习效率的博客
https://venaissance.github.io/myBlog
23 stars 3 forks source link

一次搞定四种重要的 JavaScript 设计模式 #19

Open venaissance opened 3 years ago

venaissance commented 3 years ago

什么是设计模式?

设计模式就是代码问题的一套解决方案,或者说,解决特定问题的“最佳实践”。

“每一个模式描述了一个在我们周围不断重复发生的问题,以及该问题的解决方案的核心。这样。你就能一次又一次地使用该方案而不必做重复劳动” - Christopher Alexander

常见的设计模式大概有23种,这里以一张图片说明。

image

本文先聊聊以下四种设计模式。

对象

JS 面向对象的改造

// 原始代码
function startAnimation() {
    console.log('start animation')
}
function stopAnimation() {
    console.log('stop animation')
}

// 改为面向对象的方式
var Anim = function() {
}
Anim.prototype.start = function() {
    console.log('start animation')
}
Anim.prototype.stop = function() {
    console.log('stop animation')
}
var myAnim = new Anim()
myAnim.start()
myAnim.stop()

// 变形简化
var Anim = function() {}
Anim.prototype = {
    start: function() {
        console.log('start')
    },
    stop: function() {
        console.llog('end')
    }
}

// 持续改进
Function.prototype.method = function(name, fn) {
    this.prototype[name] = fn
}
var Anim = function() {
}
Anim.method('start', function() {
    console.log('start animation')
})
Anim.method('stop', function() {
    console.log('stop animation')
})

// 继续改进,链式调用
Function.prototype.method = function(name, fn) {
    this.prototype[name] = fn
    return this
}
var Anim = function() {}
Anim.method('start', function() {
    console.log('start animation')
}).method('stop', function() {
    console.log(stop animation')
})

JS 匿名函数

// 以下是立即执行的函数表达式
(function(){
    var foo = 1
    var bar = 2
    console.log(foo * bar)
})()

// 携带参数
(function(foo, bar){
   console.log(foo * bar)
})(1, 2)

// 闭包,访问函数内部的局部变量
var baz
(function(){
    var foo = 1
    var bar = 2
    baz = function() {
        return foo * bar
    }
})()
baz()

组合模式(Composite Pattern)

image

特点:

class Container {
  constructor(id) {
    this.children = []
    this.element = document.createElement('div')
    this.element.id = id
    this.element.style.border = '1px solid black'
    this.element.style.margin = '10px'
    this.element.classList.add('container')    
  }

  add(child) {
    this.children.push(child)
    this.element.appendChild(child.getElement())
  }

  hide() {
    this.children.forEach(node => node.hide())
    this.element.style.display = 'none'
  }

  show() {
    this.children.forEach(node => node.show())
    this.element.style.display = ''
  }

  getElement() {
    return this.element
  }

}

class Text {
  constructor(text) {
    this.element = document.createElement('p')
    this.element.innerText = text
  }

  add() {}

  hide() {
    this.element.style.display = 'none'
  }

  show() {
    this.element.style.display = ''
  }

  getElement() {
    return this.element
  }
}

let header = new Container('header')
header.add(new Text('标题'))
header.add(new Text('logo'))

let main = new Container('main')
main.add(new Text('这是内容1'))
main.add(new Text('这是内容2'))

let page = new Container('page')
page.add(header)
page.add(main)
page.show()

document.body.appendChild(page.getElement())

适配器模式(Adapter Pattern)

In software engineering, the adapter pattern is a software design pattern (also known as wrapper, an alternative naming shared with the decorator pattern) that allows the interface of an existing class to be used as another interface. It is often used to make existing classes work with others without modifying their source code. --from wiki

一句话,适配器模式可以类比于家里的插座,上面有不同类型的插孔,用来适配不同的插头。

实际的开发场景

以下为适配器模式的代码示例。

const  localStorageAdapter = {
  findAll: function(callback) {
    let cartList = JSON.parse(localStorage['cart'])
    callback(cartList)
  },
  save: function(item) {
    let cartList = JSON.parse(localStorage['cart'])
    cartList.push(item)
    localStorage['cart'] = JSON.stringify(cartList)
  }
}

const  serverAdapter = {
  findAll: function(callback) {
      fetch('https://someAPI.com/getCartList')
        .then(res => res.json())
        .then(data => callback(data))
  },
  save: function(item) {
    fetch('https://someAPI.com/addToCart', { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(item) })
      .then(res => res.json())
      .then(data => callback(data))
  }
}

class ShoppingCart {
    constructor(adapter) {
        this.adapter = adapter
    }
    add(item) {
        this.adapter.save(item)
    }
    show() {
        this.adapter.findAll(list => {
            console.log(list)
        } )
    }
}

let cart = new ShoppingCart(localStorageAdapter) // 使用了 localStorage 这个插孔
// let cart = new ShoppingCart(serverAdapter)
cart.add({title: '手机'})
cart.add({title: '电脑'})
cart.show()

装饰者模式(Decorator Pattern)

先问一个问题:如何给一个对象增加额外的功能?

方法1:直接修改对象

方法2:创建子类继承自父类,子类实例化新对象(如果太细,会导致子类对象的泛滥)

方法3:不改变原对象,在原对象基础上进行“装饰”,新增一些和核心功能无关的功能

装饰器模式即为上述方法3

In object-oriented programming, the decorator pattern is a design pattern that allows behavior to be added to an individual object, dynamically, without affecting the behavior of other objects from the same class. --wiki

套用 Wiki 的描述,装饰器模式的最大特点是保留主干,增加装饰,但不影响原功能。假如用一个成语来形容,我认为最好的莫过于“锦上添花”了。

使用场景

AOP 面向切面编程

Java 大名鼎鼎的 Spring 框架的核心编程思想 - AOP 面向切面编程,装饰器模式就是它比较常见的实现。

下面用 JS 来实现一个简单的 AOP 对象。

const AOP = {}
AOP.before = function (fn, before) {
    return function() {
        before.apply(this,arguments)
        fn.apply(this, arguments)
    }
}
AOP.after = function(fn, after) {
    return function () {
        fn.apply(this, arguments)
        after.apply(this, arguments)
    }
}

// 点击按钮提交数据
function submit() {
    console.log('提交数据')
}

document.querySelector('.btn').onclick = submit

// 在原有功能基础上做点装饰:点击按钮,提交数据前做个校验
function submit() {
    console.log(this)
    console.log('提交数据')
}
function check() {
    console.log(this)
    console.log('先进行校验')
}
submit = AOP.before(submit, check)
document.querySelector('.btn').onclick = submit

ES7 Decorator 修饰符

借鉴其他语言的优势,JS 在 ES7 推出了 Decorator 修饰符,不好理解也不太实用,但如果熟悉装饰器模式的话,理解起来就容易多了:这不就是装饰器模式的语法糖嘛,示例如下。

const logWrapper = targetClass => {
    let orignRender = targetClass.prototype.render
    targetClass.prototype.render = function(){
        console.log("before render")
        orignRender.apply(this) 
        console.log("after render")
    }
    return targetClass
}

class App {
    constructor() {
      this.title = '首页'
    }
    render(){
        console.log('渲染页面:' + this.title);
    }
}

App = logWrapper(App)

new App().render()

// 使用 decorator 修饰符,修改class
const logWrapper = targetClass => {
  let orignRender = targetClass.prototype.render
  targetClass.prototype.render = function(){
    console.log("before render")
    orignRender.apply(this) 
    console.log("after render")
  }
  return targetClass
}

@logWrapper
class App {
  constructor() {
    this.title = '首页'
  }
  render(){
    console.log('渲染页面:' + this.title);
  }
}

使用 Decorator 修饰符,修改原型属性

Tips: 运行代码需babel支持,把JavaScript模式调整到ES6/babel

function logWrapper(target, name, descriptor) {
    console.log(arguments)
    let originRender = descriptor.value
    descriptor.value = function() {
      console.log('before render')
      originRender.bind(this)()
      console.log('after render')
    }
    console.log(target)
}

class App {
  constructor() {
    this.title = '首页'
  }
  @logWrapper
  render(){
    console.log('渲染页面:' + this.title);
  }
}

new App().render()

总结

设计模式看起来似乎有些枯燥乏味,学习起来短期可能也并不能见到效果,但当业务变得复杂、代码变得越来越冗余时,设计模式就能给你正确的指引。毕竟代码相关的“最佳实践”流传至今,必定有它存在的道理。

本文内容大多来自经典书籍 《Pro JavaScript Design Patterns》,这本书讲解了十几种 JavaScript 经典的设计模式。学习设计模式不仅能大幅提高 JavaScript 的内功修为,更是程序员进阶之路上必不可少的一座山。