前端常用设计模式简介

前端常用设计模式简介

一、什么是设计模式?

设计模式的原则是找出程序中的变化,并将变化封装起来,实现高效的可复用性。核心在于意图,而不在结构。通过设计模式可以帮助我们增强代码的可重用性、可扩充性、 可维护性、灵活性。我们使用设计模式的最终目的是为了实现代码的高类聚和低耦合。你是否思考过这样的一个问题,如何让代码写的更有健壮性,其实核心在于把握变与不变。确保变的部分更加灵活,不变的地方更加稳定,而使用设计模式可以让我们达到这样的目的。

二、设计原则

1.S – Single Responsibility Principle 单一职责原则
a.一个程序只做好一件事
b.如果功能过于复杂就拆分开,每个部分保持独立
2.O – OpenClosed Principle 开放/封闭原则
a.对扩展开放,对修改封闭
b.增加需求时,扩展新代码,而非修改已有代码
3.L – Liskov Substitution Principle 里氏替换原则
a.子类能覆盖父类
b.父类能出现的地方子类就能出现
4.I – Interface Segregation Principle 接口隔离原则
a.保持接口的单一独立
b.类似单一职责原则,这里更关注接口
5.D – Dependency Inversion Principle 依赖倒转原则
a.面向接口编程,依赖于抽象而不依赖于具体
b.使用方只关注接口而不关注具体类的实现

三、设计模式的类型

设计模式可以分为三大类:

1.结构型模式(Structural Patterns): 通过识别系统中组件间的简单关系来简化系统的设计。常见结构性模式如下:
a.适配器模式
b.装饰器模式
c.代理模式
d.外观模式
e.桥接模式
f.组合模式
g.享元模式
2.创建型模式(Creational Patterns): 处理对象的创建,根据实际情况使用合适的方式创建对象。常规的对象创建方式可能会导致设计上的问题,或增加设计的复杂度。创建型模式通过以某种方式控制对象的创建来解决问题。常见创建型模式如下:
a.单例模式
b.原型模式
c.工厂模式
d.抽象工厂模式
e.建造者模式
3.行为型模式(Behavioral Patterns): 用于识别对象之间常见的交互模式并加以实现,如此,增加了这些交互的灵活性。常见行为型模式如下:
a.观察者模式
b.迭代器模式
c.策略模式
d.模板方法模式
e.职责链模式
f.命令模式
g.备忘录模式
h.状态模式
i.访问者模式
j.中介者模式
k.解释器模式
下面我们选择一些在前端开发过程中常见的模式进行一一讲解。

四、前端常用设计模式

1.单例模式
定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
场景:确保只有一个实例。比如:登录浮窗、Vue中的axios实例(我们对axios进行请求拦截和响应拦截,多次调用封装好的axios但是仅设置一次,封装好的axios导出就是一个单例)、全局状态管理 store等
示例

// 单例构造器
const FooServiceSingleton = (function () {
  // 隐藏的Class的构造函数
  function FooService() {}
  // 未初始化的单例对象
  let fooService;
  return {
	// 创建/获取单例对象的函数
	getInstance: function () {
	  if (!fooService) {
		fooService = new FooService();
	  }
	  return fooService;
	}
  }
})();
const fooService1 = FooServiceSingleton.getInstance();
const fooService2 = FooServiceSingleton.getInstance();
console.log(fooService1 === fooService2); // true

优点
划分命名空间,减少全局变量
增强模块性,把自己的代码组织在一个全局变量名下,放在单一位置,便于维护且只会实例化一次。简化了代码的调试和维护
缺点
不适用动态扩展对象。
2.策略模式
定义:定义一系列的算法,把它们一个个封装起来,并且使它们可以互相替换
适用场景:根据表单验证策略进行表单验证等
示例

<!DOCTYPE html>
<html>
<head>
    <title>策略模式-校验表单</title>
    <meta content="text/html; charset=utf-8" http-equiv="Content-Type">
