欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

JavaScript里的15个常用设计模式解析

最编程 2024-08-06 14:53:06
...

在程序设计中有很多实用的设计模式,而其中大部分语言的实现都是基于“类”。

在JavaScript中并没有类这种概念,JS中的函数属于一等对象,在JS中定义一个对象非常简单(var obj = {}),而基于JS中闭包与弱类型等特性,在实现一些设计模式的方式上与众不同。

本文基于《JavaScript设计模式与开发实践》一书,用一些例子总结一下JS常见的设计模式与实现方法。文章略长,自备瓜子板凳~

 

设计原则

单一职责原则(SRP)

一个对象或方法只做一件事情。如果一个方法承担了过多的职责,那么在需求的变迁过程中,需要改写这个方法的可能性就越大。

应该把对象或方法划分成较小的粒度

最少知识原则(LKP)

一个软件实体应当 尽可能少地与其他实体发生相互作用 

应当尽量减少对象之间的交互。如果两个对象之间不必彼此直接通信,那么这两个对象就不要发生直接的 相互联系,可以转交给第三方进行处理

开放-封闭原则(OCP)

软件实体(类、模块、函数)等应该是可以 扩展的,但是不可修改

当需要改变一个程序的功能或者给这个程序增加新功能的时候,可以使用增加代码的方式,尽量避免改动程序的源代码,防止影响原系统的稳定

 

什么是设计模式

作者的这个说明解释得挺好

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

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

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

不过要注意,使用不当的话,可能会事倍功半。

 

一、单例模式

二、策略模式

三、代理模式

四、迭代器模式

五、发布—订阅模式

六、命令模式

七、组合模式

八、模板方法模式

九、享元模式

十、职责链模式

十一、中介者模式

十二、装饰者模式

十三、状态模式

十四、适配器模式

十五、外观模式

 

一、单例模式

1. 定义

保证一个类仅有一个实例,并提供一个访问它的全局访问点

2. 核心

确保只有一个实例,并提供全局访问

3. 实现

假设要设置一个管理员,多次调用也仅设置一次,我们可以使用闭包缓存一个内部变量来实现这个单例

function SetManager(name) {
    this.manager = name;
}

SetManager.prototype.getName = function() {
    console.log(this.manager);
};

var SingletonSetManager = (function() {
    var manager = null;

    return function(name) {
        if (!manager) {
            manager = new SetManager(name);
        }

        return manager;
    } 
})();

SingletonSetManager('a').getName(); // a
SingletonSetManager('b').getName(); // a
SingletonSetManager('c').getName(); // a

这是比较简单的做法,但是假如我们还要设置一个HR呢?就得复制一遍代码了

所以,可以改写单例内部,实现地更通用一些

// 提取出通用的单例
function getSingleton(fn) {
    var instance = null;

    return function() {
        if (!instance) {
            instance = fn.apply(this, arguments);
        }

        return instance;
    }
}

再进行调用,结果还是一样

// 获取单例
var managerSingleton = getSingleton(function(name) {
    var manager = new SetManager(name);
    return manager;
});

managerSingleton('a').getName(); // a
managerSingleton('b').getName(); // a
managerSingleton('c').getName(); // a

这时,我们添加HR时,就不需要更改获取单例内部的实现了,仅需要实现添加HR所需要做的,再调用即可

function SetHr(name) {
    this.hr = name;
}

SetHr.prototype.getName = function() {
    console.log(this.hr);
};

var hrSingleton = getSingleton(function(name) {
    var hr = new SetHr(name);
    return hr;
});

hrSingleton('aa').getName(); // aa
hrSingleton('bb').getName(); // aa
hrSingleton('cc').getName(); // aa

或者,仅想要创建一个div层,不需要将对象实例化,直接调用函数

结果为页面中仅有第一个创建的div

function createPopup(html) {
    var div = document.createElement('div');
    div.innerHTML = html;
    document.body.append(div);

    return div;
}

var popupSingleton = getSingleton(function() {
    var div = createPopup.apply(this, arguments);
    return div;
});

console.log(
    popupSingleton('aaa').innerHTML,
    popupSingleton('bbb').innerHTML,
    popupSingleton('bbb').innerHTML
); // aaa  aaa  aaa

 

二、策略模式

1. 定义

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

2. 核心

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

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

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

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

