10分钟掌握JavaScript设计模式

您所在的位置:网站首页 国内红色旅游景点收费么 10分钟掌握JavaScript设计模式

10分钟掌握JavaScript设计模式

2023-07-22 17:32| 来源: 网络整理| 查看: 265

在软件工程中,设计模式(design pattern)是对软件设计中普遍存在(反复出现)的各种问题,所提出的解决方案。 ——维基百科

最近学习了淘系前端大神修言大佬的《JavaScript 设计模式核⼼原理与应⽤实践》,让我受益匪浅,一直沉浸在业务上重复造轮子的调包虾,每天感觉工作满满,但是未曾感觉自己在进步,此文参照该文章进行了二次消化并记录下来,帮助自己和有同样情况的同学不断加深对设计模式的理解,并运用到日常工作中

创建型 - 构造器模式

在JavaScript中,我们使用构造函数去初始化对象,就是应用了构造器模式。

// 假如你是Riot公司的一名程序员,领导让你使用js重新开发lol这款游戏 // 首先创建一名英雄到系统内 const Aixi = { name: '艾希', title: '寒冰射手', role: '射手' } // 创建第二名英雄 const Zhaoxin = { name: '赵信', title: '德邦总管', role: '战士' }; // 想想还有100多名英雄没创建呢,要加班到深夜呀,于是写个构造函数 function Hero(name, title, role) { this.name = name; this.title = title; this.role = role; }; // 创建英雄就new一下就搞定了 const Jiakesi = new Hero('贾克斯', '武器大师', '战士'); const Yidashi = new Hero('易大师', '无极剑圣', '刺客'); 复制代码

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

如果在使用构造器模式的时候,我们本质上是去抽象了每个对象实例的变与不变。那么使用工厂模式时,我们要做的就是去抽象不同构造函数(类)之间的变与不变

创建型 - 简单工厂模式

产品经理提到不同的英雄类型有不同的特性,比如射手的普攻距离是 500 码,战士是 125 码

// 为了解决上述问题,把Hero拆成两个类别Shorter和Longer function Longer(name, title, role) { this.name = name; this.title = title; this.role = '射手'; this.attackDistance = 500; } function Shorter(name, title, role) { this.name = name; this.title = title; this.role = '战士'; this.attackDistance = 125; } // 通过一个函数判断英雄角色来使用不同构造器 function handleRole(name, title, role) { switch (role) { case: '射手': return new Longer(name, title, role); break; case: '战士': return new Shorter(name, title, role); break; } } // 我们可以这样创建了 const Jiakesi = new handleRole('贾克斯', '武器大师', '战士'); const Aixi = new handleRole('艾希', '寒冰射手', '射手'); // 为了将共性封装地更彻底,也为了将共性与个性分离地更彻底,我可以再把他们整合回Hero function Hero(name, title, role, attackDistance) { this.name = name; this.title = title; this.role = role; this.attackDistance = attackDistance; }; // 然后写一个生成英雄的函数,因为英雄们就像是从流水线上生产的产品,所以给这个函数取名为“英雄工厂” function HeroFactory(name, title, role) { let attackDistance; switch (role) { case "射手": attackDistance = 500; break; case "刺客": attackDistance = 125; break; } return new Hero(name, title, role, speciality); } const Jiakesi = new HeroFactory('贾克斯', '武器大师', '战士'); const Aixi = new HeroFactory('艾希', '寒冰射手', '射手'); 复制代码 创建型 - 抽象工厂模式

抽象工厂模式的定义,是围绕一个超级工厂创建其他工厂,抽象工厂是佐证“开放封闭原则”的良好素材

