js技巧场景举例

208 阅读2分钟

    在写业务时,往封装性、扩展性、灵活性上靠,能带来好的代码结构,但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);
  }
};&emsp;
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;
}