3. 实现

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

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

// 加权映射关系
var levelMap = {
    S: 10,
    A: 8,
    B: 6,
    C: 4
};

// 组策略
var scoreLevel = {
    basicScore: 80,

    S: function() {
        return this.basicScore + levelMap['S']; 
    },

    A: function() {
        return this.basicScore + levelMap['A']; 
    },

    B: function() {
        return this.basicScore + levelMap['B']; 
    },

    C: function() {
        return this.basicScore + levelMap['C']; 
    }
}

// 调用
function getScore(level) {
    return scoreLevel[level] ? scoreLevel[level]() : 0;
}

console.log(
    getScore('S'),
    getScore('A'),
    getScore('B'),
    getScore('C'),
    getScore('D')
); // 90 88 86 84 0

在组合业务规则方面,比较经典的是表单的验证方法。这里列出比较关键的部分

// 错误提示
var errorMsgs = {
    default: '输入数据格式不正确',
    minLength: '输入数据长度不足',
    isNumber: '请输入数字',
    required: '内容不为空'
};

// 规则集
var rules = {
    minLength: function(value, length, errorMsg) {
        if (value.length < length) {
            return errorMsg || errorMsgs['minLength']
        }
    },
    isNumber: function(value, errorMsg) {
        if (!/\d+/.test(value)) {
            return errorMsg || errorMsgs['isNumber'];
        }
    },
    required: function(value, errorMsg) {
        if (value === '') {
            return errorMsg || errorMsgs['required'];
        }
    }
};

// 校验器
function Validator() {
    this.items = [];
};

Validator.prototype = {
    constructor: Validator,
    
    // 添加校验规则
    add: function(value, rule, errorMsg) {
        var arg = [value];

        if (rule.indexOf('minLength') !== -1) {
            var temp = rule.split(':');
            arg.push(temp[1]);
            rule = temp[0];
        }

        arg.push(errorMsg);

        this.items.push(function() {
            // 进行校验
            return rules[rule].apply(this, arg);
        });
    },
    
    // 开始校验
    start: function() {
        for (var i = 0; i < this.items.length; ++i) {
            var ret = this.items[i]();
            
            if (ret) {
                console.log(ret);
                // return ret;
            }
        }
    }
};

// 测试数据
function testTel(val) {
    return val;
}

var validate = new Validator();

validate.add(testTel('ccc'), 'isNumber', '只能为数字'); // 只能为数字
validate.add(testTel(''), 'required'); // 内容不为空
validate.add(testTel('123'), 'minLength:5', '最少5位'); // 最少5位
validate.add(testTel('12345'), 'minLength:5', '最少5位');

var ret = validate.start();

console.log(ret);

4. 优缺点

优点

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

缺点

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

 

三、代理模式

1. 定义

为一个对象提供一个代用品或占位符,以便控制对它的访问

2. 核心

当客户不方便直接访问一个 对象或者不满足需要的时候,提供一个替身对象 来控制对这个对象的访问,客户实际*问的是 替身对象。

替身对象对请求做出一些处理之后, 再把请求转交给本体对象

代理和本体的接口具有一致性,本体定义了关键功能,而代理是提供或拒绝对它的访问,或者在访问本体之前做一 些额外的事情

3. 实现

代理模式主要有三种:保护代理、虚拟代理、缓存代理

保护代理主要实现了访问主体的限制行为,以过滤字符作为简单的例子

// 主体,发送消息
function sendMsg(msg) {
    console.log(msg);
}

// 代理,对消息进行过滤
function proxySendMsg(msg) {
    // 无消息则直接返回
    if (typeof msg === 'undefined') {
        console.log('deny');
        return;
    }
    
    // 有消息则进行过滤
    msg = ('' + msg).replace(/泥\s*煤/g, '');

    sendMsg(msg);
}


sendMsg('泥煤呀泥 煤呀'); // 泥煤呀泥 煤呀
proxySendMsg('泥煤呀泥 煤'); //
proxySendMsg(); // deny

它的意图很明显,在访问主体之前进行控制,没有消息的时候直接在代理中返回了,拒绝访问主体,这数据保护代理的形式

有消息的时候对敏感字符进行了处理,这属于虚拟代理的模式

 

虚拟代理在控制对主体的访问时,加入了一些额外的操作

