设计模式

设计模式的本质是面向对象设计原则的实际运用,是对类的封装性继承性多态性以及类的关联关系和组合关系的充分理解。是解决特定问题的一系列套路,是前辈们的代码设计经验的总结,具有一定的普遍性,可以反复使用。其目的是为了提高代码的可复用性、可读性、可维护性

创建型 结构型 行为型
工厂模式 适配器模式 职责链模式
抽象工厂模式 桥接模式 命令模式
原型模式 组合模式 解释器模式
单例模式 装饰器模式 迭代器模式
建造者模式 外观模式 中介者模式
- 享元模式 备忘录模式
- 代理模式 观察者模式
- - 状态模式
- - 策略模式
- - 模板方法模式
- - 访问者模式

# 工厂模式

# 概述

# 定义

工厂模式是用来创建对象的一种最常用的设计模式。不暴露创建对象的具体逻辑,而是将逻辑封装在一个函数中,那么这个函数就可以被视为一个工厂。

工厂模式根据抽象程度的不同可以分为:

  • 简单工厂模式(Simple Factory)
  • 工厂方法模式(Factory Method)
  • 抽象工厂模式(Abstract Factory)

# 优点

  • 创建对象的过程可能很复杂,但我们只需要关心创建结果
  • 构造函数和创建者分离,符合「开闭原则」
  • 一个调用者想创建一个对象,只要知道其名称就可以了
  • 扩展性高,如果想增加一个产品,只要扩展一个工厂类就可以

# 缺点

  • 添加新产品时,需要编写新的具体产品类,一定程度上增加了系统的复杂度
  • 考虑到系统的可扩展性,需要引入抽象层,在客户端代码中均使用抽象层进行定义,增加了系统的抽象性和理解难度

# 适用场景

  • 如果你不想让某个子系统与较大的那个对象之间形成强耦合,而是想运行时从许多子系统中进行挑选的话,那么工厂模式是一个理想的选择
  • new 操作简单封装,遇到 new 的时候就应该考虑是否用工厂模式
  • 需要依赖具体环境创建不同实例,这些实例都有相同的行为,这时候我们可以使用工厂模式,简化实现的过程,同时也可以减少每种对象所需的代码量,有利于消除对象间的耦合,提供更大的灵活性

# 实现

# 简单工厂模式

简单工厂模式又叫静态工厂模式,由一个工厂对象决定创建某一种产品对象类的实例。

/**
 * 封装 User 类,根据用户角色实例化用户,分配页面权限
 */
type Role = 'admin' | 'user' | 'superAdmin';

interface IOptions {
    name: string,
    viewPage: string[]
}

// User类
class User {
    name: string;
    viewPage: string[];
    // 构造器
    constructor(opt: IOptions) {
        this.name = opt.name;
        this.viewPage = opt.viewPage;
    }

    // 静态方法
    static getInstance(role: Role) {
        switch (role) {
            case 'superAdmin':
                return new User({
                    name: '超级管理员',
                    viewPage: ['首页', '通讯录', '发现页', '应用数据', '权限管理']
                });
            case 'admin':
                return new User({
                    name: '管理员',
                    viewPage: ['首页', '通讯录', '发现页', '应用数据']
                });
            case 'user':
                return new User({
                    name: '普通用户',
                    viewPage: ['首页', '通讯录', '发现页']
                });
            default:
                throw new Error('参数错误,可选参数:superAdmin、admin、user');
        }
    }
}

// 调用
let superAdmin = User.getInstance('superAdmin');
let admin = User.getInstance('admin');
let normalUser = User.getInstance('user');

# 工厂方法模式

工厂方法模式是将创建对象的工作推到子类中进行,这样核心类就变成了抽象类。

type Role = 'admin' | 'user' | 'superAdmin';

interface IOptions {
    name: string,
    viewPage: string[]
}

abstract class User {
    name: string;
    viewPage: string[];
    constructor(options: IOptions) {
        this.name = options.name;
        this.viewPage = options.viewPage;
    }
}

