首先了解一下设计原则:
单一职责原则(SRP):一个对象或一个方法只做一件事情。如果一个方法承担了过多的事情,那么在需求更改的时候,需要改写这个方法的可能性就越大。应该把对象或方法划分为更小的粒度。
最少知识原则(LKP):一个软件实体,应该尽可能少的与其他实体发生相互作用。应当尽量减少两个对象之间的交互,如果不是必要的直接关系,最好通过第三方进行处理。
开发-封闭原则(OCP):软件实体(类、函数、模块),只能为其扩展,不能更改。当需要改变一个程序的功能或为其增添新的功能时,可以通过增加代码的方式,尽量避免修改源代码,防止影响原系统的稳定。
如promise中,每一个then中做一件事,当有新的需求时,在后面添加更多的then,而不是修改之前的代码
下面介绍一些常用的设计模式
一、单例模式
确保一个类,仅有一个实例,且提供了一个全局访问点
如:有一个manager类,即使多次调用构造函数也仅创建一个manager
// 构造函数
function setManager(name) { this.manager = name; }
// 向原型上添加方法 setManager.prototype.getName = function () { console.log(this.manager); }
// 创建单例manager的方法,仅当manager不存在时,创建新的manager,最后返回 var singletonSetManager = (function () { var manager = null; return function (name) { if (!manager) { manager = new setManager(name); } return manager; }; })();
然而,以上的方法仅能实现manager单例的需求,如果此时需要实现一个hr单例需求呢?
因此,将单例的实现方法进行抽取
// 将创建单例的方法当作参数传入,单例不存在时,通过apply调用。最后将单例返回
function singletonSetInstance(fn) { var instance = null; return function () { if (!instance) { instance = fn.apply(this, arguments); } return instance; }; }
之后,对于想要创建单例的类,仅需调用以上方法,传入函数,得到关于该类的单例
function setManager(name) { this.manager = name; } setManager.prototype.getName = function () { console.log(this.manager); }; var getSingleManager = singletonSetInstance(function (name) { var manager = new setManager(name); return manager; }); function setHr(name) { this.hr = name; } setHr.prototype.getName = function () { console.log(this.hr); }; var getSingleHr = singletonSetInstance(function (name) { var hr = new setHr(name); return hr; }); getSingleManager("m1").getName(); // m1 getSingleManager("m2").getName(); // m1 getSingleHr("h1").getName(); // h1 getSingleHr("h2").getName(); // h1
二、策略模式
策略模式将一系列算法汇总到策略集中,根据不同情况进行调用。将算法的实现和使用分隔开来
一个基于策略模式的程序至少由两部分组成:
第一个部分是一组策略类,策略类封装了具体的算法,并负责具体的计算过程。
第二个部分是环境类Context,Context接受客户的请求,随后把请求委托给某一个策略类。要做到这点,说明Context 中要维持对某个策略对象的引用
例如需要根据学生的成绩等级,对应分数进行加权
var levelMap = { S: 10, A: 8, B: 6, C: 4, }; var setScore = { 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 setScore[level] ? setScore[level]() : 0; } console.log(getScore("S")); // 90 console.log(getScore("A")); // 88 console.log(getScore("B")); // 86 console.log(getScore("C")); // 84
策略模式经常用在对表单的验证中:
<script>
var errMsgs = {
default: "输入数据格式不正确",
minLength: "输入数据长度不足",
isNumber: "请输入数字",
required: "内容不能为空",
};
var rules = {
minLength: function (value, length, errMsg) {
if (value.length < length) {
return errMsg || errMsgs["minLength"];
}
},
isNumber: function (value, errMsg) {
if (!/^d+$/.test(value)) {
return errMsg || errMsgs["isNumber"];
}
},
required: function (value, errMsg) {
if (value === "") {
return errMsg || errMsgs["required"];
}
},
};
function Validators() {
this.items = [];
}
Validators.prototype = {
constructor: Validators,
add: function (value, rule, errMsg) {
var arg = [value];
if (rule.indexOf("minLength") != -1) {
var temp = rule.split(":");
arg.push(temp[1]);
rule = temp[0];
}
arg.push(errMsg);
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);
}
}
},
};
var validate = new Validators();
validate.add("111s", "isNumber", "输入内容只能是数字");
validate.add("1", "minLength:5");
validate.add("", "required");
validate.start();
</script>
三、代理模式
当客户不方便直接访问一个 对象或者不满足需要的时候,提供一个替身对象 来控制对这个对象的访问,客户实际上访问的是 替身对象。
替身对象对请求做出一些处理之后, 再把请求转交给本体对象
代理模式主要有三种:保护代理、虚拟代理、缓存代理
保护代理主要实现了访问主体的限制行为,以过滤字符作为简单的例子
// 主体,发送消息 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
五、发布-订阅模式
也称作观察者模式,定义了对象间的一种一对多的依赖关系,当一个对象的状态发 生改变时,所有依赖于它的对象都将得到通知
与传统的发布-订阅模式实现方式(将订阅者自身当成引用传入发布者)不同,在JS中通常使用注册回调函数的形式来订阅
小A在公司C完成了笔试及面试,小B也在公司C完成了笔试。他们焦急地等待结果,每隔半天就电话询问公司C,导致公司C很不耐烦。
一种解决办法是 AB直接把联系方式留给C,有结果的话C自然会通知AB
这里的“询问”属于显示调用,“留给”属于订阅,“通知”属于发布
<script>
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);
var 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") {
this.subscribes[type] = [];
return;
}
for (var i = 0; i < fns.length; ++i) {
if (fns[i] === fn) {
fns.splice(i, 1);
}
}
},
};
function jobA(jobs) {
console.log("jobList for A", jobs);
}
function jobB(jobs) {
console.log("jobList for B", jobs);
}
observer.subscribe("job", jobA);
observer.subscribe("job", jobB);
observer.subscribe("examA", function () {
console.log("100");
});
observer.subscribe("examB", function () {
console.log("99");
});
observer.subscribe("interviewA", function () {
console.log("通过");
});
observer.publish("job", ["前端开发", "设计师", "产品经理"]);
observer.publish("examA");
observer.publish("examB");
observer.publish("interviewA");
observer.remove("job", jobA);
observer.publish("job", ["咖啡师", "前台", "店长"]);
</script>
六、组合模式
1. 定义
是用小的子对象来构建更大的 对象,而这些小的子对象本身也许是由更小 的“孙对象”构成的。
2. 核心
可以用树形结构来表示这种“部分- 整体”的层次结构。
调用组合对象 的execute方法,程序会递归调用组合对象 下面的叶对象的execute方法