在滚动事件触发的时候,也许不需要频繁触发,我们可以引入函数节流,这是一种虚拟代理的实现

// 函数防抖,频繁操作中不处理,直到操作完成之后(再过 delay 的时间)才一次性处理
function debounce(fn, delay) {
    delay = delay || 200;
    
    var timer = null;

    return function() {
        var arg = arguments;
          
        // 每次操作时,清除上次的定时器
        clearTimeout(timer);
        timer = null;
        
        // 定义新的定时器,一段时间后进行操作
        timer = setTimeout(function() {
            fn.apply(this, arg);
        }, delay);
    }
};

var count = 0;

// 主体
function scrollHandle(e) {
    console.log(e.type, ++count); // scroll
}

// 代理
var proxyScrollHandle = (function() {
    return debounce(scrollHandle, 500);
})();

window.onscroll = proxyScrollHandle;

 

缓存代理可以为一些开销大的运算结果提供暂时的缓存,提升效率

来个栗子,缓存加法操作

// 主体
function add() {
    var arg = [].slice.call(arguments);

    return arg.reduce(function(a, b) {
        return a + b;
    });
}

// 代理
var proxyAdd = (function() {
    var cache = [];

    return function() {
        var arg = [].slice.call(arguments).join(',');
        
        // 如果有,则直接从缓存返回
        if (cache[arg]) {
            return cache[arg];
        } else {
            var ret = add.apply(this, arguments);
            return ret;
        }
    };
})();

console.log(
    add(1, 2, 3, 4),
    add(1, 2, 3, 4),

    proxyAdd(10, 20, 30, 40),
    proxyAdd(10, 20, 30, 40)
); // 10 10 100 100

 

四、迭代器模式

1. 定义

迭代器模式是指提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。

2. 核心

在使用迭代器模式之后,即使不关心对象的内部构造,也可以按顺序访问其中的每个元素

3. 实现

JS中数组的map forEach 已经内置了迭代器

[1, 2, 3].forEach(function(item, index, arr) {
    console.log(item, index, arr);
});

不过对于对象的遍历,往往不能与数组一样使用同一的遍历代码

我们可以封装一下

function each(obj, cb) {
    var value;

    if (Array.isArray(obj)) {
        for (var i = 0; i < obj.length; ++i) {
            value = cb.call(obj[i], i, obj[i]);

            if (value === false) {
                break;
            }
        }
    } else {
        for (var i in obj) {
            value = cb.call(obj[i], i, obj[i]);

            if (value === false) {
                break;
            }
        }
    }
}

each([1, 2, 3], function(index, value) {
    console.log(index, value);
});

each({a: 1, b: 2}, function(index, value) {
    console.log(index, value);
});

// 0 1
// 1 2
// 2 3

// a 1
// b 2

再来看一个例子,强行地使用迭代器,来了解一下迭代器也可以替换频繁的条件语句

虽然例子不太好,但在其他负责的分支判断情况下,也是值得考虑的

function getManager() {
    var year = new Date().getFullYear();

    if (year <= 2000) {
        console.log('A');
    } else if (year >= 2100) {
        console.log('C');
    } else {
        console.log('B');
    }
}

getManager(); // B

将每个条件语句拆分出逻辑函数,放入迭代器中迭代

function year2000() {
    var year = new Date().getFullYear();

    if (year <= 2000) {
        console.log('A');
    }

    return false;
}

function year2100() {
    var year = new Date().getFullYear();

    if (year >= 2100) {
        console.log('C');
    }

    return false;
}

function year() {
    var year = new Date().getFullYear();

    if (year > 2000 && year < 2100) {
        console.log('B');
    }

    return false;
}

function iteratorYear() {
    for (var i = 0; i < arguments.length; ++i) {
        var ret = arguments[i]();

        if (ret !== false) {
            return ret;
        }
    }
}

var manager = iteratorYear(year2000, year2100, year); // B

 

五、发布-订阅模式

1. 定义

也称作观察者模式,定义了对象间的一种一对多的依赖关系,当一个对象的状态发 生改变时,所有依赖于它的对象都将得到通知

2. 核心

取代对象之间硬编码的通知机制,一个对象不用再显式地调用另外一个对象的某个接口。