class UserFactory extends User {
    constructor(options: IOptions = { name: '', viewPage: [] }) {
        options.name = options.name || '';
        options.viewPage = options.viewPage || [];
        super(options);
    }
    create(role: Role) {
        switch (role) {
            case 'superAdmin':
                return new UserFactory({
                    name: '超级管理员',
                    viewPage: ['首页', '通讯录', '发现页', '应用数据', '权限管理']
                });
            case 'admin':
                return new UserFactory({
                    name: '管理员',
                    viewPage: ['首页', '通讯录', '发现页', '应用数据']
                });
            case 'user':
                return new UserFactory({
                    name: '普通用户',
                    viewPage: ['首页', '通讯录', '发现页']
                });
            default:
                throw new Error('参数错误,可选参数:superAdmin、admin、user')
        }
    }
}

let userFactory = new UserFactory();
let superAdmin = userFactory.create('superAdmin');
let admin = userFactory.create('admin');
let user = userFactory.create('user');

# 抽象工厂模式

抽象工厂模式并不直接生成实例, 而是用于对产品类簇的创建。

/**
 * 通过继承抽象类的方式创建 UserOfQq/UserOfWechat/UserOfWeibo 等子类类簇,
 * 使用 getAbstractUserFactor 来返回指定的类簇
 */
type socialPlatform = 'qq' | 'weibo' | 'wechat';

interface IOptions {
    name: string,
    viewPage: string[]
}

abstract class User {
    type: string;
    constructor(type: socialPlatform) {
        this.type = type;
    }
}

class UserOfQq extends User {
    name: string;
    viewPage: string[];
    constructor(name: string) {
        super('qq');
        this.name = name;
        this.viewPage = ['a', 'b', 'c'];
    }
}

class UserOfWechat extends User {
    name: string;
    viewPage: string[];
    constructor(name: string) {
        super('wechat');
        this.name = name;
        this.viewPage = ['a', 'b', 'c'];
    }
}

class UserOfWeibo extends User {
    name: string;
    viewPage: string[];
    constructor(name: string) {
        super('weibo');
        this.name = name;
        this.viewPage = ['a', 'b', 'c'];
    }
}

function getAbstractUserFactory(type: socialPlatform) {
    switch (type) {
        case 'wechat':
            return UserOfWechat;
        case 'qq':
            return UserOfQq;
        case 'weibo':
            return UserOfWeibo;
        default:
            throw new Error('参数错误,可选参数:superAdmin、admin、user')
    }
}

let WechatUserClass = getAbstractUserFactory('wechat');
let QqUserClass = getAbstractUserFactory('qq');
let WeiboUserClass = getAbstractUserFactory('weibo');

let wechatUser = new WechatUserClass('微信小李');
let qqUser = new QqUserClass('QQ小李');
let weiboUser = new WeiboUserClass('微博小李');

# 应用

中后台项目,我们通常需要根据用户权限来展示菜单,所以登录进入后台页面后,我们需要获取用户的角色及相应的读写权限,然后渲染出用户可访问的菜单。这一过程可利用工厂模式来处理,伪代码如下:

// router-factory.js
import SuperAdmin from '../components/super-admin.vue'
import NormalAdmin from '../components/admin.vue'
import User from '../components/user.vue'
import NotFound404 from '../components/404.vue'

let allRoute = [
    // 超级管理员页面
    {
        path: '/super-admin',
        name: 'SuperAdmin',
        component: SuperAdmin
    },
    // 普通管理员页面
    {
        path: '/normal-admin',
        name: 'NormalAdmin',
        component: NormalAdmin
    },
    // 普通用户页面
    {
        path: '/user',
        name: 'User',
        component: User
    },
    // 404页面
    {
        path: '*',
        name: 'NotFound404',
        component: NotFound404
    }
];

let routerFactory = (role) => {
    switch (role) {
        case 'superAdmin':
            return {
                name: 'SuperAdmin',
                route: allRoute
            };
        case 'normalAdmin':
            return {
                name: 'NormalAdmin',
                route: allRoute.splice(1)
            }
        case 'user':
            return {
                name: 'User',
                route:  allRoute.splice(2)
            }
        default:
            throw new Error('参数错误! 可选参数: superAdmin, normalAdmin, user')
    }
}

export { routerFactory };
// login.vue
import {routerFactory} from '../router/routerFactory.js'
export default {
    // ...
    methods: {
        userLogin() {
            // 请求登陆接口,获取用户权限,根据权限调用 this.getRoute 方法
            // ..
        },

        getRoute(role) {
            // 根据权限调用 routerFactory 方法
            let routerObj = routerFactory(role);

            // 给 vue-router 添加该权限所拥有的路由页面
            this.$router.addRoutes(routerObj.route);

            // 跳转到相应页面
            this.$router.push({ name: routerObj.name });
        }
    }
};

