精准而优雅的设计模式

1,477 阅读10分钟

构造器

名字吓人。结果天天使用。

  • 场景: 公司员工信息录入系统、少量员工录入
const lilei = {
  name: '李磊',
  age: 25,
  career: 'coder'
}
// 构造器方式
function User(name, age, career) {
  this.name = name;
  this.age = age;
  this.career = career;
}
const lilei = new User('李磊', 25, 'coder');
  • 程序自动地去读取数据库里面一行行的员工信息,然后把拿到的姓名、年龄职业等字段塞进User函数里,进行一个简单的调用。

  • 构造器是不是将 name、age、career 赋值给对象的过程封装,确保了每个对象都具备这些属性,确保了共性的不变,同时将 name、age、career 各自的取值操作开放

简单工厂模式

  • 场景: 区分员工的职业。如果是码农就写Bug。如果老板就会所。
// 构造器方式
function User(name, age, career, work) {
  this.name = name;
  this.age = age;
  this.career = career;
  this.work = work;
}
function Factory(name, age, career){
  let work;
  swtich(career){
    case 'coder':
    	work = '写Bug';
    	break;
    case 'boss':
    	work = '会所';
    	break;
    default:
    	break;
  }
 	return new User(name, age, career, work)
}

const pro = new Factory('pro', 18, 'boss');

总结: 工厂模式的简单之处,在于它的概念相对好理解:将创建对象的过程单独封装,这样的操作就是工厂模式。同时它的应用场景也非常容易识别:有构造函数的地方,我们就应该想到简单工厂;在写了大量构造函数、调用了大量的 new、自觉非常不爽的情况下,我们就应该思考是不是可以掏出工厂模式重构我们的代码了

单例模式

  • 只有一个实例
class Modal{
		static getModal(){
			if(!Modal.modal){
					Modal.modal  = 123;
			}
			return Modal.modal;
		}
}
const modal1 = Modal.getModal();
const modal2 = Modal.getModal();
modal1 === modal2; // true
  • 场景: UI框架其中的modal只有一个实例
<!DOCTYPE html>
<html lang="zh-cn">
<head>
    <meta charset="UTF-8">
    <title>单例模式弹框</title>
</head>
<style>
    #modal {
        width: 200px;
        height: 200px;
        line-height: 200px;
        text-align: center;
        border-radius: 10px;
        background-color: #f2f2f2;
        position: absolute;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
    }
</style>
<body>
<div class="btnBox">
    <button id='open'>打开弹框</button>
    <button id='close'>关闭弹框</button>
</div>

</body>
<script>
    // 闭包方式
    // const Modal = (function () {
    //     let modal = null;
    //     return function() {
    //         if (!modal){
    //             modal = document.createElement('div');
    //             modal.id = 'modal';
    //             modal.style.display = 'none';
    //             modal.innerHTML = "唯一弹窗";
    //             document.body.appendChild(modal);
    //
    //         }
    //         return modal;
    //     }
    // })();

    // class方式
    class Modal {
        static getModal() {
            if (!Modal.modal) {
                Modal.modal = document.createElement('div');
                Modal.modal.id = 'modal';
                Modal.modal.style.display = 'none';
                Modal.modal.innerHTML = '唯一弹窗';
                document.body.appendChild(Modal.modal);
            }
            return Modal.modal;
        }
    }

    // 点再多下也只是有Modal
    document.getElementById('open').addEventListener('click', function () {
        const modal = Modal.getModal();
        modal.style.display = 'block';
    });
    // 点击关闭按钮隐藏模态框
    document.getElementById('close').addEventListener('click', function () {
        const modal = Modal.getModal();
        modal.style.display = 'none';
    });
</script>
</html>

原型模式

  • 原型是 把所有的对象共用的属性全部放在堆内存的一个对象中(共用属性组成的对象),然后让每一个对象的__proto__存储这个(共用属性组成的对象)的地址。而这个共用属性就是原型。原型出现的目的就是为了减少不必要的内存消耗。

  • 原型链就是对象通过__proto__向当前实例所属类的原型上查找属性或方法的机制,如果找到Object的原型上还是没有找到想要的属性或者是方法则查找结束,最终会返回undefined,终点是null。

  • 原型模式不仅是一种设计模式,它还是一种编程范式,是 JavaScript 面向对象系统实现的根基。

