您的位置:首页 >聚焦 >

复盘前端工程师必知的javascript设计模式(附详细思维导图和源码)

2022-04-08 22:09:02    来源:程序员客栈
前言设计模式是一个程序员进阶高级的必备技巧,也是评判一个工程师工作经验和能力的试金石.设计模式是程序员多年工作经验的凝练和总结,能更大限度的优化代码以及对已有代码的合理重构.作为一名合格的前端工程师,学习设计模式是对自己工作经验的另一种方式的总结和反思,也是开发高质量,高可维护性,可扩展性代码的重要手段.我们所熟知的金典的几大框架,比如jquery, react, vue内部也大量应用了设计模式, 比如观察者模式, 代理模式, 单例模式等.所以作为一个架构师,设计模式是必须掌握的.在中高级前端工程师的面试的过程中,面试官也会适当考察求职者对设计模式的了解,所以笔者结合多年的工作经验和学习探索, 总结并画出了针对javascript设计模式的思维导图和实际案例,接下来就来让我们一起来探索习吧.你将收获

单例模式

构造器模式

建造者模式

代理模式

外观模式

观察者模式

策略模式

迭代器模式

正文

我们先来看看总览.设计模式到底可以给我们带来什么呢?

以上笔者主要总结了几点使用设计模式能给工程带来的好处, 如代码可解耦, 可扩展性,可靠性, 条理性, 可复用性. 接下来来看看我们javascript的第一个设计模式.

1. 单例模式1.1 概念解读单例模式: 保证一个类只有一个实例, 一般先判断实例是否存在,如果存在直接返回, 不存在则先创建再返回,这样就可以保证一个类只有一个实例对象.1.2 作用模块间通信保证某个类的对象的唯一性防止变量污染1.3 注意事项正确使用this闭包容易造成内存泄漏,所以要及时清除不需要的变量创建一个新对象的成本较高1.4 实际案例单例模式广泛应用于不同程序语言中, 在实际软件应用中应用比较多的比如电脑的任务管理器,回收站, 网站的计数器, 多线程的线程池的设计等.1.5 代码实现