// 接下来就是开一把游戏了,建立一种游戏模式需要游戏类型、地图、游戏机制 function Mode(type, map, mechanism) { this.type = type; this.map = map; this.mechanism = mechanism; } // 模仿英雄工厂创建有个游戏工厂 function GameFactory(type) { let map; let mechanism; switch (type) { case '普通匹配': map = '普通模式'; mechanism = '普通机制'; break; case '克隆模式': map = '克隆大作战'; mechanism = '克隆机制'; break; default: map = ''; mechanism = ''; brreak; } return new Mode(type, map, mechanism); } // 创建一个克隆模式 const cloneMode = GameFactory('克隆模式'); // 一段时间后,产品经理觉得克隆模式节奏太慢,决定在游戏机制中把技能冷却时间缩短40%; // 这时你要去GameFactory函数内修改克隆模式里的mechanism逻辑; // 没过多久,需要在克隆模式地图中加入一条哥斯拉增加游戏难度,这时还要修改对应的map逻辑; // 几次改写导致GameFactory愈发混乱、臃肿,难以维护 // 这时候,可以把地图map和游戏机制mechanism抽象出来 function xiaguMap() { // 召唤师峡谷地图逻辑 } function cloneMap() { // 克隆大作战地图逻辑 } // 地图工厂 function MapFactory(type) { switch (type) { case '召唤师峡谷': return new xiaguMap(); case '克隆大作战': return new cloneMap() } } function currentMechanism() { // 普通模式游戏机制逻辑 } function cloneMechanism() { // 克隆游戏机制逻辑 } // 游戏机制工厂 function MechanismFactory(type) { switch (type) { case '普通模式': return new currentMechanism(); case '克隆机制': return new cloneMechanism() } } function GameFactory(type) { let map; let mechanism; switch (type) { case '普通匹配': map = MapFactory('召唤师峡谷'); mechanism = MechanismFactory('普通模式'); break; case '克隆模式': map = MapFactory('克隆大作战'); mechanism = MechanismFactory('克隆机制'); break; default: map = ''; mechanism = ''; brreak; } return new Mode(type, map, mechanism); } // 好景不长,产品经理觉得新年要新出一个临时模式,叫飞升模式,地图用新年雪人地图,游戏机制全改动; // 这时我们不得不修改GameFactory中的逻辑,新增type属性,再新写一套逻辑进去,而且游新年结束后,还要改回去; // 这种对“已建工厂”的修改违反了开放封闭原则(类、模块、函数可以扩展,但是不可修改的原则); // 如何避免直接修改工厂内部的逻辑呢? // 郁闷的你中午吃完饭出去跟同事侃大山时从Java那哥们讲的的抽象类中获取了灵感,决定写“抽象工厂”和”抽象产品 class GameFactory { createMap() { throw new Error("抽象工厂方法不允许直接调用,你需要将我覆写!"); } createMechanism() { throw new Error("抽象工厂方法不允许直接调用,你需要将我覆写!"); } } class Map { init() { throw new Error("抽象工厂方法不允许直接调用,你需要将我覆写!"); } } class Mechanism { init() { throw new Error("抽象工厂方法不允许直接调用,你需要将我覆写!"); } } // 新建一个限定活动飞升模式 class FlyMap extends Map { init() { // 飞升模式逻辑 } } // 飞升模式机制 class FlyMechansims extends Mechansims { init() { // 飞升模式机制 } } // 飞升模式 class FlyModeFactory extends GameFactory { createMap() { return new FlyMap(); } createMechansims() { return new FlyMechansims(); } } // 新增一个飞升模式 const FlyMode = new FlyModeFactory(); // 总结: // GameFactory,抽象工厂:声明最终目标产品的共性、规范 // FlyModeFactory,具体工厂:继承自抽象工厂,实现了抽象工厂里声明的那些方法,用于实例化最终目标产品 // Map,Mechanism,抽象产品:具体工厂里实现的方法,会依赖一些产品,抽象产品声明这些产品的共性、规范 // FlyMap,FlyMechansims,具体产品:见抽象产品 复制代码 创建型 - 单例模式

单例模式想要做到的是,不管我们尝试去创建多少次,它都只给你返回第一次所创建的那唯一的一个实例。

// 游戏中玩家在看tab面板能看到每个人的出装,使用`es6 class`创建单例模式 class GameInfo {}; const player1 = new GameInfo(); const player2 = new GameInfo(); // 玩家之间不能共用信息 console.log(player1 === player2) // false // 通过GameInfo.instance获取同一对象 class GameInfo { static getInstance() { if(!GameInfo.instance) { GameInfo.instance = new GameInfo(); } return GameInfo.instance; } }; const player3 = GameInfo.getInstance(); const player4 = GameInfo.getInstance(); // 可以共享信息 console.log(player3 === player4) // true // vuex用到了单例模式 // 闭包实现 SingleDog.getInstance = (function() { // 定义自由变量instance,模拟私有变量 let instance = null return function() { // 判断自由变量是否为null if(!instance) { // 如果为null则new出唯一实例 instance = new SingleDog() } return instance } })() 复制代码

我的理解单例模式就是共享一块内存,new 一个实例不会再开辟新内存。

面试题:实现一个全局唯一弹窗