# 单例模式

# 概述

# 定义

保证一个类仅有一个实例,并提供一个访问它的全局访问点,这样的模式就叫做单例模式

# 优点

  • 划分命名空间,减少全局变量
  • 增强模块性,把自己的代码组织在一个全局变量名下,放在单一位置,便于维护
  • 且只会实例化一次,简化了代码的调试和维护

# 缺点

  • 由于单例模式提供的是一种单点访问,所以它有可能导致模块间的强耦合,从而不利于单元测试。无法单独测试一个调用了来自单例的方法的类,而只能把它与那个单例作为一个单元一起测试

# 适用场景

处理资源访问冲突、用来创建全局唯一类。比如:

  • 定义命名空间和实现分支型方法
  • 登录框
  • vuex 和 redux 中的 store

# 实现

class Singleton {
    constructor(name) {
        this.name = name;
        this.instance = null;
    }

    getName() {
        console.log(this.name);
    }

    getInstance(name) {
        if(!this.instance) {
            this.instance = new Singleton(name);
        }

        return this.instance;
    }
}

const singleton = new Singleton();
const a = singleton.getInstance('a');
const b = singleton.getInstance('b');

console.log(a);
console.log(b);
console.log(a === b);

# 应用

点击某个按钮的时候需要在页面弹出一个遮罩层,这个遮罩层是全局唯一的:

const singleton = function (fn) {
    let result;
    return function () {
        return result || (result = fn .apply(this, arguments));
    }
}

const createMask = singleton(function () {
    return document.body.appendChild(document.createElement('div'));
});

# 外观模式

# 概述

为一组复杂的子系统接口提供一个更高级的统一接口,通过这个接口使得对子系统接口的访问更容易,不符合「单一职责原则」和「开放封闭原则」。

# 应用

// 示例 1
class A {
    eat () {}
}
class  B {
    eat () {}
}
class C {
    eat () {
        const a = new A();
        const b = new B();
        a.eat();
        b.eat();
    }
}

// 示例 2:JS事件不同浏览器兼容处理
function addEvent(el, type, fn) {
    if (window.addEventListener) {
        el.addEventListener(type, fn, false);
    } else if (window.attachEvent) {
        el.attachEvent('on' + type, fn);
    } else {
        el['on' + type] = fn;
    }
}

# 装饰器模式

# 概述

# 定义

向一个现有的对象添加新的功能,同时又不改变其结构的设计模式被称为装饰器模式(Decorator Pattern),它是作为现有的类的一个包装(Wrapper)。

ESnext 中有一个 Decorator提案 (opens new window),使用一个以 @ 开头的函数对 ES6 中的 class 及其属性、方法进行修饰。Decorator 的详细语法请参考阮一峰的《ECMASciprt入门 —— Decorator》 (opens new window)

# 优点

  • 装饰类和被装饰类都只关心自身的核心业务,实现了解耦
  • 方便动态的扩展功能,且提供了比继承更多的灵活性

# 缺点

  • 多层装饰比较复杂
  • 常常会引入许多小对象,看起来比较相似,实际功能大相径庭,从而使得我们的应用程序架构变得复杂起来

# 适用场景

  • 装饰器类是对原始功能的增强
  • 装饰器类和原始类继承同样的父类,这样我们可以对原始类嵌套多个装饰器类
  • 主要解决继承关系过于复杂的问题,通过组合来替代继承
  • 可以通过对原始类嵌套使用多个装饰器

# 实现

class Cellphone {
    create() {
        console.log('生成一个手机');
    }
}

class Decorator {
    constructor(cellphone) {
        this.cellphone = cellphone;
    }
    create() {
        this.cellphone.create();
        this.createShell(cellphone);
    }
    createShell() {
        console.log('生成手机壳');
    }
}

// 测试代码
let cellphone = new Cellphone();
cellphone.create();

console.log('------------');
let dec = new Decorator(cellphone);
dec.create();

# 应用

以下例子结合装饰器的新语法和装饰器模式做了一个简单封装,请勿用于生产环境。

# @debounce 实现函数防抖

function debounce(timeout) {
    const instanceMap = new Map(); // 创建一个 Map 的数据结构,将实例化对象作为 key

    return function (target, key, descriptor) {
        return Object.assign({}, descriptor, {
            value: function value() {
                // 清除延时器
                clearTimeout(instanceMap.get(this));
                // 设置延时器
                instanceMap.set(this, setTimeout(() => {
                    // 调用该方法
                    descriptor.value.apply(this, arguments);
                    // 将延时器设置为 null
                    instanceMap.set(this, null);
                }, timeout));
            }
        });
    }
}

