在写业务时,往封装性、扩展性、灵活性上靠,能带来好的代码结构,但23种设计模式和js技巧让人眼花缭乱。这里以具体的场景总结个人觉得容易用的。欢迎补充指正。
1. getter和setter
可以劫持对某个属性的存取行为。可以只定义一个,也可以都定义。
- 场景:对某个值只能读,不能写。只提供getter就好。
- 场景:设置值时,需要做其他操作,比如数据转换等。
// es5写法
function Add(){this._num = 0;};
Add.prototype = {
// 有人肯定会说,这不是覆盖了,constructor咋办
// constructor不会影响原型链,如果需要识别对象是否由Add初始化,可以设定为Add
get num(){
return this._num;
},
set num(param){
this._num = param;
}
}
// es6写法
class Add{
constructor(){
this._num = 0;
}
get num() {
return this._num;
}
set num(param) {
this._num = param;
}
}
2. 扩展某个函数
- 场景: promise发生错误时,执行catch,并在其中进行监控上报。
const _catch = Promise.prototype.catch;
Promise.prototype.catch = (function (param) {
return function (rejected) {
// 暂存原函数
const _inject = rejected || function () {};
rejected = reason => {
if (reason instanceof Error) {
// 插入的操作:错误上报
}
return _inject(reason);
};
return param.apply(this, [rejected]);
}
})(_catch);
- 场景:某个函数前后打印日志。这样写就不用我们在每处调用前后都写console了
const patch = {
getDate: function(){}
}
const next = patch.getDate;
patch.getDate = function getNewDate(param) {
console.log('getNewDate start:');
next(param);
console.log('getNewDate end:');
}
- 场景:给任意一个对象的函数,增加执行前后打log功能
//接上面的代码
function logger(obj,'getDate') {
const objFun = obj.getDate;
return function _getDate(param){
console.log('getNewDate start:');
const result = objFun && objFun(param);
console.log('getNewDate end:');
return result;
}
}
// 执行时使用:
logger(patch,'getDate');
3. 面向切面编程(AOP)
- 场景:在不破坏原有代码(函数)的情况下增加新功能。
const talk = (...args) => {
console.log('这是掘金');
}
Function.prototype.before = function(cb) {
return (...args) => {
cb && cb();
this(...args); // 此处...是展开不是剩余运算,this指talk
}
}
let newTalk = talk.before(function(){
console.log('准备写了')
});
newTalk('你好');
4. 函数柯理化
- 柯里化是一种函数的转换,它是指将一个函数从可调用的 f(a, b, c) 转换为可调用的 f(a)(b)(c) 。在很多源码中有大量的应用。
- 场景:通用简介的类型判断方法
function checkType(type) {
return function(content) {
return Object.prototype.toString.call(content) === `[object ${type}]`
}
}
// 灵活的产生函数,重复利用逻辑
let isString = checkType('String');
let d = isString('aaa');
- 场景:可以分次传递参数的执行函数
const addNum = (a,b,c,d) =>{
return a+b+c+d
}
// 柯理化
const curring = (fn, arr=[]) => {
let len = fn.length; // 函数参数个数
return (...args) =>{
// 保存用户传入的参数
arr = [...arr,...args];
if(arr.length < len){
return curring(fn,arr);
}
return fn(...arr);
}
}
console.log(curring(addNum)(1,2)(3)(4));
5. 异步并发完成监控(通常用计数器解决)
- 场景:异步去读取两个文件,读取全部完成执行某一操作。这类问题一般都用计数器来解决。
const fs = require('fs');
function after(times,say){
let obj = {};
return function(key ,value){
obj[key] = value;
if(--times === 0){
say(obj);
}
}
}
let finish = after(2,(obj)=>{
console.log(obj)
});
fs.readFile('./name.txt','utf8',function(err,data){
finish('name',data);
});
fs.readFile('./age.txt','utf8',function(err,data){
finish('age',data);
});
6. 发布订阅模式
- 场景:前端通信的必备
- 备注:在多页面调试时,引用同一个eventcenter可能被其他页面触发监听的事件,可以在当前页面单独建一个eventcenter文件。
// 伪代码
const REG_EVENTS = {};
let e = {
on(eventName, handler) {
const handlers = REG_EVENTS[eventName] || (REG_EVENTS[eventName] = []);
handlers.push({ func: handler, scope, isOnce });
},
emit(eventName, ...args: any[]) {
const handlers = REG_EVENTS[eventName];
handlers.forEach((i) => {
i.func.apply(i.scope, args);
});
}
}
e.on('loaded',() => {
console.log('loaded');
});
e.on('loading',() => {
console.log('loading');
});
e.emit('loaded');
7. 观察者模式
- 场景:单个应用中进行通知
- 与发布订阅的区别:
- 发布订阅模式里,发布者和订阅者,是完全解耦的。而观察者是松耦合。
- 它只需维护一套观察者(Observer)的集合,这些Observer实现相同的接口,Subject通知Observer时,调用那个统一方法就好。
- 发布订阅有一个消息中心(经纪人)
// 发布者
function Publisher() {
this.listeners = [];
}
Publisher.prototype = {
'addListener': function(listener) {
this.listeners.push(listener);
},
'removeListener': function(listener) {
delete this.listeners[listener];
},
'notify': function(obj) {
for(var i = 0; i < this.listeners.length; i++) {
var listener = this.listeners[i];
if (typeof listener !== 'undefined') {
listener.process(obj);
}
}
}
};
// 订阅者
function Subscriber() {
}
Subscriber.prototype = {
'process': function(obj) {
console.log(obj);
}
}; 
var publisher = new Publisher();
publisher.addListener(new Subscriber());
publisher.addListener(new Subscriber());
publisher.notify({name: 'michaelqin', ageo: 30}); // 发布一个对象到所有订阅者
publisher.notify('process'); // 发布一个字符串到所
8. 单例模式
旨在保证一个类仅有一个实例,并提供一个全局的访问点。我觉得没必要背记所谓的简单、惰性、透明、代理版的单例模式,应该迎合业务去修改。
• 场景:生成全局唯一的遮罩
• 场景:比如飞机大战游戏里的主角飞机
const single = (function() {
var unique;
function getInstance() {
if (unique === undefined) {
unique = new Construct();
}
return unique;
}
function Construct() {
// 生成单例的构造函数的代码
}
return {
getInstance: getInstance
}
})();
const singleInstance = single.getInstance();
9. js hook
- 场景:从映射表取值,代替多if判断
10. 中间件机制,洋葱圈模型
- 执行某个函数/操作时,按照顺序执行我们配置的其他操作。
这个场景可能不多,一般的扩展直接修改函数即可,但如果这种扩展性操作非常频繁和多,直接改函数将非常庞大(脏)。
使用中间件思想(洋葱圈模型)就可以解决这种问题。这种机制可以在某个操作发生的前后灵活的添加其他操作,具有扩展性和灵活性。用一张图来形容:
// 加入中间件
const applyMiddleware = (obj, funName, middlewares) => {
middlewares = [ ...middlewares ] //浅拷贝
middlewares.reverse() //循环使得最先添加的操作最后执行,所以要做个反转
middlewares.forEach(middleware =>
obj[funName] = middleware(obj[funName]);
)
}
// 中间件示例:比如用来在执行前后打log
function middle1(func) {
let next = func;
return function _middle1(param) {
console.log('_middle1 start');
let result = next(param);
console.log('_middle1 end');
return result;
};
}
// 使用
const patch = {
getDate: function(){console.log('这是一个程序员')}
}
applyMiddleware(patch, 'getDate', [middle1]);
patch.getDate();
11. 装饰器
- 定义:装饰器是一个对类进行处理的函数,允许向一个现有的对象添加新的功能,同时又不改变其结构。这种类型的设计模式属于结构型模式。
- 原理:利用JS中object的descriptor
- 使用:装饰器是es7的语法,需要babel编译。
- 场景:使原型中的某个方法,不可修改(修改类)
@noWritable('say')
class Animal {
say() {}
}
function noWritable(funName) {
return function(constructor){
let descriptor = Object.getOwnPropertyDescriptor(constructor.prototype, funName)
Object.defineProperty(constructor.prototype, funName, {
...descriptor,
writable: false
});
}
}
- 场景:给某个函数的执行增加log(修改方法)
class Num {
@log
add(a, b) {
return a + b;
}
}
function log(target, name, descriptor) {
var initial = descriptor.value;
descriptor.value = function() {
console.log(`${name} doing`, arguments);
return initial.apply(this, arguments);
};
return descriptor;
}