与传统的发布-订阅模式实现方式(将订阅者自身当成引用传入发布者)不同,在JS中通常使用注册回调函数的形式来订阅

3. 实现

JS中的事件就是经典的发布-订阅模式的实现

// 订阅
document.body.addEventListener('click', function() {
    console.log('click1');
}, false);

document.body.addEventListener('click', function() {
    console.log('click2');
}, false);

// 发布
document.body.click(); // click1  click2

自己实现一下

小A在公司C完成了笔试及面试,小B也在公司C完成了笔试。他们焦急地等待结果,每隔半天就电话询问公司C,导致公司C很不耐烦。

一种解决办法是 AB直接把联系方式留给C,有结果的话C自然会通知AB

这里的“询问”属于显示调用,“留给”属于订阅,“通知”属于发布

// 观察者
var observer = {
    // 订阅集合
    subscribes: [],

    // 订阅
    subscribe: function(type, fn) {
        if (!this.subscribes[type]) {
            this.subscribes[type] = [];
        }
        
        // 收集订阅者的处理
        typeof fn === 'function' && this.subscribes[type].push(fn);
    },

    // 发布  可能会携带一些信息发布出去
    publish: function() {
        var type = [].shift.call(arguments),
            fns = this.subscribes[type];
        
        // 不存在的订阅类型,以及订阅时未传入处理回调的
        if (!fns || !fns.length) {
            return;
        }
        
        // 挨个处理调用
        for (var i = 0; i < fns.length; ++i) {
            fns[i].apply(this, arguments);
        }
    },
    
    // 删除订阅
    remove: function(type, fn) {
        // 删除全部
        if (typeof type === 'undefined') {
            this.subscribes = [];
            return;
        }

        var fns = this.subscribes[type];

        // 不存在的订阅类型,以及订阅时未传入处理回调的
        if (!fns || !fns.length) {
            return;
        }

        if (typeof fn === 'undefined') {
            fns.length = 0;
            return;
        }

        // 挨个处理删除
        for (var i = 0; i < fns.length; ++i) {
            if (fns[i] === fn) {
                fns.splice(i, 1);
            }
        }
    }
};

// 订阅岗位列表
function jobListForA(jobs) {
    console.log('A', jobs);
}

function jobListForB(jobs) {
    console.log('B', jobs);
}

// A订阅了笔试成绩
observer.subscribe('job', jobListForA);
// B订阅了笔试成绩
observer.subscribe('job', jobListForB);


// A订阅了笔试成绩
observer.subscribe('examinationA', function(score) {
    console.log(score);
});

// B订阅了笔试成绩
observer.subscribe('examinationB', function(score) {
    console.log(score);
});

// A订阅了面试结果
observer.subscribe('interviewA', function(result) {
    console.log(result);
});

observer.publish('examinationA', 100); // 100
observer.publish('examinationB', 80); // 80
observer.publish('interviewA', '备用'); // 备用

observer.publish('job', ['前端', '后端', '测试']); // 输出A和B的岗位


// B取消订阅了笔试成绩
observer.remove('examinationB');
// A都取消订阅了岗位
observer.remove('job', jobListForA);

observer.publish('examinationB', 80); // 没有可匹配的订阅,无输出
observer.publish('job', ['前端', '后端', '测试']); // 输出B的岗位

4. 优缺点

优点

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

缺点

创建订阅者本身要消耗一定的时间和内存,订阅的处理函数不一定会被执行,驻留内存有性能开销

弱化了对象之间的联系,复杂的情况下可能会导致程序难以跟踪维护和理解

 

六、命令模式

1. 定义

用一种松耦合的方式来设计程序,使得请求发送者和请求接收者能够消除彼此之间的耦合关系

命令(command)指的是一个执行某些特定事情的指令

2. 核心

命令中带有execute执行、undo撤销、redo重做等相关命令方法,建议显示地指示这些方法名

3. 实现

简单的命令模式实现可以直接使用对象字面量的形式定义一个命令

var incrementCommand = {
    execute: function() {
        // something
    }
};

不过接下来的例子是一个自增命令,提供执行、撤销、重做功能

采用对象创建处理的方式,定义这个自增

// 自增
function IncrementCommand() {
    // 当前值
    this.val = 0;
    // 命令栈
    this.stack = [];
    // 栈指针位置
    this.stackPosition = -1;
};