(function(){// 养鱼游戏let fish = nullfunction catchFish() {// 如果鱼存在,则直接返回if(fish) {return fish}else {// 如果鱼不存在,则获取鱼再返回fish = document.querySelector("#cat")return {fish,water: function() {let water = this.fish.getAttribute("weight")this.fish.setAttribute("weight", ++water)}}}}// 每隔3小时喂一次水setInterval(() => {catchFish().water()}, 3*60*60*1000)})()

2. 构造器模式2.1 概念解读构造器模式: 用于创建特定类型的对象,以便实现业务逻辑和功能的可复用.2.2 作用创建特定类型的对象逻辑和业务的封装2.3 注意事项注意划分好业务逻辑的边界配合单例实现初始化等工作构造函数命名规范,第一个字母大写new对象的成本,把公用方法放到原型链上2.4 实际案例构造器模式我觉得是代码的格局,也是用来考验程序员对业务代码的理解程度.它往往用于实现javascript的工具库,比如lodash等以及javascript框架.2.5 代码展示

function Tools(){if(!(this instanceof Tools)){return new Tools()}this.name = "js工具库"// 获取dom的方法this.getEl = function(elem) {return document.querySelector(elem)}// 判断是否是数组this.isArray = function(arr) {return Array.isArray(arr)}// 其他通用方法...}

3. 建造者模式

3.1 概念解读建造者模式: 将一个复杂的逻辑或者功能通过有条理的分工来一步步实现.3.2 作用分布创建一个复杂的对象或者实现一个复杂的功能解耦封装过程, 无需关注具体创建的细节3.3 注意事项需要有可靠算法和逻辑的支持按需暴露一定的接口3.4 实际案例建造者模式其实在很多领域也有应用,笔者之前也写过很多js插件,大部分都采用了建造者模式, 可以在笔者github地址徐小夕的github学习参考. 其他案例如下:jquery的ajax的封装jquery插件封装react/vue某一具体组件的设计3.5 代码展示笔者就拿之前使用建造者模式实现的一个案例:Canvas入门实战之用javascript面向对象实现一个图形验证码, 那让我们使用建造者模式实现一个非常常见的验证码插件吧!

// canvas绘制图形验证码(function(){function Gcode(el, option) {this.el = typeof el === "string" ? document.querySelector(el) : el;this.option = option;this.init();}Gcode.prototype = {constructor: Gcode,init: function() {if(this.el.getContext) {isSupportCanvas = true;var ctx = this.el.getContext("2d"),// 设置画布宽高cw = this.el.width = this.option.width || 200,ch = this.el.height = this.option.height || 40,textLen = this.option.textLen || 4,lineNum = this.option.lineNum || 4;var text = this.randomText(textLen);this.onClick(ctx, textLen, lineNum, cw, ch);this.drawLine(ctx, lineNum, cw, ch);this.drawText(ctx, text, ch);}},onClick: function(ctx, textLen, lineNum, cw, ch) {var _ = this;this.el.addEventListener("click", function(){text = _.randomText(textLen);_.drawLine(ctx, lineNum, cw, ch);_.drawText(ctx, text, ch);}, false)},// 画干扰线drawLine: function(ctx, lineNum, maxW, maxH) {ctx.clearRect(0, 0, maxW, maxH);for(var i=0; i < lineNum; i++) {var dx1 = Math.random()* maxW,dy1 = Math.random()* maxH,dx2 = Math.random()* maxW,dy2 = Math.random()* maxH;ctx.strokeStyle = "rgb(" + 255*Math.random() + "," + 255*Math.random() + "," + 255*Math.random() + ")";ctx.beginPath();ctx.moveTo(dx1, dy1);ctx.lineTo(dx2, dy2);ctx.stroke();}},// 画文字drawText: function(ctx, text, maxH) {var len = text.length;for(var i=0; i < len; i++) {var dx = 30 * Math.random() + 30* i,dy = Math.random()* 5 + maxH/2;ctx.fillStyle = "rgb(" + 255*Math.random() + "," + 255*Math.random() + "," + 255*Math.random() + ")";ctx.font = "30px Helvetica";ctx.textBaseline = "middle";ctx.fillText(text[i], dx, dy);}},// 生成指定个数的随机文字randomText: function(len) {var source = ["a", "b", "c", "d", "e","f", "g", "h", "i", "j", "k", "l", "m", "o", "p","q", "r", "s", "t", "u","v", "w", "x", "y", "z"];var result = [];var sourceLen = source.length;for(var i=0; i< len; i++) {var text = this.generateUniqueText(source, result, sourceLen);result.push(text)}return result.join("")},// 生成唯一文字generateUniqueText: function(source, hasList, limit) {var text = source[Math.floor(Math.random()*limit)];if(hasList.indexOf(text) > -1) {return this.generateUniqueText(source, hasList, limit)}else {return text}  }}new Gcode("#canvas_code", {lineNum: 6})})();// 调用new Gcode("#canvas_code", {lineNum: 6})

4. 代理模式4.1 概念解读代理模式: 一个对象通过某种代理方式来控制对另一个对象的访问.4.2 作用远程代理(一个对象对另一个对象的局部代理)虚拟代理(对于需要创建开销很大的对象如渲染网页大图时可以先用缩略图代替真图)安全代理(保护真实对象的访问权限)缓存代理(一些开销比较大的运算提供暂时的存储,下次运算时,如果传递进来的参数跟之前相同,则可以直接返回前面存储的运算结果)4.3 注意事项使用代理会增加代码的复杂度,所以应该有选择的使用代理.实际案例我们可以使用代理模式实现如下功能:通过缓存代理来优化计算性能图片占位符/骨架屏/预加载等合并请求/资源4.4 代码展示接下来我们通过实现一个计算缓存器来说说代理模式的应用.

// 缓存代理function sum(a, b){return a + b}let proxySum = (function(){let cache = {}return function(){let args = Array.prototype.join.call(arguments, ",");if(args in cache){return cache[args];}cache[args] = sum.apply(this, arguments)return cache[args]}})()

5. 外观模式5.1 概念解读外观模式(facade): 为子系统中的一组接口提供一个一致的表现,使得子系统更容易使用而不需要关注内部复杂而繁琐的细节.5.2 作用对接口和调用者进行了一定的解耦创造经典的三层结构MVC在开发阶段减少不同子系统之间的依赖和耦合,方便各个子系统的迭代和扩展为大型复杂系统提供一个清晰的接口5.3 注意事项当外观模式被开发者连续调用时会造成一定的性能损耗,这是由于每次调用都会进行可用性检测5.4 实际案例我们可以使用外观模式来设计兼容不同浏览器的事件绑定的方法以及其他需要统一实现接口的方法或者抽象类.5.5 代码展示接下来我们通过实现一个兼容不同浏览器的事件监听函数来让大家理解外观模式如何使用.

function on(type, fn){// 对于支持dom2级事件处理程序if(document.addEventListener){dom.addEventListener(type,fn,false);}else if(dom.attachEvent){// 对于IE9一下的ie浏览器dom.attachEvent("on"+type,fn);}else {dom["on"+ type] = fn;}}

6. 观察者模式

6.1 概念解读观察者模式: 定义了一种一对多的关系, 所有观察对象同时监听某一主题对象,当主题对象状态发生变化时就会通知所有观察者对象,使得他们能够自动更新自己.6.2 作用目标对象与观察者存在一种动态关联,增加了灵活性支持简单的广播通信, 自动通知所有已经订阅过的对象目标对象和观察者之间的抽象耦合关系能够单独扩展和重用6.3 注意事项观察者模式一般都要注意要先监听, 再触发(特殊情况也可以先发布,后订阅,比如QQ的离线模式)6.4 实际案例观察者模式是非常经典的设计模式,主要应用如下:系统消息通知网站日志记录内容订阅功能javascript事件机制react/vue等的观察者6.5 代码展示接下来我们我们使用原生javascript实现一个观察者模式:

class Subject {constructor() {this.subs = {}}addSub(key, fn) {const subArr = this.subs[key]if (!subArr) {this.subs[key] = []}this.subs[key].push(fn)}trigger(key, message) {const subArr = this.subs[key]if (!subArr || subArr.length === 0) {return false}for(let i = 0, len = subArr.length; i < len; i++) {const fn = subArr[i]fn(message)}}unSub(key, fn) {const subArr = this.subs[key]if (!subArr) {return false}if (!fn) {this.subs[key] = []} else {for (let i = 0, len = subArr.length; i < len; i++) {const _fn = subArr[i]if (_fn === fn) {subArr.splice(i, 1)}}}}}// 测试// 订阅let subA = new Subject()let A = (message) => {console.log("订阅者收到信息: " + message)}subA.addSub("A", A)// 发布subA.trigger("A","我是徐小夕")//A收到信息:-->我是徐小夕

7. 策略模式7.1 概念解读策略模式: 策略模式将不同算法进行合理的分类和单独封装,让不同算法之间可以互相替换而不会影响到算法的使用者.7.2 作用实现不同, 作用一致调用方式相同,降低了使用成本以及不同算法之间的耦合单独定义算法模型, 方便单元测试避免大量冗余的代码判断,比如if else等7.3 实际案例实现更优雅的表单验证游戏里的角色计分器棋牌类游戏的输赢算法7.4 代码展示接下来我们实现一个根据不同类型实现求和算法的模式来带大家理解策略模式.

const obj = {A: (num) => num * 4,B: (num) => num * 6,C: (num) => num * 8}const getSum =function(type, num) {return obj[type](num)}

8. 迭代器模式8.1 概念解读迭代器模式: 提供一种方法顺序访问一个聚合对象中的各个元素,使用者并不需要关心该方法的内部表示.8.2 作用为遍历不同集合提供统一接口保护原集合但又提供外部访问内部元素的方式8.3 实际案例迭代器模式模式最常见的案例就是数组的遍历方法如forEach, map, reduce.8.4 代码展示接下来笔者使用自己封装的一个遍历函数来让大家更加理解迭代器模式的使用,该方法不仅可以遍历数组和字符串,还能遍历对象.lodash里的_.forEach(collection, [iteratee=_.identity])方法也是采用策略模式的典型应用.

function _each(el, fn = (v, k, el) => {}) {// 判断数据类型function checkType(target){return Object.prototype.toString.call(target).slice(8,-1)}// 数组或者字符串if(["Array", "String"].indexOf(checkType(el)) > -1) {for(let i=0, len = el.length; i< len; i++) {fn(el[i], i, el)}}else if(checkType(el) === "Object") {for(let key in el) {fn(el[key], key, el)}}}

最后如果想了解本文完整的思维导图, 更多H5游戏, webpack,node,gulp,css3,javascript,nodeJS,canvas数据可视化等前端知识和实战,欢迎在公号《趣谈前端》加入我们一起学习讨论,共同探索前端的边界。

欢迎关注下方公众号,获取更多前端知识精粹和加入学习社群:

回复 Webpack,可获得用webpack4.0撸单页/多页脚手架思维导图回复 lodash,将获得 本人亲自翻译的lodash API中文思维导图回复学习路径,将获取 笔者多年从业经验的前端学习路径的思维导图好啦, 今天的分享就到这啦, 如果文章对你有帮助, 欢迎 「点赞」 + 「评论」, 鼓励作者创造更优质的内容~

关键词: 设计模式 观察者模式 注意事项

相关阅读