跳到主要内容位置

设计模式笔记

设计原则 S O L I D (五大设计原则)#

S -- 单一职责原则 一个程序只做好一个事 O -- 开放封闭原则 对扩展开放,对修改封闭 L -- 李氏置换原则 子类能覆盖父类 I -- 接口独立原则 保持接口独立,避免‘胖接口’ D -- 依赖倒置原则 依赖抽象,不依赖具体。 使用方只关注接口而不关注具体类的实现

创建型模式#

工厂模式#

工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象,用工厂方法代替 new 操作的一种模式。

class Creator {
create(name) {
return new Animal(name)
}
}
class Animal {
constructor(name) {
this.name = name
}
}
var creator = new Creator()
var duck = creator.create('Duck')
console.log(duck.name) // Duck
var chicken = creator.create('Chicken')
console.log(chicken.name) // Chicken

小结:

  1. 构造函数和创建者分离,对 new 操作进行封装
  2. 符合开放封闭原则
场景#

React 的 createElement Vue 的 异步组件

单例模式#

class Login {
createLayout() {
var oDiv = document.createElement('div')
oDiv.innerHTML = '我是登录框'
document.body.appendChild(oDiv)
oDiv.style.display = 'none'
return oDiv
}
}
class Single {
getSingle(fn) {
var result;
return function() {
return result || (result = fn.apply(this, arguments))
}
}
}
var oBtn = document.getElementById('btn')
var single = new Single()
var login = new Login()
// 由于闭包,createLoginLayer对result的引用,所以当single.getSingle函数执行完之后,内存中并不会销毁result。
// 当第二次以后点击按钮,根据createLoginLayer函数的作用域链中已经包含了result,所以直接返回result
// 讲获取单例和创建登录框的方法解耦,符合开放封闭原则
var createLoginLayer = single.getSingle(login.createLayout)
oBtn.onclick = function() {
var layout = createLoginLayer()
layout.style.display = 'block'
}

小结:

  1. 单例模式的主要思想就是,实例如果已经创建,则直接返回
function creatSingleton() {
var obj = null
// 实例如已经创建过,直接返回
if (!obj) {
obj = xxx
}
return obj
}
  1. 符合开放封闭原则

原型模式#

用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。-- 百度百科

在 JavaScript 中,实现原型模式是在 ECMAScript5 中,提出的 Object.create 方法,使用现有的对象来提供新创建的对象的proto

var prototype = {
name: 'Jack',
getName: function() {
return this.name
}
}
var obj = Object.create(prototype, {
job: {
value: 'IT'
}
})
console.log(obj.getName()) // Jack
console.log(obj.job) // IT
console.log(obj.__proto__ === prototype) //true

结构型模式#

适配器模式#

class GooleMap {
show() {
console.log('渲染谷歌地图')
}
}
class BaiduMap {
display() {
console.log('渲染百度地图')
}
}
// 定义适配器类, 对BaiduMap类进行封装
class BaiduMapAdapter {
show() {
var baiduMap = new BaiduMap()
return baiduMap.display()
}
}
function render(map) {
if (map.show instanceof Function) {
map.show()
}
}
render(new GooleMap()) // 渲染谷歌地图
render(new BaiduMapAdapter()) // 渲染百度地图

小结:

  1. 适配器模式主要解决两个接口之间不匹配的问题,不会改变原有的接口,而是由一个对象对另一个对象的包装。
  2. 适配器模式符合开放封闭原则

代理模式#

本文举一个使用代理对象加载图片的例子来理解代理模式,当网络不好的时候,图片的加载需要一段时间,这就会产生空白,影响用户体验,这时候我们可在图片真正加载完之前,使用一张 loading 占位图片,等图片真正加载完再给图片设置 src 属性。

class MyImage {
constructor() {
this.img = new Image()
document.body.appendChild(this.img)
}
setSrc(src) {
this.img.src = src
}
}
class ProxyImage {
constructor() {
this.proxyImage = new Image()
}
setSrc(src) {
let myImageObj = new MyImage()
myImageObj.img.src = 'file://xxx.png' //为本地图片url
this.proxyImage.src = src
this.proxyImage.onload = function() {
myImageObj.img.src = src
}
}
}
var proxyImage = new ProxyImage()
proxyImage.setSrc('http://xxx.png') //服务器资源url