</head>
<body>
    <form id = "registerForm" method="post" action="http://xxxx.com/api/register">
        用户名:<input type="text" name="userName">
        密码:<input type="text" name="password">
        手机号码:<input type="text" name="phoneNumber">
        <button type="submit">提交</button>
    </form>
    <script type="text/javascript">
        // 策略对象
        const strategies = {
          isNoEmpty: function (value, errorMsg) {
            if (value === '') {
              return errorMsg;
            }
          },
          isNoSpace: function (value, errorMsg) {
            if (value.trim() === '') {
              return errorMsg;
            }
          },
          minLength: function (value, length, errorMsg) {
            if (value.trim().length < length) {
              return errorMsg;
            }
          },
          maxLength: function (value, length, errorMsg) {
            if (value.length > length) {
              return errorMsg;
            }
          },
          isMobile: function (value, errorMsg) {
            if (!/^(13[0-9]|14[5|7]|15[0|1|2|3|5|6|7|8|9]|17[7]|18[0|1|2|3|5|6|7|8|9])\d{8}$/.test(value)) {
              return errorMsg;
            }                
          }
        }
        
        // 验证类
        class Validator {
          constructor() {
            this.cache = []
          }
          add(dom, rules) {
            for(let i = 0, rule; rule = rules[i++];) {
              let strategyAry = rule.strategy.split(':')
              let errorMsg = rule.errorMsg
              this.cache.push(() => {
                let strategy = strategyAry.shift()
                strategyAry.unshift(dom.value)
                strategyAry.push(errorMsg)
                return strategies[strategy].apply(dom, strategyAry)
              })
            }
          }
          start() {
            for(let i = 0, validatorFunc; validatorFunc = this.cache[i++];) {
              let errorMsg = validatorFunc()
              if (errorMsg) {
                return errorMsg
              }
            }
          }
        }

        // 调用代码
        let registerForm = document.getElementById('registerForm')

        let validataFunc = function() {
          let validator = new Validator()
          validator.add(registerForm.userName, [{
            strategy: 'isNoEmpty',
            errorMsg: '用户名不可为空'
          }, {
            strategy: 'isNoSpace',
            errorMsg: '不允许以空白字符命名'
          }, {
            strategy: 'minLength:2',
            errorMsg: '用户名长度不能小于2位'
          }])
          validator.add(registerForm.password, [ {
            strategy: 'minLength:6',
            errorMsg: '密码长度不能小于6位'
          }])
          validator.add(registerForm.phoneNumber, [{
            strategy: 'isMobile',
            errorMsg: '请输入正确的手机号码格式'
          }])
          return validator.start()
        }

        registerForm.onsubmit = function() {
          let errorMsg = validataFunc()
          if (errorMsg) {
            alert(errorMsg)
            return false
          }
        }
    </script>
</body>
</html>

优点
利用组合、委托、多态等技术和思想,可以有效的避免多重条件选择语句
提供了对开放-封闭原则的完美支持,将算法封装在独立的strategy中,使得它们易于切换,理解,易于扩展
利用组合和委托来让Context拥有执行算法的能力,这也是继承的一种更轻便的代替方案
缺点
会在程序中增加许多策略类或者策略对象
要使用策略模式,必须了解所有的strategy,必须了解各个strategy之间的不同点,这样才能选择一个合适的strategy
3.工厂模式
定义:工厂模式是指通过一个工厂类来创建其他类的实例。这个模式非常适合那些需要根据不同条件创建不同实例的场景
场景:如jquery的$函数,该函数接收一个选择器字符串作为参数,然后返回与该选择器匹配的一个或多个元素。

(function () {
	    function jQuery(selector) {
	        return new jQuery.prototype.init(selector);
	    }
	    // jQuery对象的构造函数
	    jQuery.prototype.init = function (selector) {
	       
	    }
	    // jQuery原型上的css方法
	    jQuery.prototype.css = function (config) {
	        
	    }
	    // 将jQuery原型上的方法都放到init的原型链上
	    jQuery.prototype.init.prototype = jQuery.prototype;
	    window.$ = window.jQuery = jQuery;
	})();

示例
function Factory (name, age) {
this.name = name;
this.age = age;
}
Factory.prototype.say = function () {
console.log(我的名字叫${this.name}, 我今年${this.age}了)
}
let zs = new Factory(‘张三’, 18);
let ls = new Factory(‘李四’, 20);
zs.say()
ls.say()
优点
创建对象的过程可能很复杂,但我们只需要关心创建结果。
构造函数和创建者分离, 符合“开闭原则”
一个调用者想创建一个对象,只要知道其名称就可以了。
扩展性高,如果想增加一个产品,只要扩展一个工厂类就可以。
缺点
添加新产品时,需要编写新的具体产品类,一定程度上增加了系统的复杂度
考虑到系统的可扩展性,需要引入抽象层,在客户端代码中均使用抽象层进行定义,增加了系统的抽象性和理解难度
4.外观模式
定义:外观模式是最常见的设计模式之一,它为子系统中的一组接口提供一个统一的高层接口,使子系统更容易使用。简而言之外观设计模式就是把多个子系统中复杂逻辑进行抽象,从而提供一个更统一、更简洁、更易用的API
场景:比如JQuery就把复杂的原生DOM操作进行了抽象和封装,并消除了浏览器之间的兼容问题,从而提供了一个更高级更易用的版本。
示例:

//兼容浏览器事件绑定
let addMyEvent = function (el, ev, fn) {
    if (el.addEventListener) {
        el.addEventListener(ev, fn, false)
    } else if (el.attachEvent) {
        el.attachEvent('on' + ev, fn)
    } else {
        el['on' + ev] = fn
    }
}; 

优点
减少系统相互依赖。
提高灵活性。
提高了安全性
缺点
不符合开闭原则,如果要改东西很麻烦,继承重写都不合适。
5.代理模式
定义:代理模式是指使用一个代理对象来控制对另一个对象的访问。访问之前先做一层拦截,这个模式非常适合那些需要控制对某些敏感资源的访问的场景。
场景:如缓存接口请求,如vue3的响应式原理proxy
示例

