设计原则

  • 在设计一些设计模式时,一般遵循如下七项基本原则:
1. 单一职责原则 (Single Responsibility Principle)
2. 开放-关闭原则 (Open-Closed Principle)
3. 里氏替换原则 (Liskov Substitution Principle)
4. 依赖倒转原则 (Dependence Inversion Principle)
5. 接口隔离原则 (Interface Segregation Principle)
6. 最少知道原则(The Least Knowledge Principle)
7. 组合/聚合复用原则 (Composite/Aggregate Reuse Principle)

单一职责原则:

1. 一个对象或方法只做一件事情。
2. 如果一个方法承担了过多的职责,那么在需求的变迁过程中,需要改写这个方法的可能性就越大。
3. 应该把对象或方法划分成较小的粒度,提高代码可读性,提高系统可维护性。

开放-关闭原则:

1. 对扩展开放:有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况。
2. 对修改封闭:一旦设计完成,模块的源代码不能被侵犯,任何人不允许修改已有源代码。
3. 开放封闭的核心思想就是对抽象编程,而不对具体编程,因为抽象相对稳定。
4. 让类依赖于固定的抽象,所以对修改就是封闭的;
5. 而通过面向对象的继承和多态机制,可以实现对抽象体的继承,通过覆写其方法来改变固有行为,实现新的扩展方法,所以对于扩展就是开放的。

最少知道原则:

1. 它描述了一种保持代码松耦合的策略,每个单元对其他单元只拥有有限的知识,只了解与当前单元紧密联系的单元;
2. 现代面向对象程序设计语言通常使用 "." 作为访问标识,LoD 可以被简化为 "仅使用一个点(use only one dot)"3. 也就是说,代码 a.b.Method() 违反了 LoD,而 a.Method() 则符合 LoD。
打个比方,人可以命令一条狗行走,但是不应该直接指挥狗的腿行走,应该由狗去指挥它的腿行走。

设计模式

  • 设计模式是我们在 解决问题的时候针对特定问题给出的简洁而优化的处理方案
  • 在 JS 设计模式中,最核心的思想:封装变化。
  • 将变与不变分离,确保变化的部分灵活、不变的部分稳定。

单例模式

  • 单例模式限制实例的数量必须是一个,这个唯一的实例被称作单例(singleton)。
  • 单例模式适合应用在一个广泛的系统范围内、在一个集中的地方来处理某些逻辑。
  • 减少了全局变量的使用,从而清除了命名空间污染和命名冲突的风险。
  • 例子:数据库连接池,连接池需要管理很多连接的生命周期,以便在将来需要对数据库发出请求时可以重用连接。
const Singleton = (function () {
    let instance; // 实例

    function init() {
        return {
            publicProperty: 'I am a property',
            publicMethod: function () {
                console.log('I am a mehtod');
            },
        };
    }

    return {
        getInstance: function () {
            if (!instance) {
                instance = init();
            }
            return instance;
        }
    };
})();

// 获取实例
const instance1 = Singleton.getInstance();
const instance2 = Singleton.getInstance();

  • 什么是单例模式呢?
  • 我们都知道,构造函数可以创造一个对象
  • 我们 new 很多次构造函数就能得到很多的对象
  • 单例模式:
    • 就是使用构造函数实例化的时候,不管实例化多少回,都是同一个对象
    • 也就是一个构造函数一生只能 new 出一个对象
  • 当我们使用构造函数,每一次 new 出来的对象 属性/功能/方法 完全一样 的时候,我们把他设计成单例模式

核心代码

  • 其实就是判断一下,他曾经有没有 new 出来过对象
  • 如果有,就还继续使用之前的那个对象,如果没有,那么就给你 new 一个
// 准备一个构造函数
// 将来要 new 的
function Person() {};

// 准备一个单例模式函数
// 这个单例模式函数要把 Person 做成一个单例模式
// 将来再想要 new Person 的时候只要执行这个 singleton 函数就可以了
function singleton () {
  let instance
  
  if (!instance) { // 如果 instance 没有内容
    // 来到这里,证明 instance 没有内容
    // 给他赋值为 new Person
      instance = new Person()
  }
  
  // 返回的永远都是第一次 new Person 的实例
  // 也就是永远都是一个实例
  return instance
}