function Dog() {
}
// 原型增加属性和方法
Dog.prototype.name = 'pro';
Dog.prototype.eat = () => {
  console.log(123);
}

装饰器模式

只添加,不修改就是装饰器模式了

  • 场景: 初始需求是每个业务中的按钮在点击后都弹出「您还未登录哦」的弹框。
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>按钮点击需求1.0</title>
</head>
<style>
    #modal {
        height: 200px;
        width: 200px;
        line-height: 200px;
        position: fixed;
        left: 50%;
        top: 50%;
        transform: translate(-50%, -50%);
        border: 1px solid black;
        text-align: center;
    }
</style>
<body>
	<button id='open'>点击打开</button>
	<button id='close'>关闭弹框</button>
</body>
<script>
    // 弹框创建逻辑,这里我们复用了单例模式面试题的例子
    const Modal = (function() {
    	let modal = null
    	return function() {
            if(!modal) {
            	modal = document.createElement('div')
            	modal.innerHTML = '您还未登录哦~'
            	modal.id = 'modal'
            	modal.style.display = 'none'
            	document.body.appendChild(modal)
            }
            return modal
    	}
    })()
    
    // 点击打开按钮展示模态框
    document.getElementById('open').addEventListener('click', function() {
        // 未点击则不创建modal实例,避免不必要的内存占用
    	const modal = new Modal()
    	modal.style.display = 'block'
    })
    
    // 点击关闭按钮隐藏模态框
    document.getElementById('close').addEventListener('click', function() {
    	const modal = document.getElementById('modal')
    	if(modal) {
    	    modal.style.display = 'none'
    	}
    })
</script>
</html>
  • 突然修改需求:弹框被打开后把按钮的文案改为“快去登录”,同时把按钮置灰。存在几百按钮且同时他们不是组件且有复杂业务情况下。不去关心它现有的业务逻辑是啥样的。对它已有的功能做个拓展,只关心拓展出来的那部分新功能如何实现

为了不被已有的业务逻辑干扰,当务之急就是将旧逻辑与新逻辑分离,把旧逻辑抽出去

// 将展示Modal的逻辑单独封装
function openModal() {
    const modal = new Modal()
    modal.style.display = 'block'
}
// 新增逻辑
// 按钮文案修改逻辑
function changeButtonText() {
    const btn = document.getElementById('open')
    btn.innerText = '快去登录'
}

// 按钮置灰逻辑
function disableButton() {
    const btn =  document.getElementById('open')
    btn.setAttribute("disabled", true)
}

// 新版本功能逻辑整合
function changeButtonStatus() {
    changeButtonText()
    disableButton()
}

document.getElementById('open').addEventListener('click', function() {
    openModal()
    changeButtonStatus()
})
  • 使用ES6面向对象写法
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>按钮点击需求1.0</title>
</head>
<style>
    #modal {
        height: 200px;
        width: 200px;
        line-height: 200px;
        position: fixed;
        left: 50%;
        top: 50%;
        border-radius: 10px;
        transform: translate(-50%, -50%);
        border: 1px solid black;
        text-align: center;
    }