class Editor {
    constructor() {
        this.content = '';
    }

    @debounce(500)
    updateContent(content) {
        console.log(content);
        this.content = content;
    }
}

const editor1 = new Editor();
editor1.updateContent(1);
setTimeout(() => { editor1.updateContent(2); }, 400);

const editor2 = new Editor();
editor2.updateContent(3);
setTimeout(() => { editor2.updateContent(4); }, 600);

// 打印结果: 3 2 4

# @deprecate 实现警告提示

function deprecate(deprecatedObj) {
    return function(target, key, descriptor) {
        const deprecatedInfo = deprecatedObj.info;
        const deprecatedUrl = deprecatedObj.url;
        // 警告信息
        const txt = `DEPRECATION ${target.constructor.name}#${key}: ${deprecatedInfo}. ${deprecatedUrl ? 'See '+ deprecatedUrl + ' for more detail' : ''}`;

        return Object.assign({}, descriptor, {
            value: function value() {
                // 打印警告信息
                console.warn(txt);
                descriptor.value.apply(this, arguments);
            }
        })
    }
}

class MyLib {
    @deprecate({
        info: 'The methods will be deprecated in next version',
        url: 'http://www.baidu.com'
    })
    deprecatedMethod(txt) {
        console.log(txt);
    }
}

const lib = new MyLib();
lib.deprecatedMethod('调用了一个要在下个版本被移除的方法');
// DEPRECATION MyLib#deprecatedMethod: The methods will be deprecated in next version. See http://www.baidu.com for more detail
// 调用了一个要在下个版本被移除的方法

# 适配器模式

# 概述

# 定义

将一个类的接口转化为另外一个接口,以满足用户需求,使类之间接口不兼容问题通过适配器得以解决。

# 优点

  • 可以让任何两个没有关联的类一起运行
  • 提高了类的复用
  • 适配对象,适配库,适配数据

# 缺点

  • 额外对象的创建,非直接调用,存在一定的开销(且不像代理模式在某些功能点上可实现性能优化)
  • 如果没必要使用适配器模式的话,可以考虑重构,如果使用的话,尽量把文档完善

适配器与代理模式差异

  • 代理模式:提供一模一样的接口
  • 适配器模式:提供一个不同的接口(如不同版本的插头)

# 适用场景

  • 适配器模式用于补救设计上的缺陷,将不兼容的接口变得兼容
  • 封装有缺陷的接口设计
  • 统一多个类的接口设计
  • 替换依赖的外部系统
  • 兼容老版本接口
  • 适配不同格式的数据

# 实现

适配器模式的实现可以有多种方式,此处仅做简单示例:

class Plug {
    getName() {
        return '「iphone 充电头」';
    }
}

class Target {
    constructor() {
        this.plug = new Plug();
    }
    getName() {
        return this.plug.getName() + ' 适配器「Type-c 充电头」';
    }
}

let target = new Target();
target.getName(); // 「iphone 充电头」适配器转「Type-c 充电头」

# 应用

# 第三方库适配

项目上线前我们通常需要接入页面数据统计,比如「百度统计」。随着项目迭代可能需要接入/更换其他数据平台,比如「神策」、「友盟」等,因为埋点的位置会分散在所有页面的各个点位,若更换一次数据平台就调整一次需要耗费大量的时间精力,此时我们可以用适配器模式做各类平台接口的适配处理,对外提供统一的接口:

// 伪代码,仅作参考
interface IOptions {
    platform: 'web' | 'mobile';
    action: string;
    label?: string;
    value?: any;
}

class Tracker {
    type: 'baidu' | 'shence' | 'youmeng';
    constructor(type) {
        // ...
        this.type = type;
    }

    trackEvent(options: IOptions) {
        switch (this.type) {
            case 'baidu':
                this.trackEventOfBd(
                    options.platform,
                    options.action,
                    options.label,
                    options.value,
                );
                break;
            case 'shence':
                this.trackEventOfSC(options.action, options.value);
                break;
            case 'youmeng':
                this.trackEventOfYM();
                break;
            default:
                throw new Error('参数错误,可选参数:superAdmin、admin、user');
        }
    }