const p1 = singleton()
const p2 = singleton()
console.log(p1 === p2) // true
  • 应用
// 这个构造函数的功能就是创建一个 div,添加到页面中
function CreateDiv() {
    this.div = document.createElement('div');
    document.body.appendChild(this.div);
}

CreateDiv.prototype.init = function (text) {
    this.div.innerHTML = text;
}

// 准备把这个 CreateDiv 做成单例模式
// 让 singleton 成为一个闭包函数
const singleton = (function () {

    let instance

    return function (text) {
        if (!instance) {
            instance = new CreateDiv()
        }
        instance.init(text)
        return instance
    }
})()

singleton('hello') // 第一次的时候,页面中会出现一个新的 div ,内容是 hello
singleton('world') // 第二次的时候,不会出现新的 div,而是原先的 div 内容变成了 world

组合模式

  • 组合模式,就是把几个构造函数的启动方式组合再一起
  • 然后用一个 ”遥控器“ 进行统一调用
class GetHome {

    init () {
        console.log('到家了')
    }
}

class OpenComputer {

    init () {
        console.log('打开电脑')
    }
}

class PlayGame {

    init () {
        console.log('玩游戏')
    }
}
  • 上面几个构造函数的创造的实例化对象的 启动方式 都一致
  • 那么我们就可以把这几个函数以组合模式的情况书写
  • 然后统一启动(准备一个 组合模式 的构造函数)
class Compose {
    constructor () {
        this.compose = []
    }
    	
    // 添加任务的方法
    add (task) {
        this.compose.push(task)
    }
    
    // 一个执行任务的方法
    execute () {
        this.compose.forEach(item => {
            item.init()
        })
    }
}
  • 我们就用我们的组合模式构造函数来吧前面的几个功能组合起来
const c = new Compose()
// 把所有要完成的任务都放在队列里面
c.add(new GetHome())
c.add(new OpenComputer)
c.add(new PlayGame)

// 直接器动任务队列
c.execute()
// 就会按照顺序执行三个对象中的 init 函数

构造器模式

  • 构造器是一个当新建对象的内存被分配后,用来初始化该对象的一个特殊函数,在 JavaScript 中几乎所有的东西都是对象。
  • 同时构造器可以使用的参数,以在第一次创建对象时,设置成员属性的方法的值。
  • 对象定义:状态、属性、行为
function Friend(name, age, height) {
    this.name = name;
    this.age = age;
    this.height = height;
}

Friend.prototype.myIndentity = function () {
    console.log(`我是被创造出来的朋友,我叫${this.name}`);
};

const aFriend = new Friend('A 朋友', 28,  '178cm');
const bFriend = new Friend('B 朋友', 19,  '135cm');

适配器模式

  • 适配器模式可以将一个接口转换到另一种完全不同的接口,它允许接口不同的组件在一起工作。
  • 例子:新研发出的组件和以往接口完全不同,可以通过适配器模式来兼容已有的老代码。
// 包裹计价 - 老服务
function Shipping() {
    this.request = function (zipStart, zipEnd, weight) {
        // ...
        return "$49.75";
    }
}

// 包裹计价 - 新服务(带权限验证且逻辑解耦)
function AdvancedShipping() {
    this.login = function (credentials) { /* ... */ };
    this.setStart = function (start) { /* ... */ };
    this.setDestination = function (destination) { /* ... */ };
    this.calculate = function (weight) { return "$39.50"; };
}

// 适配器
function ShippingAdapter(credentials) {
    // 创建新服务
    const shipping = new AdvancedShipping();
    // 验证登录权限
    shipping.login(credentials);

    return {
        // 保持老服务接口,内容逻辑适配到新服务
        request: function (zipStart, zipEnd, weight) {
            shipping.setStart(zipStart);
            shipping.setDestination(zipEnd);
            return shipping.calculate(weight);
        }
    };
}
// 调用老服务
const shipping = new Shipping();
const cost = shipping.request("78701", "10010", "2 lbs");
// 调用新服务代码
const credentials = { token: "30a8-6ee1" };
const adapter = new ShippingAdapter(credentials);
const cost = adapter.request("78701", "10010", "2 lbs");