但要注意的是,组合模式不是父子关系,它是一种HAS-A(聚合)的关系,将请求委托给 它所包含的所有叶对象。基于这种委托,就需要保证组合对象和叶对象拥有相同的 接口
此外,也要保证用一致的方式对待 列表中的每个叶对象,即叶对象属于同一类,不需要过多特殊的额外操作
3. 实现
使用组合模式来实现扫描文件夹中的文件
<script>
function Folder(name) {
this.name = name;
this.files = [];
this.parent = null;
}
Folder.prototype = {
constructor: Folder,
add: function (file) {
// 返回this,实现链式调用
file.parent = this;
this.files.push(file);
return this;
},
scan: function () {
// 传给叶子执行
for (var i = 0; i < this.files.length; ++i) {
this.files[i].scan();
}
},
remove(file) {
// 删除全部
if (typeof file === "undefined") {
this.files = [];
return;
}
for (var i = 0; i < this.files.length; ++i) {
if (this.files[i] === file) {
this.files.splice(i, 1);
}
}
},
};
function File(name) {
this.name = name;
this.parent = null;
}
File.prototype = {
constructor: File,
add: function () {
console.log("文件中不能添加文件");
},
scan: function () {
var name = [this.name];
var parent = this.parent;
while (parent) {
name.unshift(parent.name);
parent = parent.parent;
}
console.log(name.join(" / "));
},
};
var projects = new Folder("projects");
var mainweb = new Folder("mainweb");
var center = new Folder("center");
var src = new Folder("src");
var index = new File("index.html");
var app = new File("app.js");
var main = new File("main.js");
var home = new File("home.html");
var utils = new File("utils.js");
projects.add(mainweb).add(center); // projects / center / src / index.html
center.add(src); // projects / center / src / utils.js
src.add(index).add(utils); // projects / center / app.js
center.add(app).add(main); // projects / center / main.js
projects.add(home); // projects / home.html
projects.scan();
</script>
七、命令模式
命令模式,就是将一系列命令添加到类中,通过包装实例对象的命令为对象,就可以随意通过实例对象发出命令,并按情况将命令压入栈中。通常命令模式都有Redo(重做)、undo(撤销)和execute(执行)三种命令
以下代码示例,实现了自增命令,包含撤销和重做
<script>
function Increment() {
// 自加栈为空
this.stack = [];
// 初始时,指针指向-1
this.stackPosition = -1;
// 初始值为0
this.val = 0;
}
Increment.prototype = {
// 执行命令
execute: function () {
this._cleanUedo();
// 定义自加命令
var command = function () {
this.val += 2;
}.bind(this);
// 执行
command();
// 缓存
this.stack.push(command);
// 指针后移
this.stackPosition++;
this.getValue();
},
// 判断是否可以重做
canRedo: function () {
return this.stackPosition < this.stack.length - 1;
},
canUndo: function () {
return this.stackPosition >= 0;
},
redo: function () {
if (!this.canRedo()) {
return;
}
// 执行当前指针后一位的命令
this.stack[++this.stackPosition]();
this.getValue();
},
undo: function () {
if (!this.canUndo()) {
return;
}
var command = function () {
this.val -= 2;
}.bind(this);
command();
// 撤销命令不需缓存,指针向前移一位,自加命令依然在栈中
this.stackPosition--;
this.getValue();
},
getValue() {
console.log(this.val);
},
_cleanUedo() {
// 撤销的命令不再执行
this.stack = this.stack.slice(0, this.stackPosition + 1);
},
};
var increment = new Increment();
var eventTriggle = {
execute: function () {
increment.execute();
},
undo: function () {
increment.undo();
},
redo: function () {
increment.redo();
},
};
eventTriggle.execute(); // 2
eventTriggle.execute(); // 4
eventTriggle.execute(); // 6
eventTriggle.execute(); // 8
eventTriggle.undo(); // 6
eventTriggle.undo(); // 4
eventTriggle.undo();// 2
eventTriggle.undo(); // 0
eventTriggle.undo(); // 无输出
eventTriggle.redo(); // 2
eventTriggle.redo(); // 4
eventTriggle.redo(); // 6
eventTriggle.redo(); // 8
eventTriggle.redo();// 无输出
</script>
当然,以上只针对自加命令做了处理,当命令增多时,可以将命令抽取,调用execute时,将命令当作参数传入,压入栈中。如下:
<script>
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 () {
if (this.commands.length === 0) {
return;
}
for (var i = 0; i < this.commands.length; i++) {
this.commands[i].execute();
}
},
};
var showName = {
execute: function () {
console.log("ashen");
},
};
var showGendle = {
execute: function () {
console.log("female");
},
};
MacroCommand.add(showName).add(showGendle);
MacroCommand.execute();
</script>
八、模板方法模式
模板方法模式由抽象父类和具体的实现子类组成
在抽象父类中封装子类的算法框架,它的 init方法可作为一个算法的模板,指导子类以何种顺序去执行哪些方法。
由父类分离出公共部分,要求子类重写某些父类的(易变化的)抽象方法
模板方法模式一般的实现方式为继承
以运动为例,如下
<script>
function Sport() {}
Sport.prototype = {
init: function () {
this.strech();
this.jog();
this.deepBreath();
this.start();
this.free = this.end();
if (this.free) {
this.strech();
}
},
strech: function () {
console.log("先拉伸一下肌肉");
},
jog: function () {
console.log("再慢跑一会,热热身");
},
deepBreath: function () {
console.log("跑完深呼吸~");
},
start: function () {
throw new Error("子类必须改写此方法");
},
end: function () {
console.log("运动结束");
},
};
function Run() {}
Run.prototype = new Sport();
Run.prototype.start = function () {
console.log("每天跑个半小时");
};
Run.prototype.end = function () {
console.log("跑完要回去虐腹,先走啦!");
return false;
};
function Zumba() {}
Zumba.prototype = new Sport();
var run = new Run();
var zumba = new Zumba();
run.init();
zumba.init();
</script>
执行结果如下:

九、享元模式
享元(flyweight)模式是一种用于性能优化的模式,“fly”在这里是苍蝇的意思,意为蝇量级。享元模式的核心是运用共享技术来有效支持大量细粒度的对象。如果系统中因为创建了大量类似的对象而导致内存占用过高,享元模式就非常有用了。在javascript中,浏览器特别是移动端的浏览器分配的内存并不算多,如何节省内存就成了一件非常有意义的事情。
十、职责链模式
使多个对象都有机会处理请求,从而避免请求的发送者和请求的接收者之间的耦合关系,将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。
请求发送者只需要知道链中的第一个节点,弱化发送者和一组接收者之间的强联系,可以便捷地在职责链中增加或删除一个节点,同样地,指定谁是第一个节点也很便捷。
如下,实现了一个简单的判断数据类型的职责链
<script>
function ChainItem(fn) {
this.fn = fn;
this.next = null;
}
ChainItem.prototype = {
constructor: ChainItem,
setNext: function (next) {
this.next = next;
return next;
},
start: function () {
this.fn.apply(this, arguments);
},
toNext: function () {
if (this.next) {
this.start.apply(this.next, arguments);
} else {
console.log("无匹配的执行项目");
}
},
};
function showNumber(num) {
if (typeof num === "number") {
console.log("number", num);
} else {
this.toNext(num);
}
}
function showString(str) {
if (typeof str === "string") {
console.log("string", str);
} else {
this.toNext(str);
}
}
function showObject(obj) {
if (typeof obj === "object") {
console.log("object", obj);
} else {
this.toNext(obj);
}
}
var numberItem = new ChainItem(showNumber);
var stringItem = new ChainItem(showString);
var objectItem = new ChainItem(showObject);
numberItem.setNext(stringItem).setNext(objectItem);
numberItem.start({ name: "ashen" }); // object {name: "ashen"}
objectItem.start("str"); // 无匹配的执行项目
</script>
当需要向其中加入判断是否undefined也很容易,如下
function showUndefined(un) { if (typeof un === "undefined") { console.log("undefined", un); } else { this.toNext(un); } } var undefinedItem = new ChainItem(showUndefined); objectItem.setNext(undefinedItem); // 可以添加到任意一个节点后 numberItem.start(); // 可以从任意节点开始 undefined undefined
十一、中介者模式
<script>
var A = {
score: 100,
changeTo: function (score) {
this.score = score;
this.getRank();
},
getRank: function () {
var scores = [this.score, B.score, C.score].sort((a, b) => {
return a < b;
});
console.log(scores.indexOf(this.score) + 1);
},
};
var B = {
score: 90,
changeTo: function (score) {
this.score = score;
rankMediator(B);
},
};
var C = {
score: 80,
changeTo: function (score) {
this.score = score;
rankMediator(C);
},
};
function rankMediator(person) {
var scores = [A.score, B.score, C.score].sort();
console.log(scores);
console.log(scores.indexOf(person.score) + 1);
}
A.changeTo(120);
B.changeTo(150);
C.changeTo(130);
</script>
以上例子中,A 通过自身的函数,拿到B、C的成绩进行排名,而B和C通过中介者rankMediator进行排名,减少了多对象间的相互引用
十二、装饰者模式
var person = { name: 'ashen', } function decorator(){ console.log(person.name + '1999'); }
还可以通过传统的面向对象实现
function Person() {} Person.prototype.skill = function () { console.log("唱歌"); }; function CodeDecorator(person) { this.person = person; } CodeDecorator.prototype.skill = function () { this.person.skill(); console.log("敲代码"); }; function DanceDecorator(person) { this.person = person; } DanceDecorator.prototype.skill = function () { this.person.skill(); console.log("跳舞"); }; var person = new Person(); var person1 = new Person(); person1 = new CodeDecorator(person1); person1 = new DanceDecorator(person1); person1.skill(); // 唱歌 敲代码 跳舞
在JS中,函数为一等对象,所以我们也可以使用更通用的装饰函数
function decorateBefore(fn, beforeFn) { return function () { var ret = beforeFn.apply(this, arguments); if (ret !== false) { fn.apply(this, arguments); } }; } function skill() { console.log("说话"); } function skillEat() { console.log("吃饭"); } function skillSleep() { console.log("睡觉"); } var getSkill = decorateBefore(skill, skillEat); getSkill = decorateBefore(getSkill, skillSleep); getSkill(); // 睡觉 吃饭 说话
十三、适配器模式
是解决两个软件实体间的接口不兼容的问题,对不兼容的部分进行适配。
例如下面的数据类型转换的适配器
<script>
// 限制只能传入数组
function render(data) {
data.forEach((item) => {
console.log(item);
});
}
// 数据格式适配器
function adapter(data) {
if (typeof data !== "object") {
// 数据不可迭代
return [];
}
// 如果是数组,直接返回
if (Object.prototype.toString.call(data) === "[Object Array]") {
return data;
}
// 如果是对象,进行迭代,转换为数组
var temp = [];
for (var item in data) {
if (data.hasOwnProperty(item)) {
temp.push(data[item]);
}
}
return temp;
}
var data = {
name: "ashen",
age: 21,
gender: "female",
};
var str = "asharren";
var arr = ["一小", "三中", "一中"];
render(adapter(data));
render(adapter(str));
render(adapter(arr));
</script>