let obj1 = {
	name:'vue',
	age:'10'
}
let obj2 = new Proxy(obj1,{
	get(target,property) {
		//访问obj2的某个属性的时候调用
		//target即obj这个对象
		//property 所访问的属性
		console.log('触发get');
		return target[property];
	},
	set(target,property,newVal) {
		//修改obj2的某个属性的时候调用
		//target即obj这个对象
		//property 所访问的属性
		//newVal 要设置的新值
		console.log('触发set')
		target[property] = newVal;
	},
})
obj2.age = '12';
console.log(obj2.age);

优点
代理模式能将代理对象与被调用对象分离,降低了系统的耦合度。代理模式在客户端和目标对象之间起到一个中介作用,这样可以起到保护目标对象的作用
代理对象可以扩展目标对象的功能;通过修改代理对象就可以了,符合开闭原则;
缺点
处理请求速度可能有差别,非直接访问存在开销
6.观察者模式
定义:观察者模式又称发布订阅模式(Publish/Subscribe Pattern),是我们经常接触到的设计模式,日常生活中的应用也比比皆是,比如你订阅了某个博主的频道,当有内容更新时会收到推送;又比如JavaScript中的事件订阅响应机制。观察者模式的思想用一句话描述就是:被观察对象(subject)维护一组观察者(observer),当被观察对象状态改变时,通过调用观察者的某个方法将这些变化通知到观察者。
场景:比如给DOM元素绑定事件的 addEventListener() 方法、还有Vue中的watch 就是依赖于这个模式,在wacth中,被监听的数据充当了发布者,而执行的回调函数充当了订阅者。当数据变化时,发布者会通知所有订阅者执行回调函数,从而实现对数据变化的监听。
示例

document.body.addEventListener("click", function() {    
alert("Hello World");
});
document.body.click();

在这里需要监控用户点击 document.body 的动作,但是我们没办法预知用户将在什么时候点击。所以我们订阅了 document.body 的 click 事件,当 body 节点被点击时,body 节点便会向订阅者发布这个消息
优点
支持简单的广播通信,自动通知所有已经订阅过的对象
目标对象与观察者之间的抽象耦合关系能单独扩展以及重用
增加了灵活性
观察者模式所做的工作就是在解耦,让耦合的双方都依赖于抽象,而不是依赖于具体。从而使得各自的变化都不会影响到另一边的变化。
缺点
过度使用会导致对象与对象之间的联系弱化,会导致程序难以跟踪维护和理解
7.适配器模式
定义:适配器模式的英文翻译是 Adapter Design Pattern。顾名思义,这个模式就是用来做适配的,它将不兼容的接口转换为可兼容的接口,让原本由于接口不兼容而不能一起工作的类可以一起工作。对于这个模式,有一个经常被拿来解释它的例子,就是 USB 转接头充当适配器,把两种不兼容的接口,通过转接变得可以一起工作。
场景:统一多个类的接口设计、兼容老版本接口、适配不同格式的数据等
示例

class GooleMap {
    show() {
        console.log('渲染地图')
    }
}

class BaiduMap {
    display() {
        console.log('渲染地图')
    }
}
class GaodeMap {
    show() {
        console.log('渲染地图')
    }
}
// 上面三个类,如果我们用多态的思想去开发的话是很难受的,所以我们通过适配器来统一接口
class BaiduAdaapterMap {
    show() {
        return new BaiduMap().display()
    }
}

优点
可以让任何两个没有关联的类一起运行
提高了类的复用
增加了类的透明度
灵活性好
缺点
过多地使用适配器,会让系统非常零乱,不易整体进行把握。比如,明明看到调用的是 A 接口,其实内部被适配成了 B 接口的实现,一个系统如果太多出现这种情况,无异于一场灾难。因此如果不是很有必要,可以不使用适配器,而是直接对系统进行重构。

8.装饰器模式
定义:装饰器模式是一种在不改变对象自身的基础上,在程序运行期间给对象动态地添加职责的设计模式。在前端开发中,装饰者模式可以用于对已有功能的扩展和增强。
场景:ES7中增加了对装饰器Decorator的支持,它就是用来修改类的行为,或者增强类,这使得在js中使用装饰者模式变得更便捷。
示例

// 原始对象
function Component() {}

Component.prototype = {
  operation: function () {
    console.log("Component operation");
  },
};

// 装饰者对象
function Decorator(component) {
  this.component = component;
}

Decorator.prototype = {
  operation: function () {
    this.component.operation();
    console.log("Decorator operation");
  },
};

// 使用
var component = new Component();
component.operation(); // 输出 "Component operation"

var decorator = new Decorator(component);
decorator.operation(); // 输出 "Component operation" 和 "Decorator operation"		

优点
装饰器是继承的有力补充,比继承灵活,在不改变原有对象的情况下,动态的给一个对象扩展功能,即插即用
通过使用不同装饰类及这些装饰类的排列组合,可以实现不同效果
装饰器模式完全遵守开闭原则
缺点
装饰器模式会增加许多子类,过度使用会增加程序得复杂性

你可能感兴趣的:(javascript,设计模式)