观察者模式

  • 观察者模式包含观察目标和观察者两类对象,
  • 一个目标可以有任意数目的与之相依赖的观察者
  • 一旦观察目标的状态发生改变,所有的观察者都将得到通知。
// 定义一个主体对象
class Subject {
    constructor() {
        this.observers = []; // 观察者
    }
    add(observer) { // 添加
        this.observers.push(observer)
    }
    remove(observer) { // 移除
        this.observers = this.observers.filter(item => item !== observer);
    }
    notify() { // 通知
        this.observers.forEach(item => {
            item.update();
        })
    }
}
// 定义观察着对象
class Observer {
    constructor(name) {
        this.name = name;
    }
    update() {
        console.log(`my name is:${this.name}`);
    }
}

// 调用
const observer1 = new Observer('observer1');
const observer2 = new Observer('observer2');

const subject = new Subject();
subject.add(observer1);
subject.add(observer2);
subject.notify();

  • 观察者模式(Observer),通常也被叫做 发布-订阅模式 或者 消息模式
  • 官方解释:
  • 当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新,解决了主体对象与观察者之间功能的耦合,即一个对象状态改变给其他对象通知的问题

addEventListener

  • 这个东西其实就是一个标准的 观察者模式
btn.addEventListener('click', function () {
    console.log('btn 被点击了')
})
  • 上面这个就是有一个 无形的观察者 再观察着 btn 的一举一动
  • 当这个 btn 被点击的时候,就会执行 对应的函数
  • 我们也可以多绑定几个函数
  • 说白了: 观察者模式就是我们自己实现一个 addEventListener 的功能
  • 只不过 addEventListaner 只有固定的一些事件,而且只能给 dom 元素绑定
  • 而我们自己写的可以随便绑定一个事件名称,自己选择触发时机而已

书写代码

  • 我们要有一个观察者(这里抽象为一个对象 {})
  • 需要有一个属性,存放消息的盒子(把你绑定的所有事件放在里面)
  • 需要一个 on 方法,用于添加事件
  • 需要一个 emit 方法,用于发布事件(触发)
  • 需要一个 off 方法,把已经添加的方法取消
const observer = {
    message: {},
    on: function () {},
    emit: function () {},
    off: function () {}
}
  • 把它写成一个构造函数的形式
class Observer {
    constructor () {
        this.message = {}
    }
    
    on () {}
    
    emit () {}
    
    off () {}
}
  • 现在,一个观察者的雏形就出来了
  • 接下来完善方法就可以了

ON(添加事件)

  • 我们的 on 方法需要接受 两个参数
    • 事件类型
    • 事件处理函数
class Observer {
    constructor () {
        this.message = {}
    }
    
    on (type, fn) {
        // 判断消息盒子里面有没有设置事件类型
        if (!this.message[type]) {
            // 证明消息盒子里面没有这个事件类型
            // 那么我们直接添加进去
            // 并且让他的值是一个数组,再数组里面放上事件处理函数
            this.message[type] = [fn]
        } else {
            // 证明消息盒子里面有这个事件类型
            // 那么我们直接向数组里面追加事件处理函数就行了
            this.message[type].push(fn)
        }
    }
    
    emit () {}
    
    off () {}
}

EMIT(发布事件)

  • 也就是让我们已经订阅好的事件执行一下
  • 同样需要接受两个参数
    • 要触发的事件类型
    • 给事件处理函数传递的参数
class Observer {
    constructor () {
        this.message = {}
    }
    
    on (type, fn) {
        // 判断消息盒子里面有没有设置事件类型
        if (!this.message[type]) {
            // 证明消息盒子里面没有这个事件类型
            // 那么我们直接添加进去
            // 并且让他的值是一个数组,再数组里面放上事件处理函数
            this.message[type] = [fn]
        } else {
            // 证明消息盒子里面有这个事件类型
            // 那么我们直接向数组里面追加事件处理函数就行了
            this.message[type].push(fn)
        }
    }
    