    trackEventOfBD(category, action, opt_label, opt_value) {
        _hmt.push(['_trackEvent', category, action, opt_label, opt_value]);
    }

    trackEventOfSC(eventName, value) {
        sa.track(eventName, {
            attrName: value
        });
    }

    trackEventOfYM() {
        // ...
    }
}

# 参数适配

在对外开放的 SDK 中,我们调用内部接口通常需要传入非常多的参数,但这对于外部开发这来说是没有必要关注的:

import Editor from '@abc/editor-framework';

class imageEditor {
    // ...
    editor: Editor;
    init(appID: string) {
        this.editor = new Editor({
            version: '1.0.0',
            platform: 'web',
            type: 'image',
            // ...
            appid: appID,
        });
    }
}

# 数据适配

数据的适配在前端中是最为常见的场景,这时适配器在解决前后端的数据依赖上有着重要的意义。通常服务器端传递的数据和我们前端需要使用的数据格式是不一致的,特别是在在使用一些UI框架时,框架所规定的数据有着固定的格式。所以,这个时候我们就需要对后端的数据格式进行适配。

例如网页中有一个使用 Echarts 折线图对网站每周的 uv,通常后端返回的数据格式如下所示:

[
    {
        "day": "周一",
        "uv": 6300
    },
    {
        "day": "周二",
        "uv": 7100
    },  {
        "day": "周三",
        "uv": 4300
    },  {
        "day": "周四",
        "uv": 3300
    },  {
        "day": "周五",
        "uv": 8300
    },  {
        "day": "周六",
        "uv": 9300
    }, {
        "day": "周日",
        "uv": 11300
    }
]

但是 Echarts 需要的 x 轴的数据格式和坐标点的数据是长下面这样的:

["周二", "周二", "周三""周四""周五""周六""周日"] // x 轴的数据

[6300. 7100, 4300, 3300, 8300, 9300, 11300] // 坐标点的数据

所以这是我们就可以使用一个适配器,将后端的返回数据做适配:

// x 轴适配器
function echartXAxisAdapter(res) {
    return res.map(item => item.day);
}

// 坐标点适配器
function echartDataAdapter(res) {
    return res.map(item => item.uv);
}

适配器模式在 JS 中的使用场景很多,在参数的适配上,有许多库和框架都使用适配器模式;数据的适配在解决前后端数据依赖上十分重要。但是适配器模式本质上是一个亡羊补牢的模式,它解决的是现存的两个接口之间不兼容的问题,你不应该在软件的初期开发阶段就使用该模式;如果在设计之初我们就能够统筹的规划好接口的一致性,那么适配器就应该尽量减少使用。

# 代理模式

# 概述

# 定义

为其他对象提供一种代理以控制对这个对象的访问。在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。

# 优点

在面向对象的编程中,代理模式的合理使用能够很好的体现下面两条原则:

  • 单一职责原则:面向对象设计中鼓励将不同的职责分布到细粒度的对象中,Proxy 在原对象的基础上进行了功能的衍生而又不影响原对象,符合松耦合高内聚的设计理念。
  • 开放-封闭原则:代理可以随时从程序中去掉,而不用对其他部分的代码进行修改,在实际场景中,随着版本的迭代可能会有多种原因不再需要代理,那么就可以容易的将代理对象换成原对象的调用

# 缺点

  • 处理请求速度可能有差别,非直接访问存在开销

装饰器模式与代理模式的差别

  • 装饰器模式:扩展功能,原有功能不变且可直接使用
  • 代理模式:显示原有功能,但是经过限制之后的

# 适用场景

  • 给原类添加非功能性需求,为了将代码与原业务解耦
  • 业务系统的非功能性需求开发:监控、统计、鉴权、限流、日志、缓存

# 实现

interface Login {
    login()
}

class User implements Login {
    login() {
        console.log('user login...');
    }
}

class UserProxy implements Login {
    user = new User()

    login() {
        console.log('login before');
        this.user.login();
        console.log('login after');
    }
}

# 应用

# 缓存代理

缓存代理可以将一些开销很大的方法的运算结果进行缓存,再次调用该函数时,若参数一致,则可以直接返回缓存中的结果,而不用再重新进行运算:

const getCacheProxy = (fn, cache = new Map()) => {
    return new Proxy(fn, {
        apply(target, context, args) {
            const argsString = args.join(' ');

            if (cache.has(argsString)) {
                // 如果有缓存,直接返回缓存数据
                // console.log(`输出${args}的缓存结果: ${cache.get(argsString)}`);
                return cache.get(argsString);
            }

            const result = fn(...args);
            cache.set(argsString, result);

            return result;
        }
    });
}

// 测试
const getFib = (number) => {
    if (number <= 2) {
        return 1;
    } else {
        return getFib(number - 1) + getFib(number - 2);
    }
}
const getFibProxy = getCacheProxy(getFib);
getFibProxy(40); // => 102334155

第二次调用 getFibProxy(40) 时,getFib 函数并没有被调用,而是直接从 cache 中返回了之前被缓存好的计算结果。通过加入缓存代理的方式,getFib 只需要专注于自己计算斐波那契数列的职责,缓存的功能使由 Proxy 对象实现的。这实现了我们之前提到的单一职责原则

# 验证代理

利用 Proxy 构造函数第二个参数中的 set 方法,可以很方便的验证向一个对象的传值:

// 表单对象
const userForm = {
    account: '',
    password: '',
}
// 验证方法
const validators = {
    account(value) {
        // account 只允许为中文
        const re = /^[\u4e00-\u9fa5]+$/;
        return {
            valid: re.test(value),
            error: '"account" is only allowed to be Chinese'
        }
    },
    password(value) {
        // password 的长度应该大于6个字符
        return {
            valid: value.length >= 6,
            error: '"password" should more than 6 character'
        }
    }
}

const getValidateProxy = (target, validators) => {
    return new Proxy(target, {
        _validators: validators,
        set(target, prop, value) {
            if (value === '') {
                console.error(`"${prop}" is not allowed to be empty`);
                return target[prop] = false;
            }

            const validResult = this._validators[prop](value);

            if(validResult.valid) {
                return Reflect.set(target, prop, value);
            } else {
                console.error(`${validResult.error}`);
                return target[prop] = false;
            }
        }
    });
}

// 测试
const userFormProxy = getValidateProxy(userForm, validators);
userFormProxy.account = '123'; // "account" is only allowed to be Chinese
userFormProxy.password = 'he'; // "password" should more than 6 character

我们调用 getValidateProxy 方法去生成了一个代理对象 userFormProxy,该对象在设置属性的时候会根据 validators 的验证规则对值进行校验。这我们使用的是 console.error 抛出错误信息,当然我们也可以加入对 DOM 的事件来实现页面中的校验提示。

# 实现私有属性

ES2022 (opens new window) 之前 JavaScript 没有私有属性,通常私有属性的实现是通过函数作用域中变量实现的。利用 Proxy 构造函数中的第二个参数所提供的方法,我们可以为对象提供私有属性的能力:

function getPrivateProps(obj, filterFunc) {
    return new Proxy(obj, {
        get(obj, prop) {
            if (!filterFunc(prop)) {
                let value = Reflect.get(obj, prop);
                // 如果是方法,将 this 指向修改原对象
                if (typeof value === 'function') {
                    value = value.bind(obj);
                }
                return value;
            }
        },
        set(obj, prop, value) {
            if (filterFunc(prop)) {
                throw new TypeError(`Can't set property "${prop}"`);
            }
            return Reflect.set(obj, prop, value);
        },
        has(obj, prop) {
            return filterFunc(prop) ? false : Reflect.has(obj, prop);
        },
        ownKeys(obj) {
            return Reflect.ownKeys(obj).filter(prop => !filterFunc(prop));
        },
        getOwnPropertyDescriptor(obj, prop) {
            return filterFunc(prop) ? undefined : Reflect.getOwnPropertyDescriptor(obj, prop);
        }
    });
}

// 测试
function propFilter(prop) {
    return prop.indexOf('_') === 0;
}

const myObj = {
    public: 'hello',
    _private: 'secret',
    method: function () {
        console.log(this._private);
    }
};

const myProxy = getPrivateProps(myObj, propFilter);

console.log(JSON.stringify(myProxy));       // {"public":"hello"}
console.log(myProxy._private);              // undefined
console.log('_private' in myProxy);         // false
console.log(Object.keys(myProxy));          // ["public", "method"]
for (let prop in myProxy) { console.log(prop); }    // public  method
myProxy._private = 1; // Uncaught TypeError: Can't set property "_private"

# 策略模式

# 概述

# 定义

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