// 闭包 let Modal = (function() { let modal = null; return function() { if (!modal) { modal = document.createElement("div"); modal.innerHTML = "this is modal!"; modal.style.display = "none"; modal.id = "modal"; document.body.appendChild(modal); } return modal; }; })(); document.getElementById("open").addEventListener("click", () => { let modal = new Modal(); modal.style.display = "block"; }); document.getElementById("close").addEventListener("click", () => { let modal = new Modal(); modal.style.display = "none"; }); // class类 class Modal { constructor() { this.dom = null; if (!Modal.instance) { this.dom = document.createElement("div"); this.dom.innerHTML = "this is modal"; this.dom.id = "modal"; this.dom.style.display = "none"; this.dom.show = this.show.bind(this); this.dom.hide = this.hide.bind(this); document.body.appendChild(this.dom); Modal.instance = this.dom; } return Modal.instance; } show() { this.dom.style.display = "block"; } hide() { this.dom.style.display = "none"; } } const btnShow = document.getElementById("open"); const btnHide = document.getElementById("close"); btnShow.addEventListener("click", () => { let modal = new Modal(); modal.show(); }); btnHide.addEventListener("click", () => { let modal = new Modal(); modal.hide(); }); 复制代码 创建型 - 原型模式

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

原型编程范式的核心思想就是利用实例来描述对象,用实例作为定义对象和继承的基础。在 JavaScript 中,原型编程范式的体现就是基于原型链的继承。这其中,对原型、原型链的理解是关键。

ECMAScript 2015 中引入的 JavaScript 类实质上是 JavaScript 现有的基于原型的继承的语法糖。类语法不会为 JavaScript 引入新的面向对象的继承模型。 ——MDN

在 JavaScript 中,每个构造函数都拥有一个 prototype 属性,它指向构造函数的原型对象,这个原型对象中有一个 construtor 属性指回构造函数;每个实例都有一个proto属性,当我们使用构造函数去创建实例时,实例的proto属性就会指向构造函数的原型对象。

// 创建一个Dog构造函数 function Dog(name, age) { this.name = name this.age = age } Dog.prototype.eat = function() { console.log('肉骨头真好吃') } // 使用Dog构造函数创建dog实例 const dog = new Dog('旺财', 3) 复制代码

实现一个深拷贝

// 方法一,缺点是没法拷贝函数和正则 function deepClone(obj) { let str = JSON.stringify(obj); return JSON.parse(str); } // 方法二,使用递归,原理层面实现深拷贝 function deepClone(obj) { // 如果是值类型或 null,则直接return if(typeof obj !== 'object' || obj === null) { return obj; } // 定义结果对象 let copy = {}; // 如果对象是数组,则定义结果数组 if(obj.constructor === Array) { copy = []; } // 遍历对象的key for(let key in obj) { // 如果key是对象的自有属性 if(obj.hasOwnProperty(key)) { // 递归调用深拷贝方法 copy[key] = deepClone(obj[key]); } } return copy; } 复制代码 结构型 - 装饰器模式

装饰器模式,又名装饰者模式。它的定义是“在不改变原对象的基础上,通过对其进行包装拓展,使原有对象可以满足用户的更复杂需求”。

在 ES7 中,通过一个@语法糖轻松地给一个类装上装饰器

function classDecorator(target) { target.hasDecorator = true return target } // 将装饰器“安装”到Button类上 @classDecorator class Button { // Button类的相关逻辑 } 复制代码

React 中的装饰器:HOC

高阶组件就是一个函数,且该函数接受一个组件作为参数,并返回一个新的组件。

import React, { Component } from 'react' const BorderHoc = WrappedComponent => class extends Component { render() { return } } export default borderHoc 复制代码

装饰目标组件

import React, { Component } from 'react' import BorderHoc from './BorderHoc' // 用BorderHoc装饰目标组件 @BorderHoc class TargetComponent extends React.Component { render() { // 目标组件具体的业务逻辑 } } // export出去的其实是一个被包裹后的组件 export default TargetComponent 复制代码 结构型 - 适配器模式

适配器模式通过把一个类的接口变换成客户端所期待的另一种接口,可以帮我们解决不兼容的问题。不影响现有实现方式,兼容调用旧接口的代码

// 你写了个新的攻击特效 function newSpecial(params) { // 业务逻辑 } // 旧的攻击特效 function oldSpecial(params) { // 业务逻辑 } // 产品经理让你升级一下,替换掉旧的攻击特效,于是写了个适配器 function Apatar(params) { newSpecial(params); } function oldSpecial(params) { Apatar(params); } // 适配器类似于转换器, 类似一种可以给安卓和苹果系统充电的手机转换接头。 复制代码 结构型 - 代理模式

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

最常见的四种代理类型:事件代理、虚拟代理、缓存代理和保护代理

事件代理

其他三种模式参照修言大佬的掘金小册中有详细介绍。时间代理可能是代理模式最常见的一种应用方式,它的场景是一个父元素下有多个子元素,像这样:

事件代理 链接1号 链接2号 链接3号 链接4号 链接5号 链接6号 复制代码

我们现在的需求是,希望鼠标点击每个 a 标签,都可以弹出“我是 xxx”这样的提示。比如点击第一个 a 标签,弹出“我是链接 1 号”这样的提示。这意味着我们至少要安装 6 个监听函数给 6 个不同的的元素(一般我们会用循环,代码如下所示),如果我们的 a 标签进一步增多,那么性能的开销会更大。

// 假如不用代理模式,我们将循环安装监听函数 const aNodes = document.getElementById('father').getElementsByTagName('a') const aLength = aNodes.length for(let i=0;i= 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 } } 复制代码

缺点: 1.违背了“单一功能”原则。一个 function 里面,它竟然处理了四坨逻辑。 2.违背了“开放封闭”原则。假如增加一个满 100 - 50 的“新人价”怎么办?他只能继续 if-else,由于改动了询价函数,需测试重新回归测试。

解决办法:使用对象映射,同时遵循单一功能和开放封闭原则

// 定义一个询价处理器对象 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) } 复制代码

假如新增一个条件 新人满 100 返 50

priceProcessor.newUser = function (originPrice) { if (originPrice >= 100) { return originPrice - 50; } return originPrice; } 复制代码 行为型 - 状态模式

状态模式和策略模式相似,但状态模式强调了类的概念

// 即把处理过程放在处理对象本身中 class CoffeeMaker { constructor() { this.state = "init"; this.stock = { coffee: 500, milk: 200, }; } changeState(state) { this.state = state; this.changeStateProcessor[state](); } changeStateProcessor = { that: this, coffee() { console.log("咖啡机现在的咖啡存储量是:", this.that.stock.coffee); console.log("我只吐黑咖啡"); }, latte() { this.coffee(); console.log("咖啡机现在的咖啡存储量是:", this.that.stock.milk); console.log("加点奶"); }, vanillaLatte() { this.latte(); console.log("再加香草糖浆"); }, mocha() { this.latte(); console.log("再加巧克力"); }, }; } // 测试一下 const coffeeMaker = new CoffeeMaker(); coffeeMaker.changeState("latte"); 复制代码 行为型 - 观察者模式

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

// 观察者订阅发布者,发布者能群通知观察者们 class Publisher { constructor() { this.observers = []; } addObserver(observer) { this.observers.push(observer); } removeObserver(observer) { this.observers.forEach((item, index) => { if (item === observer) this.observers.splice(index, 1); }); } notifyObersers() { this.observers.forEach((item) => { item.update(this); }); } } class Observer { update(publisher) { // 更新操作 } } // 在上面两个类中扩展出具体的类, 游戏中所有玩家设有提醒机制,如大龙死亡通知所有玩家 class DragonPublisher extends Publisher { constructor() { super(); this.state = null; this.observers = []; } getState() { return this.state; } setState(state) { this.state = state; this.notifyObersers(); } } class PlayerObserver extends Observer { constructor(id) { super(); this.id = id; this.state = null; } update(publisher) { this.state = publisher.getState(); this.remind(); } remind() { console.log(`接收信息 ${this.state},巴拉巴拉`); } } // 测试一下 const Dragon = new DragonPublisher(); const player1 = new PlayerObserver("#01"); const player2 = new PlayerObserver("#02"); Dragon.addObserver(player1); Dragon.addObserver(player2); Dragon.setState("大龙已被击杀"); 复制代码 行为型 - 发布——订阅模式

观察者模式:发布者直接接触订阅者;发布——订阅模式:发布者和订阅者通过中间平台间接接触, 发布者无法感知订阅者

class EventEmitter { constructor() { this.handlers = {}; } // 注册事件监听器 on(event, callback) { if (!this.handlers[event]) this.handlers[event] = []; this.handlers[event].push(callback); } // 移除某个事件回调队列里的指定回调函数 off(event, callback) { const callbacks = this.handlers[event], index = callbacks.indexOf(callback); if (index !== -1) { callbacks.splice(index, 1); } } // 触发目标事件 emit(event, ...params) { if (this.handlers[event]) { this.handlers[event].forEach((callback) => { callback(...params); }); } } // 为事件注册单次监听器 once(event, callback) { // 对回调函数进行包装,使其执行完毕自动被移除 const wrapper = (...params) => { callback(...params); this.off(event, wrapper); }; this.on(event, wrapper); } } 复制代码

参考文档:掘金小册-《JavaScript 设计模式核⼼原理与应⽤实践》-修言



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3