    emit (type, ...arg) {
        // 判断你之前有没有订阅过这个事件
        if (!this.message[type]) return

        // 如果有,那么我们就处理一下参数
        const event = {
            type: type,
            arg: arg || {}
        }

        // 循环执行为当前事件类型订阅的所有事件处理函数
        this.message[type].forEach(item => {
            item.call(this, event)
        })
    }
    
    off () {}
}

OFF(移除事件)

  • 就是把已经订阅的事件处理函数移除掉
  • 同样需要接受两个参数
    • 要移除的事件类型
    • 要移除的事件处理函数
class Observer {
    constructor () {
        this.message = {}
    }
    
    on (type, fn) {
        // 判断消息盒子里面有没有设置事件类型
        if (!this.message[type]) {
            // 证明消息盒子里面没有这个事件类型
            // 那么我们直接添加进去
            // 并且让他的值是一个数组,再数组里面放上事件处理函数
            this.message[type] = [fn]
        } else {
            // 证明消息盒子里面有这个事件类型
            // 那么我们直接向数组里面追加事件处理函数就行了
            this.message[type].push(fn)
        }
    }
    
    emit (type, ...arg) {
        // 判断你之前有没有订阅过这个事件
        if (!this.message[type]) return

        // 如果有,那么我们就处理一下参数
        const event = {
            type: type,
            arg: arg || {}
        }

        // 循环执行为当前事件类型订阅的所有事件处理函数
        this.message[type].forEach(item => {
            item.call(this, event)
        })
    }
    
    off (type, fn) {
        // 判断你之前有没有订阅过这个事件
        if (!this.message[type]) return

        // 如果有我们再进行移除
        for (let i = 0; i < this.message[type].length; i++) {
            const item =  this.message[type][i]
            if (item === fn) {
                this.message[type].splice(i, 1)
                i--
            }
        }
    }
}

使用

  • 以上就是最基本的 观察者模式
  • 接下来我们就使用一下试试看
const o = new Observer()

// 准备两个事件处理函数
function a(e) {
    console.log('hello')
}

function b(e) {
    console.log('world')
}

// 订阅事件
o.on('abc', a)
o.on('abc', b)

// 发布事件(触发)
o.emit('abc', '100', '200', '300') // 两个函数都回执行

// 移除事件
o.off('abc', 'b')

// 再次发布事件(触发)
o.emit('abc', '100', '200', '300') // 只执行一个 a 函数了

发布-订阅模式

  • 发布 + 订阅 = 观察者模式 ?
  • 在观察者模式中,观察者是知道Subject的,Subject一直保持对观察者进行记录。
  • 然而,在发布订阅模式中,发布者和订阅者不知道对方的存在,它们只有通过消息代理进行通信。
  • 在发布订阅模式中,组件是松散耦合的,正好和观察者模式相反。
const publisher = {
    topics: [], // 可订阅的主题
    subscribe(key, fn) { // 订阅
        if (!this.topics[key]) {
            this.topics[key] = [];
        }
        this.topics[key].push(fn);
    },
    unSubscribe(key, fn) { // 删除
        let fns = this.topics[key];
        for (let i = 0; i < fns.length; ++i) {
            if (fns[i] === fn) {
                fns.splice(i, 1);
            }
        }
    },
    publish(...args) { // 发布
        let key = args.shift();
        let fns = this.topics[key];
        if (!fns || fns.length <= 0) return;
        for (let i = 0, len = fns.length; i < len; i++) {
            fns[i].apply(this, args);
        }
    }
}

// 调用
publisher.subscribe('name', (name) => {
    console.log(`your name is ${name}`);
})
publisher.subscribe('gender', (gender) => {
    console.log(`your gender is ${gender}`);
})
publisher.publish('name', '奥特曼');  // your name is 奥特曼
publisher.publish('gender', '男');  // your gender is 男

模块模式

  • 模块模式是前端最常用的模式,特别适合维护一段独立的代码片段。
  • 模块模式几乎是所有JS流行的插件、框架的基础,比如jQuery、Backbone、Ember等。
  • 可以创建私有(private)、公有(public)属性。
  • 模块模式要利用 IIFE(立即调用函数表达式)和闭包来形成私有作用域。