# 优点

  • 利用组合、委托、多态等技术和思想,可以有效的避免多重条件选择语句
  • 提供了对「开放-封闭原则」的完美支持,将算法封装在独立的 strategy 中,使得它们易于切换,理解,易于扩展
  • 利用组合和委托来让 Context 拥有执行算法的能力,这也是继承的一种更轻便的代替方案

# 缺点

  • 会在程序中增加许多策略类或者策略对象
  • 要使用策略模式,必须了解所有的 strategy,必须了解各个 strategy 之间的不同点,这样才能选择一个合适的 strategy

# 适用场景

  • 如果在一个系统里面有许多类,它们之间的区别仅在于它们的「行为」,那么使用策略模式可以动态地让一个对象在许多行为中选择一种行为
  • 一个系统需要动态地在几种算法中选择一种
  • 表单验证

# 实现

enum StrategyType {
    S,
    A,
    B
}

const strategyFn = {
    'S': function (salary: number) {
        return salary * 4;
    },
    'A': function (salary: number) {
        return salary * 3;
    },
    'B': function (salary: number) {
        return salary * 2;
    }
}

const calculateBonus = function (level: StrategyType, salary: number) {
    return strategyFn[level](salary);
}

calculateBonus(StrategyType.A, 10000); // 30000

# 应用

# 状态模式

# 概述

# 定义

状态模式的关键是区分事物内部的状态,事物内部状态的改变往往会带来事物的行为改变

# 优点

  • 定义了状态与行为之间的关系,封装在一个类里,更直观清晰,增改方便
  • 状态与状态间,行为与行为间彼此独立互不干扰
  • 用对象代替字符串来记录当前状态,使得状态的切换更加一目了然

# 缺点

  • 会在系统中定义许多状态类
  • 逻辑分散

# 适用场景

  • 一个对象的行为取决于它的状态,并且它必须在运行时刻根据状态改变它的行为
  • 一个操作中含有大量的分支语句,而且这些分支语句依赖于该对象的状态

# 应用

class weakLight {
    light: Light

    constructor(light: Light) {
        this.light = light;
    }

    press() {
        console.log('打开强光');
        this.light.setState(this.light.strongLight);
    }
}

class strongLight {
    light: Light

    constructor(light: Light) {
        this.light = light;
    }

    press() {
        console.log('关灯');
        this.light.setState(this.light.offLight);
    }
}

class offLight {
    light: Light

    constructor(light: Light) {
        this.light = light;
    }

    press() {
        console.log('打开弱光');
        this.light.setState(this.light.weakLight);
    }
}


class Light {
    weakLight: weakLight
    strongLight: strongLight
    offLight: offLight
    currentState: offLight | weakLight | strongLight // 当前状态(默认关灯状态)

    constructor() {
        this.weakLight = new weakLight(this);
        this.strongLight = new strongLight(this);
        this.offLight = new offLight(this);
        this.currentState = this.offLight;
    }

    press() {
        this.currentState.press();
    }

    setState(state) {
        this.currentState = state;
    }
}

// test
const light = new Light();
light.press();
light.press();
light.press();
light.press();
light.press();
light.press();

# 观察者模式

# 概述

# 定义

定义了一种一对多的关系,让多个观察者对象同时监听某一个主题对象,这个主题对象的状态发生变化时就会通知所有的观察者对象,使它们能够自动更新自己,当一个对象的改变需要同时改变其它对象,并且它不知道具体有多少对象需要改变的时候,就应该考虑使用观察者模式。

# 优点

  • 支持简单的广播通信,自动通知所有已经订阅过的对象
  • 目标对象与观察者之间的抽象耦合关系能单独扩展以及重用
  • 增加了灵活性
  • 观察者模式所做的工作就是在解耦,让耦合的双方都依赖于抽象,而不是依赖于具体,从而使得各自的变化都不会影响到另一边的变化

# 缺点

过度使用会导致对象与对象之间的联系弱化,会导致程序难以跟踪维护和理解

# 应用

// 主题,保存状态,状态变化之后触发所有观察者对象
class Subject {
    constructor() {
        this.state = 0;
        this.observers = [];
    }
    getState() {
        return this.state;
    }
    setState(state) {
        this.state = state;
        this.notifyAllObservers();
    }
    notifyAllObservers() {
        this.observers.forEach(observer => {
            observer.update();
        });
    }
    attach(observer) {
        this.observers.push(observer);
    }
}