</style>
<body>
<button id='open'>点击打开</button>
<button id='close'>关闭弹框</button>
</body>
<script>
    // 弹框创建逻辑,这里我们复用了单例模式面试题的例子
    const Modal = (function() {
        let modal = null;
        return function() {
            if(!modal) {
                modal = document.createElement('div');
                modal.innerHTML = '您还未登录哦~';
                modal.id = 'modal';
                modal.style.display = 'none';
                document.body.appendChild(modal)
            }
            return modal
        }
    })();

    // 定义打开按钮
    class OpenButton {
        onClick() {
            const modal = new Modal();
            modal.style.display = 'block';
        }
    }
    // 定义按钮对应的装饰器
    class Decorator{
        // 将按钮传入
        constructor(open_button) {
            this.open_button = open_button;
        }
        onClick(){
            this.open_button.onClick();
            this.changeButtonStatus();
        }
        changeButtonStatus() {
            this.disableButton();
            this.changeButtonText();
        }
        disableButton() {
            const btn = document.getElementById('open');
            btn.setAttribute('disabled', true);
        }
        changeButtonText() {
            const btn = document.getElementById('open');
            btn.innerText = '快去登录'
        }
    }

    // 点击打开按钮展示模态框
    document.getElementById('open').addEventListener('click', function() {
        // 未点击则不创建modal实例,避免不必要的内存占用
        const openButton = new OpenButton();
        const decorator = new Decorator(openButton);
        decorator.onClick();
    });

    // 点击关闭按钮隐藏模态框
    document.getElementById('close').addEventListener('click', function() {
        const modal = document.getElementById('modal');
        if(modal) {
            modal.style.display = 'none'
        }
    })
</script>
</html>

适配模式

适配器模式通过把一个类的接口变换成客户端所期待的另一种接口,可以帮我们解决不兼容的问题。

  • 场景: iPhoneX没有圆头耳机孔、转接头是个适配模式。

把一个(iPhone X)的接口(方形)变换成客户端(用户)所期待的另一种接口(圆形)

  • axios能在网页和Nodejs不同环境使用就是使用了适配模式

代理模式

代理模式,式如其名——在某些情况下,出于种种考虑/限制,一个对象不能直接访问另一个对象,需要一个第三者(代理)牵线搭桥从而间接达到访问目的,这样的模式就是代理模式

代理服务器 = 代理模式

  • 事件代理也是代理模式的一种

事件代理,可能是代理模式最常见的一种应用方式,也是一道实打实的高频面试题。它的场景是一个父元素下有多个子元素。

需求: 点击每个 a 标签,都可以弹出“我是xxx”这样的提示。比如点击第一个 a 标签,弹出“我是链接1号”这样的提示

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>事件代理</title>
</head>
<body>
<div id="father">
    <a href="#">链接1号</a>
    <a href="#">链接2号</a>
    <a href="#">链接3号</a>
    <a href="#">链接4号</a>
    <a href="#">链接5号</a>
    <a href="#">链接6号</a>
</div>
<script>
    const father = document.getElementById('father');
    father.addEventListener('click',(e) => {
        if (e.target.tagName === 'A'){
            e.preventDefault();
            alert(`我是${e.target.innerText}`);
        }
    })
</script>
</body>
</html>

策略模式

需求:

  • 当价格类型为“预售价”时,满 100 - 20,不满 100 打 9 折
  • 当价格类型为“大促价”时,满 100 - 30,不满 100 打 8 折
  • 当价格类型为“返场价”时,满 200 - 50,不叠加
  • 当价格类型为“尝鲜价”时,直接打 5 折

转成字段

预售价 - pre
大促价 - onSale
返场价 - back
尝鲜价 - fresh

当初的我对prd处理为:

// 询价方法,接受价格标签和原价为入参
function askPrice(tag, originPrice) {

  // 处理预热价
  if(tag === 'pre') {
    if(originPrice >= 100) {
      return originPrice - 20
    } 
    return originPrice * 0.9
  }
  
  // 处理大促价
  if(tag === 'onSale') {
    if(originPrice >= 100) {
      return originPrice - 30
    } 
    return originPrice * 0.8
  }
  
  // 处理返场价
  if(tag === 'back') {
    if(originPrice >= 200) {
      return originPrice - 50
    }
    return originPrice
  }
  
  // 处理尝鲜价
  if(tag === 'fresh') {
     return originPrice * 0.5
  }
}

现在的我会采用策略模式:

// 定义一个询价处理器对象
const priceProcessor = {
  pre(originPrice) {
    if (originPrice >= 100) {
      return originPrice - 20;
    }
    return originPrice * 0.9;
  },
  onSale(originPrice) {
    if (originPrice >= 100) {
      return originPrice - 30;
    }
    return originPrice * 0.8;
  },
  back(originPrice) {
    if (originPrice >= 200) {
      return originPrice - 50;
    }
    return originPrice;
  },
  fresh(originPrice) {
    return originPrice * 0.5;
  },
};

