笔记-11-JavaScript设计模式
设计原则:
- 单一职责原则:一个类,应该仅有一个引起它变化的原因,简而言之就是功能要单一;
- 开放封闭原则:对拓展开放,对修改关闭;
- 接口隔离原则:一个接口应该是一种角色,不该干的事情不要干,该干的都要干,简而言之就是降低耦合、减低依赖;
工厂模式
工厂模式是由一个方法来决定到底创建哪个类的实例,而这些类经常拥有相同的接口。这种模式主要用在所实例化的类型在编译期不能确定,而是在执行期决定的情况。
分为简单工厂和工厂方法
简单工厂是将创建对象的步骤放在父类进行,工厂方法是延迟到子类中进行,它们两者都可以总结为:根据传入的字符串来选择对应的类
简单工厂
var UserFactory = function(role) {
function Admin() {
this.name = "管理员";
this.viewPage = ['首页', '查询', '权限管理'];
}
function User() {
this.name = "普通用户";
this.viewPage = ['首页', '查询'];
}
switch(role) {
case 'admin':
return new Admin();
break;
case 'user':
return new User();
break;
default:
throw new Error('参数错误,可选参数:admin、user');
}
}
var admin = UserFactory('admin');
var user = UserFactory('user')
工厂方法
// 安全模式创建的工厂方法函数
var UserFactory = function(role) {
if(this instanceof UserFactory) {
return new this[role]();
} else {
return new UserFactory(role);
}
}
// 工厂方法函数的原型中设置所有对象的构造函数
UserFactory.prototype = {
Admin: function() {
this.name = "管理员";
},
User: function() {
this.name = "用户";
}
}
// 调用
var admin = UserFactory('Admin');
var user = UserFactory('User');
构造器模式
在面向对象的编程语言中,构造器是一个类中用来初始化新对象的特殊方法。并且可以接受参数用来设定实例对象的属性的方法。
利用原型链上被继承的特性,实现了构造器:
function Car(model, year, miles) {
this.model = model
this.year = year
this.miles = miles
}
// 覆盖原型对象上的toString
Car.prototype.toString = function() {
return `${this.model} has done ${this.miles} miles`
}
// 使用
var civic = new Car("Honda Civic", 2009, 20000)
var mondeo = new Car("Ford Mondeo", 2010, 50000)
单例模式
保证一个类仅有一个实例,并提供一个访问它的全局访问点。例如页面中的登陆弹窗、全局缓存等。
案例:假设要设置一个管理员,多次调用也仅设置一次,我们可以使用闭包缓存一个内部变量来实现这个单例。
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
原型模式
用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象,在JS中实现原型模式是在ECMAScript5中,提出的Object.create方法,使用现有的对象来提供新创建的对象的隐式原型(proto)
案例:使用现有的对象来提供创建的对象proto
var prototype = {
name: "Jack",
getName: function() {
return this.name;
},
};
var obj = Object.create(prototype, {
job: {
value: "IT",
},
});
console.log(obj.getName()); // Jack
console.log(obj.job); // IT
console.log(obj.__proto__ === prototype); //true
发布-订阅模式
发布-订阅模式,它定义了对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。在JS中通常使用注册回调函数的形式来订阅
优点:一为时间上的解耦,二为对象间解耦,可以用在异步编程中。
缺点:创建订阅者本身要消耗一定的时间和内存,订阅的处理函数不一定会被执行,驻留内存有性能开销,弱化了对象之间的联系,复杂的情况下可能会增加代码的可维护性。
class EventEmitter {
constructor() {
this.events = {};
}
// 订阅事件
on(eventName, callback) {
if (!this.events[eventName]) {
this.events[eventName] = [];
}
this.events[eventName].push(callback);
}
// 触发事件
emit(eventName, ...args) {
if (this.events[eventName]) {
this.events[eventName].forEach((callback) => callback(...args));
}
}
// 取消订阅事件
off(eventName, callback) {
if (this.events[eventName]) {
this.events[eventName] = this.events[eventName].filter(
(cb) => cb !== callback
);
}
}
}
// 使用示例
const eventEmitter = new EventEmitter();
eventEmitter.on("message", (data) => {
console.log("Received message:", data);
});
eventEmitter.emit("message", "Hello, World!"); // 输出: Received message: Hello, World!
适配器模式
它的作用是解决两个软件实体间的接口不兼容的问题。使用适配器模式后,原本由于接口不兼容而不能工作的两个软件实体可以一起工作。
适配器的别名是包装器(wrapper),这是一个相对简单的模式。在程序开发中有许多这样的场景:当我们试图调用模块或者对象的某个接口时,却发现这个接口的格式并不符合当前的需求。这时候有两种解决办法,第一种是修改原来的接口实现,但如果原来的模块很复杂,或者我们拿到的模块时一段别人编写的经过压缩的代码,修改原接口就显得太不现实了。第二种办法是创建一个适配器,将原接口转换为客户希望的另一个接口,客户只需要和适配器打交道。
实现一个简单的数据格式转换适配器:
// 渲染数据,格式限制为数组了
function renderData(data) {
data.forEach(function(item) {
console.log(item);
});
}
// 对非数组的进行转换适配
function arrayAdapter(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 = {
0: 'A',
1: 'B',
2: 'C'
};
renderData(arrayAdapter(data)); // A B C
装饰器模式
以动态地给某个对象添加一些额外的职责,而不会影响从这个类中派生的其他对象。
是一种“即用即付”的方式,能够在不改变对象自身的基础上,在程序运行期间给对象动态的添加职责。
为对象动态的加入行为,经过多重包装,可以形成一条装饰链。
最简单的装饰者,就是重写对象的属性
function Person() {}
Person.prototype.skill = function() {
console.log('数学');
};
// 装饰器,还会音乐
function MusicDecorator(person) {
this.person = person;
}
MusicDecorator.prototype.skill = function() {
this.person.skill();
console.log('音乐');
};
// 装饰器,还会跑步
function RunDecorator(person) {
this.person = person;
}
RunDecorator.prototype.skill = function() {
this.person.skill();
console.log('跑步');
};
var person = new Person();
// 装饰一下
var person1 = new MusicDecorator(person);
person1 = new RunDecorator(person1);
person.skill(); // 数学
person1.skill(); // 数学 音乐 跑步
代理模式
当客户不方便直接访问一个对象或者不满足需要的时候,提供一个替身对象来控制对这个对象的访问,客户实际上访问的是替身对象。替身对象对请求作出一些处理后,再把请求转交给本体对象。代理和本体的接口具有一致性,本体定义了关键功能,而代理是提供或拒绝对它的访问,或者在访问本体之前做一些额外的事情。
代理模式主要有三种:保护代理,虚拟代理,缓存代理
保护代理主要实现了访问主体的限制行为,以过滤字符作为简单的例子:
// 主体,发送消息
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
外观模式
为子系统中的一组接口提供一个一致的页面,定义一个高层接口,这个接口使子系统更加容易使用。
可以通过请求外观接口来达到访问子系统,也可以选择越过外观来直接访问子系统。
外观模式在JS中,可以认为是一组函数的集合
// 三个处理函数
function start() {
console.log('start');
}
function doing() {
console.log('doing');
}
function end() {
console.log('end');
}
// 外观函数,将一些处理统一起来,方便调用
function execute() {
start();
doing();
end();
}
// 调用init开始执行
function init() {
// 此处直接调用了高层函数,也可以选择越过它直接调用相关的函数
execute();
}
init(); // start doing end
迭代器模式
迭代器模式是指提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。迭代器模式可以把迭代的过程从业务逻辑中分离出来,在使用迭代器模式后,即使不关心对象的内部构造,也可以按顺序访问其中的每个元素。
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