// 观察者
class Observer {
    constructor(name, subject) {
        this.name = name;
        this.subject = subject;
        this.subject.attach(this);
    }
    update() {
        console.log(`${this.name} update, state: ${this.subject.getState()}`);
    }
}

// 测试
let s = new Subject();
let o1 = new Observer('o1', s);
let o2 = new Observer('02', s);

s.setState(12);

# 迭代器模式

# 概述

# 定义

提供一种方法顺序访问一个聚合对象中各个元素,而又无须暴露该对象的内部表示。

# 特点

  • 访问一个聚合对象的内容而无需暴露它的内部表示
  • 为遍历不同的集合结构提供一个统一的接口,从而支持同样的算法在不同的集合结构上进行操作

# 应用

  • Array.prototype.forEach
  • jQuery 中的 $.each()
  • ES6 Iterator

# 中介者模式

# 概述

# 定义

中介者模式:用一个中介对象来封装一系列的对象交互。中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。

# 优点

  • 使各对象之间耦合松散,而且可以独立地改变它们之间的交互
  • 中介者和对象一对多的关系取代了对象之间的网状多对多的关系
  • 如果对象之间的复杂耦合度导致维护很困难,而且耦合度随项目变化增速很快,就需要中介者重构代码

# 缺点

系统中会新增一个中介者对象,因为对象之间交互的复杂性,转移成了中介者对象的复杂性,使得中介者对象经常是巨大的。中介者对象自身往往就是一个难以维护的对象。

# 适用场景

  • 系统中对象之间存在比较复杂的引用关系,导致它们之间的依赖关系结构混乱而且难以复用该对象
  • 想通过一个中间类来封装多个类中的行为,而又不想生成太多的子类

# 应用

var mediator = (function () {
    // Storage for topics that can be broadcast or listened to
    var topics = {};

    // Subscribe to a topic, supply a callback to be executed
    // when that topic is broadcast to
    var subscribe = function (topic, fn) {
        if (!topics[topic]) {
            topics[topic] = [];
        }

        topics[topic].push({ context: this, callback: fn });
        return this;
    };

    // Publish/broadcast an event to the rest of the application
    var publish = function (topic) {
        var args;
        if (!topics[topic]) {
            return false;
        }

        args = Array.prototype.slice.call(arguments, 1);
        for (var i = 0, l = topics[topic].length; i < l; i++) {
            var subscription = topics[topic][i];
            subscription.callback.apply(subscription.context, args);
        }
        return this;
    };

    return {
        publish: publish,
        subscribe: subscribe,
        installTo: function (obj) {
            obj.subscribe = subscribe;
            obj.publish = publish;
        }
    };
}());

# 享元模式

# 概述

# 定义

运用共享技术有效地支持大量细粒度对象的复用。系统只使用少量的对象,而这些对象都很相似,状态变化很小,可以实现对象的多次复用。由于享元模式要求能够共享的对象必须是细粒度对象,因此它又称为轻量级模式,它是一种对象结构型模式。

# 优点

大大减少对象的创建,降低系统的内存,使效率提高。

# 缺点

提高了系统的复杂度,需要分离出外部状态和内部状态,而且外部状态具有固有化的性质, 不应该随着内部状态的变化而变化,否则会造成系统的混乱。

# 应用

// 对象池:维护一个空的池子,如果需要一个对象,直接从池子中获取,
// 若是没有则新建一个,该对象完成职责后,再进入池子等待下次被获取。
const toolTipFactory = (function() {
    let toolTipPool = [];

    return {
        create () {
            if (toolTipPool.length === 0) {
                const div = document.createElement('div');
                document.body.appendChild(div);

                return div;
            } else {
                return toolTipPool.shift();
            }
        },
        recover (toolTipDom) {
            return toolTipPool.push(toolTipDom);
        }
    }
})();

// 创建两个节点
const args = [];
const node1 = ['A', 'B'];
for (let i = 0; i < node1.length; i++) {
    const node = toolTipFactory.create();
    node.innerHTML = node1[i];
    args.push(node);
}

// 回收创建的节点
for (let i = 0; i < args.length; i++) {
    toolTipFactory.recover(args[i]);
}

// 创建6个节点
const node2 = ['A', 'B', 'C', 'D', 'E', 'F'];
for (let i = 0; i < node2.length; i++) {
    const node = toolTipFactory.create();
    node.innerHTML = node2[i];
    args.push(node);
}
上次更新: 2024/4/15 02:28:03