const Exposer = (function () {
    let privateVariable = 10;

    const privateMethod = function () {
        console.log('Inside a private method!');
        privateVariable++;
    }

    return {
        first: function () {
            console.log('This is a method I want to expose!');
        },
        second: function () {
            privateMethod();
        }
    };
})();

Exposer.first();        // Output: This is a method I want to expose!
Exposer.second();       // Output: Inside a private method!
Exposer.privateMethod;  // undefined

工厂模式

  • 工厂模式,顾名思义,就是为了创造对象。
  • 工厂模式类似于现实的工厂生产线,可以生产出大量类似的商品。
  • 工厂模式的优点在于:能解决多个相似的问题,减少大量冗余代码。
const FactoryFriend = function () {
    function Friend(name, age, height, nationality) {
        this.name = name;
        this.age = age;
        this.height = height;
        this.nationality = nationality
    }
    this.create = function (category) {
        switch (category) {
            case '男':
                return new Friend('A 朋友', 28,  170, '中国')
                break;
            case '女':
                return new Friend('B 朋友', 19, 160, '日本')
                break;
            default:
                throw new Error('参数错误')
        }
    };
}

const aGirl = FactoryFriend.create('男');
const bGirl = FactoryFriend.create('女');

策略模式

  • 策略模式定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。
  • 策略模式可以有效避免很多if条件语句。
  • 策略模式符合开放-封闭原则,使代码更容易理解和扩展。
  • 策略模式中的代码可以复用。
const express = {
    '顺丰': function (package) {
        // 路径计算...
        return "¥45.95";
    },
    '京东': function (package) {
        // 路径计算...
        return "¥39.40";
    },
    '韵达': function (package) {
        // 路径计算...
        return "¥24.45";
    }
};
const calculateExpense = function (type, package) {
    return express[type] ? express[type](package) : 0;
};
calculateExpense('京东', 1); // ¥39.40
calculateExpense('韵达', 1); // ¥24.45

代理模式

  • 为其他对象提供一种代理以控制对这个对象的访问,客户端甚至感知不到代理层的存在。
  • 代理对象可以代替本体对象被实例化,此时本体对象未真正实例化,等到合适时机再实例化。
  • 代理模式可以延迟创建开销很大的本体对象,他会把本体的实例化推迟到有方法被调用时。

示例代码:图片加载

// 普通模式
const myImage = (function () {
    const imgNode = document.createElement('img');

    return {
        setSrc: function (src, id) {
            imgNode.src = src;
            document.getElementById(id).appendChild(imgNode);
        }
    }
})();

// 普通模式加载图片
myImage.setSrc('https://tengmed.com/a.png', "avatar");

// 代理模式
const ProxyImage = (function () {
    const img = new Image();
    img.onload = function () {
        myImage.setSrc(this.src, this.id);
    };
    return {
        setSrc: function (src, id) {
            myImage.setSrc('http://loading.gif', id); // 添加默认loading图片
            img.src = src;
            img.id = id;
        }
    }
})();

// 代理模式加载图片
ProxyImage.setSrc('https://tengmed.com/a.png', "avatar");

示例代码:缓存代理

let mult = function () {
    let a = 1;
    for (let i = 0, ilen = arguments.length; i < ilen; i += 1) {
        a = a * arguments[i];
    }
    return a;
};
// 计算加法
let plus = function () {
    let a = 0;
    for (let i = 0, ilen = arguments.length; i < ilen; i += 1) {
        a += arguments[i];
    }
    return a;
}
// 代理函数
let proxyFunc = function (fn) {
    let cache = {};  // 缓存对象
    return function () {
        let args = Array.prototype.join.call(arguments, ',');
        if (args in cache) {
            return cache[args];   // 使用缓存代理
        }
        return cache[args] = fn.apply(this, arguments);
    }
};
let proxyMult = proxyFunc(mult);
console.log(proxyMult(1, 2, 3, 4)); // 24
console.log(proxyMult(1, 2, 3, 4)); // 缓存取 24

let proxyPlus = proxyFunc(plus);
console.log(proxyPlus(1, 2, 3, 4));  // 10
console.log(proxyPlus(1, 2, 3, 4));  // 缓存取 10
Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