JavaScript 里边的设计模式——0627笔记整理

    技术2022-07-12  63

    一、什么是设计模式

    假设有一个空房间,我们要日复一日地往里 面放一些东西。最简单的办法当然是把这些东西 直接扔进去,但是时间久了,就会发现很难从这 个房子里找到自己想要的东西,要调整某几样东 西的位置也不容易。所以在房间里做一些柜子也 许是个更好的选择,虽然柜子会增加我们的成 本,但它可以在维护阶段为我们带来好处。使用 这些柜子存放东西的规则,或许就是一种模式。——《JavaScript设计模式与开发实践》

    学习设计模式,有助于写出可复用和可维护性高的程序

    设计模式的原则是“找出 程序中变化的地方,并将变化封装起来”,它的关键是意图,而不是结构。

    设计原则:

    (1)单一职责原则(SRP) 一个对象或方法只做一件事情。如果一个方法承担了过多的职责,那么在需求的变迁过程中,需要改写这个方法的可能性就越大。 应该把对象或方法划分成较小的粒度。

    (2)最少知识原则(LKP) 一个软件实体应当尽可能少地与其他实体发生相互作用,应当尽量减少对象之间的交互。如果两个对象之间不必彼此直接通信,那么这两个对象就不要发生直接的相互联系,可以转交给第三方进行处理。

    (3)开放-封闭原则(OCP) 软件实体(类、模块、函数)等应该是可以 扩展的,但是不可修改。 当需要改变一个程序的功能或者给这个程序增加新功能的时候,可以使用增加代码的方式,尽量避免改动程序的源代码,防止影响原系统的稳定。

    二、常用的设计模式

    1、工厂模式

    使用函数 直接创建并返回对象(做一个创建对象的封装),避免多次创建对象。 简单工厂:

    function Factory(name,sex,age,job) { var obj = {}; obj.name = name; obj.sex = sex; obj.age = age; obj.job = job; obj.eat = function () { return this.name + "吃饭"; }; return obj; } //通过调用函数来创建对象实例 console.log(Factory("张三", "男", "24", "老师")); console.log(Factory("李四", "女", "13", "学生"));

    工厂方法:

    //使用工厂模式写用户权限检测 //在这里根据用户类型检测 返回原型对象里边的方法的执行 function Factory1(role) { if (this instanceof Factory1) { console.log(this); //当前类的实例对象 return this[role](); //这里的role是个变量,所以不能写成点的形式 } else { console.log(this); //window return new Factory1(role); } } //在原型对象里边写 规则对应的权限方法(创建并返回对象) Factory1.prototype = { admin: function () { return { name: "管理员", list: ["增加", "删除", "修改", "查找"] }; }, common: function () { return { name: "普通用户", list: ["查找"] }; } }; var quan = new Factory1("admin"); console.log(quan); var quanxian = Factory1("common"); console.log(quanxian); console.log(quanxian instanceof Factory1); //false

    2、构造函数模式

    就是把程序抽象成方法。

    举例:抽象数据分页

    //获取总页数和当前页数据 抽象成方法 function Page() { this.nowPage = 1; //当前页 this.totalPage = 0; //总页数 this.pageNum = 20; //每页显示条数 this.total = 0; //总数据条数 this.data = null; //当前页的数据 //获取总页数的方法 this.getTotalPage = function () { //使用ajax去服务器获取数据总条数 this.total = 1000; this.totalPage = Math.ceil(this.total / this.pageNum); }; //获取当前页数据的方法 this.getNowData = function () { //使用ajax去服务器获取当前页的20条数据 //http://www.maodou.com?now=1&num=20 this.data = []; //拿到数据后,进行页面渲染 }; this.init = function () { this.getTotalPage(); this.getNowData(); } } var page = new Page(); page.init();

    3、原型模式

    原型模式的缺点: 原型对象上的属性和方法是共享的。 每次创建新对象时,都要给该实例对象添加所需要的不同属性值,因为不能给原型对象上的属性传参,也不能修改原型对象上的属性(会影响到其它实例对象)。

    function Person() { } Person.prototype = { constructor: Person, name: "默认值", sex: null, sleep: function (m) { return this.name + "睡觉" + m } }; //实例化一个对象 var p1=new Person(); console.log(p1.name); //默认值 自身没有该属性,从原型对象上获取 p1.name="张三"; //是给自身设置属性 不会修改原型对象上的属性 console.log(p1.name); //张三 自身有该属性,从自身获取 console.log(p1.sleep("打呼噜")); //张三睡觉打呼噜 var p2=new Person(); console.log(p2.sleep("不踏实")); //默认值睡觉不踏实

    注意:

    1.空对象获取属性时,自身没有该属性,默认从原型对象上获取。 2.设置空对象属性时,是给自己设置属性,不会修改原型对象上的属性。 3.如果要修改原型对象上的属性,请使用 Person.prototype.name="张三"; 但这会影响到其它实例对象,因为原型对象上的属性和方法是共享的。

    4、混合模式

    即:构造函数模式+原型模式 将公用属性和方法放在原型对象上共享,私有属性和方法放在构造函数上,可以传参。 解决了原型模式的不足。

    //将私有属性和方法放在构造函数上,可以传参 function Animal(n,c){ this.name=n; this.color=c; } //将公用属性和方法放在原型对象上共享 Animal.prototype={ eat:function(m){ return this.name+"吃"+m; } }; //实例化一个对象 传参 var cat=new Animal("小花","花色"); console.log(cat.eat("小鱼干")); //小花吃小鱼干 var dog=new Animal("小黑","黑色"); console.log(dog.eat("骨头")); //小黑吃骨头

    5、单例模式

    什么是单例模式? 保证一个类仅有一个实例,并提供一个访问它的全局访问点。(使用闭包)

    举例:

    //简单的单例 function Person(name) { this.name = name; } Person.prototype.getName = function () { return this.name; }; //下边是一个单例方法 //自执行函数执行 返回闭包里边的对象 var getObject = (function () { var instance = null; return function (name) { if (!instance) { instance = new Person(name); } return instance; }; })(); //调用单例模式 因为是闭包,所以全部输出张三 console.log(getObject("张三").getName()); //张三 console.log(getObject("李四").getName()); //张三 console.log(getObject("王五").getName()); //张三 //为了设置多个类对象 让多个类共用一个方法 //将上边的单例方法 抽象为一个共享方法 var getInstance = function (callback) { //callback 为回调函数 因为要给callback传实参 所以这里不能用自执行 var instance = null; return function () { if (!instance) { console.log(this); //window instance = callback.apply(this, arguments); //这里的this也可以改为null 不替换this指针 因为都指向window } return instance; } }; //调用共享方法 传参 console.log(getInstance(function () { console.log(this); //window var per = new Person(arguments[0]); return per; })("李四").getName()); //李四 console.log(getInstance(function () { var per = new Person(arguments[0]); return per; })("王五").getName()); //王五 //这里输出王五 是因为再次调用了外部函数,重置了闭包变量 //单例模式 一直输出“小米” var getPer=getInstance(function(){ var per=new Person(arguments[0]); //一个类提供一个实例,并且暴漏出一个全局访问点 getPer就是全局访问点 return per; }); //调用单例模式 console.log(getPer("小米").getName()); //小米 console.log(getPer().getName()); //小米 //重新设置一个类 使用上边的共享方法 function Animal(name, age) { this.name = name; this.age = age; } Animal.prototype.getName = function () { return this.name; }; //使用共享方法 var getCat=getInstance(function () { var name = arguments[0]; var age = arguments[1]; var cat = new Animal(name, age); return cat; }); //调用单例模式 console.log(getCat("小花", 2).getName()); //小花

    我认为:如果需要重复使用同一实例,就可以使用单例模式(一个类提供一个实例,并且暴漏出一个全局访问点),避免重复实例化对象。

    6、策略模式

    什么是策略模式?

    定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。

    策略模式可以用于组合一系列算法,也可用于组合一系列业务规则。

    策略模式的核心:

    将算法的使用和算法的实现分离开来。

    一个基于策略模式的程序至少由两部分组成:

    第一个部分是一组策略类,策略类封装了具体的算法,并负责具体的计算过程。

    第二个部分是环境类Context,Context接受客户的请求,随后把请求委托给某一个策略类。要做到这点,说明Context 中要维持对某个策略对象的引用。

    优点:

    可以有效地避免多重条件语句,将一系列方法封装起来也更直观,利于维护

    缺点:

    往往策略集会比较多,我们需要事先就了解定义好所有的情况。

    举例一: 假设需要通过成绩等级来计算学生的最终得分,每个成绩等级有对应的加权值。我们可以利用对象字面量的形式直接定义这个组策略。 代码如下:

    //策略 var levalMap = { S: 8, A: 6, B: 4, C: 2, D: 0 }; //策略的使用 var scoreLevel = { basicScore: 80, S: function () { return this.basicScore + levalMap["S"]; }, A: function () { return this.basicScore + levalMap["A"]; }, B: function () { return this.basicScore + levalMap["B"]; }, C: function () { return this.basicScore + levalMap["C"]; }, D: function () { return this.basicScore + levalMap["D"]; } }; //调用 计算得分情况 function getScore(score, level) { scoreLevel.basicScore = score; return scoreLevel[level](); } console.log(getScore(89, "A")); //95 console.log(getScore(70, "S")); //78 console.log(getScore(90, "B")); //94 console.log(getScore(93, "C")); //95 console.log(getScore(69, "D")); //69

    举例二: 在组合业务规则方面,比较经典的是表单的验证方法。 代码如下:

    <script> //错误提示信息 var errMsg = { minLength: "输入长度不够!", maxLength: "输入长度过长!", isNumber: "请输入数字!", isEmpty: "不能为空!", default: "输入的格式不正确!" }; //策略集 var rules = { minLength: function (value, length, errorinfo) { if (value.length < length) { return errorinfo || errMsg["minLength"]; } }, maxLength: function (value, length, errorinfo) { if (value.length > length) { return errorinfo || errMsg["maxLength"]; } }, isNumber: function (value, errorinfo) { if (!/^\d+$/.test(value)) { return errorinfo || errMsg["isNumber"]; } }, isEmpty: function (value, errorinfo) { if (value == "") { return errorinfo || errMsg["isEmpty"]; } }, default: function (value, errorinfo) { if (!/^\w+$/.test(value)) { return errorinfo || errMsg["default"]; } } }; //验证类 function Validator() { this.item = []; //存储验证的函数 } //在当前类的原型对象里边 添加校验规则 Validator.prototype = { constructo: Validator, //添加校验信息的方法 add: function (value, rule, error) { var arg = [value]; //要给验证函数传递的参数集合 如[value,10,error] if (rule.indexOf(":") != -1) { var arr = rule.split(":"); //将rule转为数组 如maxLength:10 转为 ["maxLength",10] arg.push(arr[1]); rule = arr[0]; } arg.push(error); //存储验证的函数 this.item.push(function () { return rules[rule].apply(this, arg); }) }, //开始校验 start: function () { for (var i = 0; i < this.item.length; i++) { var error = this.item[i](); if (error) { console.log(error); } } } }; //实例化校验对象 var validator = new Validator(); //添加校验信息 validator.add("1234", "minLength:6", "长度不小于6位!"); validator.add("123456789", "maxLength:8", "长度不超过8位!"); validator.add("abcdef", "isNumber", "输入的值必须为数字!"); validator.add("", "isEmpty", "输入的值不能为空!"); validator.add("1234##", "default", "请输入数字、字母、下划线,不得包含特殊字符!"); //开始校验 validator.start();

    7、发布——订阅模式(观察者模式)

    什么是发布订阅模式? 发布——订阅模式也叫观察者模式,定义了对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。

    核心: 取代对象之间硬编码的通知机制,一个对象不用再显式地调用另外一个对象的某个接口。在JS中通常使用注册回调函数的形式来订阅。

    优点: 一为时间上的解耦,二为对象之间的解耦。可以用在异步编程中与MV*框架中。

    缺点: 创建订阅者本身要消耗一定的时间和内存,订阅的处理函数不一定会被执行,驻留内存有性能开销。弱化了对象之间的联系,复杂的情况下可能会导致程序难以跟踪维护和理解。

    观察者模式和发布-订阅模式的区别: 观察者模式: 观察者(Observer)直接订阅(Subscribe)主题(Subject),而当主题被激活的时候,会触发(Fire Event)观察者里的事件。 发布订阅模式: 订阅者(Subscriber)把自己想订阅的事件注册(Subscribe)到调度中心(Topic),当发布者(Publisher)发布该事件(Publish topic)到调度中心,也就是该事件触发时,由调度中心统一调度(Fire Event)订阅者注册到调度中心的处理代码。

    观察者模式和发布订阅模式最大的区别就是发布订阅模式有个事件调度中心。

    观察者模式由具体目标调度,每个被订阅的目标里面都需要有对观察者的处理,这种处理方式比较直接粗暴,但是会造成代码的冗余。

    而发布订阅模式中统一由调度中心进行处理,订阅者和发布者互不干扰,消除了发布者和订阅者之间的依赖。这样一方面实现了解耦,还有就是可以实现更细粒度的一些控制。比如发布者发布了很多消息,但是不想所有的订阅者都接收到,就可以在调度中心做一些处理,类似于权限控制之类的。还可以做一些节流操作。

    ———————————————— 版权声明:本段落转自博主「hf_872914334」的原创文章。 原文链接:https://blog.csdn.net/hf872914334/article/details/88899326

    举例:猎人发布与订阅任务

    下边是观察者模式: 比如有一家猎人工会,其中每个猎人都具有发布任务(publish)和订阅任务(subscribe)的功能,并且他们都有一个订阅列表来记录谁谁订阅了自己。 猎人们(观察者)关联他们感兴趣的猎人(目标对象),如hunter_4,当hunter_4有困难时,会自动通知给他们(观察者)。

    //定义一个猎人类 function Hunter(name, level) { this.name = name; this.level = level; this.list = []; //订阅集合 } //猎人订阅任务 Hunter.prototype.subscribe = function (target, fn) { console.log(this.level + "猎人" + this.name + "订阅了" + target.name); target.list.push(fn); }; //猎人发布任务 订阅了他的猎人收到消息, Hunter.prototype.publish = function (money) { console.log(this.level + "猎人" + this.name + "赏金" + money + "寻求帮助"); this.list.forEach(function (current) { current.call(this, money); }) }; //实例化四个猎人 var hunter_1 = new Hunter("小红", "王者"); var hunter_2 = new Hunter("小黑", "钻石"); var hunter_3 = new Hunter("小绿", "黄金"); var hunter_4 = new Hunter("小白", "白银"); //猎人1 2 3 订阅 猎人4 hunter_1.subscribe(hunter_4, function (money) { console.log("小红表示" + (money > 300 ? "" : "暂时很忙,不能") + "给予帮助"); }); hunter_2.subscribe(hunter_4, function (money) { console.log("小表黑示" + (money > 200 ? "" : "暂时很忙,不能") + "给予帮助"); }); hunter_3.subscribe(hunter_4, function (money) { console.log("小绿表示" + (money > 100 ? "" : "暂时很忙,不能") + "给予帮助"); }); //猎人发布任务 hunter_4.publish(200);

    下边是发布—订阅模式: 猎人们发布(发布者)或订阅(观察者/订阅者)任务都是通过猎人工会(调度中心)关联起来的,他们没有直接的交流。

    //定义一家猎人工会 //主要功能包括任务发布大厅(topics),以及订阅任务(subscribe),发布任务(publish) var HunterUnion = { type: "hunt", topics: Object.create(null), //任务集合 subscribe: function (topic, fn) { if (!this.topics[topic]) { this.topics[topic] = []; } typeof fn == "function" && this.topics[topic].push(fn); }, publish: function (topic, money) { if (!this.topics[topic]) { return; } this.topics[topic].forEach(function (current) { current.call(this, money); }) } }; //定义一个猎人类 function Hunter(name, level) { this.name = name; this.level = level; } //猎人可在猎人工会订阅、发布任务 Hunter.prototype.subscribe = function (topic, fn) { console.log(this.level + "猎人" + this.name + "订阅了狩猎" + topic + "的任务"); HunterUnion.subscribe(topic, fn); }; Hunter.prototype.publish = function (topic, money) { console.log(this.level + "猎人" + this.name + "发布了狩猎" + topic + "赏金" + money + "的任务"); HunterUnion.publish(topic, money) }; //实例化几个猎人 var hunterMing = new Hunter("小明", "王者"); var hunterJin = new Hunter("小金", "钻石"); var hunterZhang = new Hunter("小张", "黄金"); var hunterPeter = new Hunter("Peter", "青铜"); //小明,小金,小张分别订阅了狩猎tiger的任务 hunterMing.subscribe("tiger", function (money) { console.log("小明表示" + (money > 300 ? "" : "不") + "接受任务"); }); hunterJin.subscribe("tiger", function (money) { console.log("小金表示" + (money > 200 ? "" : "不") + "接受任务"); }); hunterZhang.subscribe("tiger", function (money) { console.log("小张表示" + (money > 100 ? "" : "不") + "接受任务"); }); hunterPeter.subscribe("sheep", function (money) { console.log("Peter表示:接受任务") }); //发布任务 hunterPeter.publish("tiger",200); hunterJin.publish("sheep",50);

    关于观察者模式的其它应用: 下例通过往 publish 中传入要执行的函数名称,来达到发布消息时,只执行该订阅者所有订阅中的某一个。

    <script> /* * 观察者 * */ var observer = { subscribes: [], //订阅集合 //订阅 subscribe: function (types, fn) { //检测types有没有订阅 if (!this.subscribes[types]) { this.subscribes[types] = []; } /*if (typeof fn == "function") { this.subscribes[types].push(fn); }*/ //下边这句和上边的条件语句一个意思 typeof fn == "function" && this.subscribes[types].push(fn); }, //发布 publish: function () { var types = [].shift.call(arguments); //改变shift方法的this指向为arguments 删除并返回第一个参数 即订阅者; var fns = this.subscribes[types]; var fname = [].shift.call(arguments).name; //获取订阅函数名称 //检测当前订阅是否存在 if (!fns || fns.length == 0) { return; } //如果存在 开始执行 var arg = arguments; //shift两次之后剩下要给订阅函数传的参数 fns.forEach(function (current) { console.log(arguments); //currentValue,index,array //forEach方法里边的arguments有三个,分别为currentValue,index,array //所以这里要使用一个变量arg接收外边的arguments current.name == fname && current.apply(this, arg); //只执行指定的函数 达到只通知指定的订阅者的目的 }) }, //删除订阅 remove: function (types, fn) { //检测types不存在(没传值),删除全部订阅 if (!types) { this.subscribes = []; return; } //获取当前types的订阅集合 var fns = this.subscribes[types]; if (!fns || fns.length == 0) { return; } //检测fn if (typeof fn != "function") { return; } //删除订阅 fns.forEach(function (current, index) { if (current == fn) { fns.splice(index, 1) } }) } }; //模拟maodou订飞机票 火车票 function getTicket(res) { console.log("订购:", res); } function bookHotel(res) { console.log("订:", res, "酒店"); } observer.subscribe("maodou", getTicket); observer.subscribe("maodou", bookHotel); observer.subscribe("maodou", function () { console.log("先去吃饭!"); }); //执行订阅 observer.publish("maodou", getTicket, ["飞机票", "火车票"]); //订购: ["飞机票", "火车票"] observer.publish("maodou", bookHotel, "速8"); //订: 速8 酒店 //删除订阅 observer.remove("maodou", bookHotel); console.log(observer.subscribes); </script>
    Processed: 0.013, SQL: 9