IncrementCommand.prototype = {
    constructor: IncrementCommand,

    // 执行
    execute: function() {
        this._clearRedo();
        
        // 定义执行的处理
        var command = function() {
            this.val += 2;
        }.bind(this);
        
        // 执行并缓存起来
        command();
        
        this.stack.push(command);

        this.stackPosition++;

        this.getValue();
    },
    
    canUndo: function() {
        return this.stackPosition >= 0;
    },
    
    canRedo: function() {
        return this.stackPosition < this.stack.length - 1;
    },

    // 撤销
    undo: function() {
        if (!this.canUndo()) {
            return;
        }
        
        this.stackPosition--;

        // 命令的撤销,与执行的处理相反
        var command = function() {
            this.val -= 2;
        }.bind(this);
        
        // 撤销后不需要缓存
        command();

        this.getValue();
    },
    
    // 重做
    redo: function() {
        if (!this.canRedo()) {
            return;
        }
        
        // 执行栈顶的命令
        this.stack[++this.stackPosition]();

        this.getValue();
    },
    
    // 在执行时,已经撤销的部分不能再重做
    _clearRedo: function() {
        this.stack = this.stack.slice(0, this.stackPosition + 1);
    },
    
    // 获取当前值
    getValue: function() {
        console.log(this.val);
    }
};

再实例化进行测试,模拟执行、撤销、重做的操作

var incrementCommand = new IncrementCommand();

// 模拟事件触发,执行命令
var eventTrigger = {
    // 某个事件的处理中,直接调用命令的处理方法
    increment: function() {
        incrementCommand.execute();
    },

    incrementUndo: function() {
        incrementCommand.undo();
    },

    incrementRedo: function() {
        incrementCommand.redo();
    }
};


eventTrigger['increment'](); // 2
eventTrigger['increment'](); // 4

eventTrigger['incrementUndo'](); // 2

eventTrigger['increment'](); // 4

eventTrigger['incrementUndo'](); // 2
eventTrigger['incrementUndo'](); // 0
eventTrigger['incrementUndo'](); // 无输出

eventTrigger['incrementRedo'](); // 2
eventTrigger['incrementRedo'](); // 4
eventTrigger['incrementRedo'](); // 无输出

eventTrigger['increment'](); // 6

 

此外,还可以实现简单的宏命令(一系列命令的集合)

var MacroCommand = {
    commands: [],

    add: function(command) {
        this.commands.push(command);

        return this;
    },

    remove: function(command) {
        if (!command) {
            this.commands = [];
            return;
        }

        for (var i = 0; i < this.commands.length; ++i) {
            if (this.commands[i] === command) {
                this.commands.splice(i, 1);
            }
        }
    },

    execute: function() {
        for (var i = 0; i < this.commands.length; ++i) {
            this.commands[i].execute();
        }
    }
};

var showTime = {
    execute: function() {
        console.log('time');
    }
};

var showName = {
    execute: function() {
        console.log('name');
    }
};

var showAge = {
    execute: function() {
        console.log('age');
    }
};

MacroCommand.add(showTime).add(showName).add(showAge);

MacroCommand.remove(showName);

MacroCommand.execute(); // time age

 

七、组合模式

1. 定义

是用小的子对象来构建更大的 对象,而这些小的子对象本身也许是由更小 的“孙对象”构成的。

2. 核心

可以用树形结构来表示这种“部分- 整体”的层次结构。

调用组合对象 的execute方法,程序会递归调用组合对象 下面的叶对象的execute方法

但要注意的是,组合模式不是父子关系,它是一种HAS-A(聚合)的关系,将请求委托给 它所包含的所有叶对象。基于这种委托,就需要保证组合对象和叶对象拥有相同的 接口

此外,也要保证用一致的方式对待 列表中的每个叶对象,即叶对象属于同一类,不需要过多特殊的额外操作

3. 实现

使用组合模式来实现扫描文件夹中的文件

// 文件夹 组合对象
function Folder(name) {
    this.name = name;
    this.parent = null;
    this.files = [];
}

Folder.prototype = {
    constructor: Folder,

    add: function(file) {
        file.parent = this;
        this.files.push(file);

        return this
						

上一篇: 掌握JavaScript设计模式的10种实用方法

下一篇: 超详细解析!JS设计模式20大常用范例全攻略