本例中,本体类中有自己的 setSrc 方法,如果有一天网络速度已经不需要预加载了,我们可以直接使用本体对象的 setSrc 方法,并且不需要改动本体类的代码,而且可以删除代理类。

小结:

  1. 代理模式符合开放封闭原则
  2. 本体对象和代理对象拥有相同的方法,在用户看来并不知道请求的本体对象还是代理对象。

外观模式#

行为型模式#

装饰器模式#

装饰器 (Decorator) 是 ES7 的一个语法,是一种与类相关的语法,用来注释或修改类和类的方法。

装饰器是一种函数,写成 @ + 函数名。它可以放在类和类方法的定义前面

core-decorators.js#

@autobind:使得方法中的 this 对象,绑定原始对象 @readonly:使得属性或方法不可写。 @override:检查子类的方法,是否正确覆盖了父类的同名方法,如果不正确会报错。 @deprecate (别名 @deprecated):在控制台显示一条警告,表示该方法将废除。

观察者模式(发布 - 订阅模式) 请看《JavaScript 设计模式与开发实践》 110-124 页#

我们可以只订阅自己感兴趣的事件 就像 EventHub 可以取消订阅 也可以发布后订阅(一个存放离线事件的堆栈) 可以创建命名空间

js 中可以利用 arguments 方便的拿到注册的回调函数 来实现优雅的发布订阅模式(不需要像传统的方式需要订阅者自身提供 update 方法)

// 主题,接收状态变化,触发每个观察者
class Subject {
constructor() {
this.state = 0
this.observers = []
}
getState() {
return this.state
}
setState(state) {
this.state = state
this.notifyAllObservers()
}
attach(observer) {
this.observers.push(observer)
}
notifyAllObservers() {
this.observers.forEach(observer => {
observer.update()
})
}
}
// 观察者,等待被触发
class Observer {
constructor(name, subject) {
this.name = name
this.subject = subject
this.subject.attach(this)
}
update() {
console.log(`${this.name} update, state: ${this.subject.getState()}`)
}
}
// 测试代码
let s = new Subject()
let o1 = new Observer('o1', s)
let o2 = new Observer('o2', s)
let o3 = new Observer('o3', s)
s.setState(1)
s.setState(2)
s.setState(3)

迭代器模式#

var iteratorUploadObj = function() {
//能在 for 里面把数据都遍历出来
//会在 fn 取值后 i 自加,所以序号是从1开始的
for (var i = 0, fn; fn = argument[i++];) {
var uploadObj = fn()
if(uploadObj !== false) {
return uploadObj
}
}
}
var uploadObj = iteratorUploadObj(getx,getxx,getxxx)
// 数组原生具备 iterator 接口(即默认部署了 Symbol.iterator 属性),for...of 循环本质上就是调用这个接口产生的遍历器,可以用下面的代码证明。
const arr = ['red', 'green', 'blue'];
for(let v of arr) {
console.log(v); // red green blue
}
const obj = {};
obj[Symbol.iterator] = arr[Symbol.iterator].bind(arr);
for(let v of obj) {
console.log(v); // red green blue
}
// 上面代码中,空对象 obj 部署了数组 arr 的 Symbol.iterator 属性,结果 obj 的 for...of 循环,产生了与 arr 完全一样的结果。
// for...of 循环可以代替数组实例的 forEach 方法。

for...in 循环有几个缺点。 数组的键名是数字,但是 for...in 循环是以字符串作为键名“0”、“1”、“2”等等。 for...in 循环不仅遍历数字键名,还会遍历手动添加的其他键,甚至包括原型链上的键。 某些情况下,for...in 循环会以任意顺序遍历键名。 总之,for...in 循环主要是为遍历对象而设计的,不适用于遍历数组。

最近更新于