// 询价函数
function askPrice(tag, originPrice) {
  return priceProcessor[tag](originPrice)
}
// 如要增加需求
priceProcessor.newUser = function (originPrice) {
  if (originPrice >= 100) {
    return originPrice - 50;
  }
  return originPrice;
}

一个函数只做一件事、遇到 Bug 时,就可以做到“头痛医头,脚痛医脚”,而不必在庞大的逻辑海洋里费力去定位到底是哪块不对。

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

策略模式·延展: 替代多个ifelse的方案

1. 早早的 return 代替 if else

看看下面的代码。嵌套式的 if 判断的代码是很丑陋的,很难控制,很难定位 bug。如果你嵌套得太多层,层次太深,而且如果你的电脑屏幕太小,都很难展示完整的语句。你必须用鼠标滚动屏幕才能显示出来。

const isBabyPet = (pet, age) => {
  if (pet) {
    if (isPet(pet)) {
      console.log(‘It is a pet!’);
      
      if (age < 1) {
        console.log(‘It is a baby pet!’);
      }
    } else {
      throw new Error(‘Not a pet!’);
    }
  } else {
    throw new Error(‘Error!’);
  }
};

如果解决上面这个问题呢?就是要早早地 return。如果遇到错误,或者无效的情况,我们早早地 return 或者抛出错误,就会少一些判断,且看下面的代码:

const isBabyPet = (pet, age) => {
  if (!pet) throw new Error(‘Error!’);
  if (!isPet(pet)) throw new Error(‘Not a pet!’);
  
  console.log(‘It is a pet!’);
  if (age < 1) {
    console.log(‘It is a baby pet!’);
  }
};

2. 使用 Array.includes 假设您需要检查动物是否是宠物,如下所示:

const isPet = animal => {
  if (animal === ‘cat’ || animal === ‘dog’) {
    return true;
  }
  
  return false;
};

上面的代码中:动物如果是猫或者狗就是宠物,如果还要加上其他的呢,比如蛇,鸟。这个时候你可能会再加上类似这样的判断 || animal=== 'snake'。

其实我们可以用 Array.includes 代替它,比如:

const isPet = animal => {
  const pets = [‘cat’, ‘dog’, ‘snake’, ‘bird’];
  
  return pets.includes(animal);
};

3. 在函数中使用参数默认值 我们在定义一个函数的时候,你通常会确定你的参数是非空的(null 或 undefined),如果是为空,我们会为它设置一个默认值,我们可能会这样做:

const pets = [
  { name: ‘cat’,   nLegs: 4 },
  { name: ‘snake’, nLegs: 0 },
  { name: ‘dog’,   nLegs: 4 },
  { name: ‘bird’,  nLegs: 2 }
];
const check = (pets) => {
  for (let i = 0; i < pets.length; i++) {
    if (pets[i].nLegs != 4) {
      return false;
    }
  }
  return true;
}
check(pets); // false

我们会用到 for 去循环遍历这个数组,然后再用 if 来判断。

其实一条语句就可以简化:

let areAllFourLegs = pets.every(p => p.nLegs === 4);

6. 用索引代替 switch…case 下面的 switch 语句将返回给定普通宠物的品种。

const getBreeds = pet => {
  switch (pet) {
    case ‘dog’:
      return [‘Husky’, ‘Poodle’, ‘Shiba’];
    case ‘cat’:
      return [‘Korat’, ‘Donskoy’];
    case ‘bird’:
      return [‘Parakeets’, ‘Canaries’];
    default:
      return [];
  }
};
let dogBreeds = getBreeds(‘dog’); //[“Husky”, “Poodle”, “Shiba”]

这里写了好多 case return,看到这样的代码,我们就要想,能不能优化它。 看看下面更清洁的方法。

