Setcina(目前还在更新以及完善中...)

关于JS的设计模式那些事儿

2020-04-09

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

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

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

设计原则

单一职责原则(SRP)

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

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

最少知识原则(LKP)

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

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

开放-封闭原则(OCP)

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

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

什么是设计模式

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

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

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

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

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

单例模式

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

  2. 核心: 确保只有一个实例,并提供全局访问

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    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

策略模式

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

  2. 核心:

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

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

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

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

  1. 实现
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    // 加权映射关系
    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);

代理模式

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

  2. 核心

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

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
// 主体,发送消息
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;

迭代器模式

  1. 定义

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

  1. 核心

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

  1. 实现
    JS中数组的map forEach 已经内置了迭代器
    1
    2
    3
    [1, 2, 3].forEach(function(item, index, arr) {
    console.log(item, index, arr);
    });

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

我们可以封装一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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

发布-订阅模式

  1. 定义

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

  1. 核心

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

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

  1. 实现

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

自己实现一下

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
// 观察者
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的岗位

模板方法模式

  1. 定义

模板方法模式由两部分结构组成,第一部分是抽象父类,第二部分是具体的实现子类。

  1. 核心

在抽象父类中封装子类的算法框架,它的 init方法可作为一个算法的模板,指导子类以何种顺序去执行哪些方法。

由父类分离出公共部分,要求子类重写某些父类的(易变化的)抽象方法

  1. 实现

模板方法模式一般的实现方式为继承

以运动作为例子,运动有比较通用的一些处理,这部分可以抽离开来,在父类中实现。具体某项运动的特殊性则有自类来重写实现。

最终子类直接调用父类的模板函数来执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
// 体育运动
function Sport() {

}

Sport.prototype = {
constructor: Sport,

// 模板,按顺序执行
init: function() {
this.stretch();
this.jog();
this.deepBreath();
this.start();

var free = this.end();

// 运动后还有空的话,就拉伸一下
if (free !== false) {
this.stretch();
}

},

// 拉伸
stretch: function() {
console.log('拉伸');
},

// 慢跑
jog: function() {
console.log('慢跑');
},

// 深呼吸
deepBreath: function() {
console.log('深呼吸');
},

// 开始运动
start: function() {
throw new Error('子类必须重写此方法');
},

// 结束运动
end: function() {
console.log('运动结束');
}
};

// 篮球
function Basketball() {

}

Basketball.prototype = new Sport();

// 重写相关的方法
Basketball.prototype.start = function() {
console.log('先投上几个三分');
};

Basketball.prototype.end = function() {
console.log('运动结束了,有事先走一步');
return false;
};


// 马拉松
function Marathon() {

}

Marathon.prototype = new Sport();

var basketball = new Basketball();
var marathon = new Marathon();

// 子类调用,最终会按照父类定义的顺序执行
basketball.init();
marathon.init();

扫描二维码,分享此文章