精准而优雅的设计模式

构造器

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

  • 场景: 公司员工信息录入系统、少许员工录入
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函数里,进行一个简单的调用。css

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

简单工厂模式

  • 场景: 区分员工的职业。若是是码农就写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、自觉很是不爽的状况下,咱们就应该思考是否是能够掏出工厂模式重构咱们的代码了java

单例模式

  • 只有一个实例
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__存储这个(共用属性组成的对象)的地址。而这个共用属性就是原型。原型出现的目的就是为了减小没必要要的内存消耗。ios

  • 原型链就是对象经过__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>
复制代码

适配模式

适配器模式经过把一个类的接口变换成客户端所期待的另外一种接口,能够帮咱们解决不兼容的问题。axios

  • 场景: 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

  • 如有错误,欢迎在评论区指正
  • 更好解决方案,至关欢迎指导
  • 帮到了您,点个赞再走吧~😊
相关文章
相关标签/搜索