const breeds = {
  ‘dog’: [‘Husky’, ‘Poodle’, ‘Shiba’],
  ‘cat’: [‘Korat’, ‘Donskoy’],
  ‘bird’: [‘Parakeets’, ‘Canaries’]
};
const getBreeds = pet => {
  return breeds[pet] || [];
};
let dogBreeds = getBreeds(‘cat’); //[“Korat”, “Donskoy”]

我们先分类组合形成一个对象 breeds ,这样对象的索引就是动物的名称,而值就是动物的品种,我们在使用getBreeds的时候直接传入索引就好,就会返回出它的种类。 扩展结束·相信你会避免大量的if

状态模式

原理跟策略模式相似。故不展开

观察者模式

观察者模式,是所有 JavaScript 设计模式中使用频率最高,面试频率也最高的设计模式,所以说它十分重要——如果我是面试官,考虑到面试时间有限、设计模式这块不能多问,我可能在考查你设计模式的时候只会问观察者模式这一个模式。该模式的权重极高,我们此处会花费两个较长的章节把它掰碎嚼烂了来掌握。

观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个目标对象,当这个目标对象的状态发生变化时,会通知所有观察者对象,使它们能够自动更新。

// 定义发布者类
class Publisher {
  constructor() {
    this.observers = []
    console.log('Publisher created')
  }
  // 增加订阅者
  add(observer) {
    console.log('Publisher.add invoked')
    this.observers.push(observer)
  }
  // 移除订阅者
  remove(observer) {
    console.log('Publisher.remove invoked')
    this.observers.forEach((item, i) => {
      if (item === observer) {
        this.observers.splice(i, 1)
      }
    })
  }
  // 通知所有订阅者
  notify() {
    console.log('Publisher.notify invoked')
    this.observers.forEach((observer) => {
      observer.update(this)
    })
  }
}
// 定义订阅者类
class Observer {
    constructor() {
        console.log('Observer created')
    }

    update() {
        console.log('Observer.update invoked')
    }
}
  • 面试题: Vue双向绑定
// observe方法遍历并包装对象属性
function observe(target) {
    // 若target是一个对象,则遍历它
    if(target && typeof target === 'object') {
        Object.keys(target).forEach((key)=> {
            // defineReactive方法会给目标属性装上“监听器”
            defineReactive(target, key, target[key])
        })
    }
}

// 定义defineReactive方法
function defineReactive(target, key, val) {
    // 属性值也可能是object类型,这种情况下需要调用observe进行递归遍历
 		const dep = new Dep()
    observe(val)
    // 为当前属性安装监听器
    Object.defineProperty(target, key, {
         // 可枚举
        enumerable: true,
        // 不可配置
        configurable: false, 
        get: function () {
            return val;
        },
        // 监听器函数
        set: function (value) {
            // 通知所有订阅者
            dep.notify()
        }
    });
}

// 定义订阅者类Dep
class Dep {
    constructor() {
        // 初始化订阅队列
        this.subs = []
    }
    
    // 增加订阅者
    addSub(sub) {
        this.subs.push(sub)
    }
    
    // 通知订阅者(是不是所有的代码都似曾相识?)
    notify() {
        this.subs.forEach((sub)=>{
            sub.update()
        })
    }
}

观察者模式与发布-订阅模式的区别是什么?

所有的开发者拉了一个群,直接把需求文档丢给每一位群成员,这种发布者直接触及到订阅者的操作,叫观察者模式。但如果把需求文档上传到了公司统一的需求平台上,需求平台感知到文件的变化、自动通知了每一位订阅了该文件的开发者,这种发布者不直接触及到订阅者、而是由统一的第三方来完成实际的通信的操作,叫做发布-订阅模式

迭代器模式

迭代器模式是设计模式中少有的目的性极强的模式: 遍历

  • 任何数据结构只要具备Symbol.iterator属性,就可以被遍历
// 编写一个迭代器生成函数
function *iteratorGenerator() {
    yield '1号选手'
    yield '2号选手'
    yield '3号选手'
}

const iterator = iteratorGenerator()

iterator.next()
iterator.next()
iterator.next()

Thanks for reading

  • 若有错误,欢迎在评论区指正
  • 更好解决方案,相当欢迎指导
  • 帮到了您,点个赞再走吧~😊