阅读 489

javascript设计模式与应用

javascript设计模式与应用

目录

前言

设计模式真的很多很复杂,建议有兴趣的看我文章最后的链接去学习,要想掌握和熟练应用到项目中绝对不是一蹴而就的,我这篇文章顶多就是一个入门级别的学习,让大家对设计模式有个概念,我讲的也非常简单,代码太长我自己都懒得看,所以尽量举简单的例子,说实话,设计模式我啃得也很痛苦,说多了都是泪,哈哈!最后说明一下,文章并未列出23种设计模式,我只按照我觉得重要和使用多的讲了上面11个,也并非所有的模式都写了实现代码,后续有时间和必要的话,可能还会更新其他的模式。

设计模式

构造函数模式

用构造函数来生成对象

// 实例共享的方法定义在原型上,实例本身的属性定义在构造函数里面
function Car( model, year, miles ) { 
  this.model = model;
  this.year = year;
  this.miles = miles; 
}
// 这里注意要在原型上添加方法,而不是给原型赋值,不然就会丢失原型
Car.prototype.toString = function () {
  return this.model + " has done " + this.miles + " miles";
};
// Usage:
var civic = new Car( "Honda Civic", 2009, 20000 );
var mondeo = new Car( "Ford Mondeo", 2010, 5000 );
 
console.log( civic.toString() );
console.log( mondeo.toString() );
复制代码

工厂模式(Factory)

  • 定义: 工厂模式是用来创建对象的一种最常用的设计模式。我们不暴露创建对象的具体逻辑,而是将将逻辑封装在一个函数中,那么这个函数就可以被视为一个工厂。工厂模式根据抽象程度的不同可以分为:简单工厂,工厂方法和抽象工厂。除非项目很复杂,否则一般用不到工厂方法和抽象方法
  • 应用:jQuery的$选择器就是用工厂模式创建的
    // 简单模拟一下jQuery的实现
    class jQuery {
    constructor(selector) {
        let slice = Array.prototype.slice
        let dom = slice.call(document.querySelectorAll(selector))
        let len = dom.length? dom.length: 0
        for (let i = 0; i < len; i++) {
            this[i] = dom[i]          
        }
        this.length = len
        this.selector = selector
    }
    addClass() {    
    }
    ...
}
// $就是一个工厂
window.$ = function(selector) {
    return new jQuery(selector)
}
复制代码

单例模式(Singleton)

  • 系统中只能有一个实例,一个类只能创建一个实例, 比如登陆框,购物车, vuex和redux的store也是单例
  • 来实现一个单例
class Singleton {
    login() {

    }
}
Singleton.getInstance = (function() {
    let instance
    return function() {
        if(!instance) {
            instance = new Singleton()
        }
        return instance
    }
})()
let obj1 = Singleton.getInstance()
let obj2 = Singleton.getInstance()
console.log(obj1 === obj2) // true
复制代码

代理模式(Proxy)

  • 使用者无权访问目标对象, 为其他对象提供一种代理以控制对这个对象的访问, 接口不变
  • 网页事件代理,jQuery.$.proxy, es6的Proxy
  • es6的代理proxy
  • 在 Vue3.0 中将会通过 Proxy 来替换原本的 Object.defineProperty 来实现数据响应式, 接下来我们自己用Proxy实现一下吧
let onWatch = (obj, setBind, getLogger) => {
  let handler = {
    get(target, property, receiver) {
      getLogger(target, property);
      return Reflect.get(target, property, receiver);
    },
    set(target, property, value, receiver) {
      setBind(value, property);
      return Reflect.set(target, property, value);
    }
  };
  return new Proxy(obj, handler);
};

let obj = { a: 1 };
let p = onWatch(
  obj,
  (v, property) => {
    console.log(`监听到属性${property}改变为${v}`);
  },
  (target, property) => {
    console.log(`'${property}' = ${target[property]}`);
  }
);
p.a = 2; // 监听到属性a改变
p.a; // 'a' = 2

复制代码

观察者模式(Observer)

  • 主题和观察者分离,当主题更新的时候,通知所有的观察者更新自己
  • 应用:vue的响应式,node的eventEmitter, 我们来实现一个简单的EventEmitter
  • 观察者模式和发布订阅者模式还是有点区别的,在这里不做区分了
class EventEmitter {
  constructor() {
    this._events = {}; // 维护订阅者列表
  }
  // 订阅主题
  on(name, fn) {
    if (name in this._events) {
      // 避免重复订阅
      if(!this._events[name].find(f => f === fn)) {
        this_events[name].push(fn);
      }
    } else {
      this._events[name] = [];
      this._events[name].push(fn);
    }
  }
  // 发布主题,相关主题的订阅者更新
  emit(name, ...arg) {
    if (name in this._events) {
      let events = this._events[name];
      for (let i = 0; i < events.length; i++) {
        events[i](...arg);
      }
    }
  }
  // 取消订阅
  off(name, fn) {
    if (name in this._events) {
      let index = this._events[name].findIndex(f => f === fn);
      if (index > -1) {
        this._events[name].splice(index, 1);
      }
    }
  }
}
let event = new EventEmitter();
function subFn(data) {
  console.log(data);
}
// 订阅主题
event.on("vue", subFn);
// 发布通知
event.emit("vue", "vue3.0要出来了"); // "vue3.0要出来了"
// 取消订阅
event.off("vue", subFn);
// 再发布通知,就不会打印了
event.emit("vue", "vue3.0马上要出来了");

复制代码

适配器模式(Adaptor)

  • 适配器模式(Adapter)是将一个类(对象)的接口(方法或属性)转化成客户希望的另外一个接口(方法或属性),适配器模式使得原本由于接口不兼容而不能一起工作的那些类(对象)可以一些工作。
  • 旧接口和用户分离

装饰器模式(Decorator)

  • 为对象添加新功能,不改变原有的结构和功能, 优点是把类(函数)的核心职责和装饰功能区分开了
  • ES7已经有了装饰器

阮一峰的ES6教程中对装饰器讲的很好很全面,大家可以去看看,链接在此(es6.ruanyifeng.com/#docs/decor…)

迭代器模式(Iterator)

  • 提供一种方法顺序一个聚合对象中各个元素,而又不暴露该对象内部表示。
  • 迭代器的几个特点是:
    • 访问一个聚合对象的内容而无需暴露它的内部表示。
    • 为遍历不同的集合结构提供一个统一的接口,从而支持同样的算法在不同的集合结构上进行操作。
    • 遍历的同时更改迭代器所在的集合结构可能会导致问题(比如C#的foreach里不允许修改item。
  • 应用
    • es6的Iterator es6的有序数据集合(Array, string, Map, Set, generator等)都部署了[Symbol.iterator]属性, 这个属性是一个方法,返回一个迭代器,因此都可以用for...of遍历
    • jQuery里一个非常有名的迭代器就是$.each方法,通过each我们可以传入额外的function,然后来对所有的item项进行迭代操作,例如:
$.each(['dudu', 'dudu', '酸奶小妹', '那个MM'], function (index, value) {
    console.log(index + ': ' + value);
});
//或者
$('li').each(function (index) {
    console.log(index + ': ' + $(this).text());
});
复制代码

外观模式(Facade)

  • 定义
    • 为子系统中的一组接口提供了一个一致的界面,此模块定义了一个高层接口,这个接口使得这一子系统更加容易使用。
  • 特点
    • 外观模式不仅简化类中的接口,而且对接口与调用者也进行了解耦。外观模式经常被认为开发者必备,它可以将一些复杂操作封装起来,并创建一个简单的接口用于调用。

    • 外观模式经常被用于JavaScript类库里,通过它封装一些接口用于兼容多浏览器,外观模式可以让我们间接调用子系统,从而避免因直接访问子系统而产生不必要的错误。

    • 外观模式的优势是易于使用,而且本身也比较轻量级。但也有缺点 外观模式被开发者连续使用时会产生一定的性能问题,因为在每次调用时都要检测功能的可用性

  • js中ie浏览器的事件api和其他浏览器的不同,为了兼容,我们一般都会封装一个统一的事件处理函数
var addMyEvent = function (el, event, fn) {
    if (el.addEventListener) {
        el.addEventListener(event, fn, false);
    } else if (el.attachEvent) {
        el.attachEvent('on' + event, fn);
    } else {
        el['on' + event] = fn;
    }
}; 
复制代码

状态模式(State)

命令模式(Command)

  • 命令模式(Command)的定义是:用于将一个请求封装成一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或者记录请求日志,以及执行可撤销的操作。也就是说改模式旨在将函数的调用、请求和操作封装成一个单一的对象,然后对这个对象进行一系列的处理。此外,可以通过调用实现具体函数的对象来解耦命令对象与接收对象。

我们现在有一个汽车管理类, 如下

(function() {
  var carManager = {
    // 获取汽车的信息
    requestInfo: function(model, id) {
      return "The information for " + model + " with ID " + id + " is foobar";
    },
    // 购买汽车
    buyVehicle: function(model, id) {
      return "You have successfully purchased Item " + id + ", a " + model;
    },
    // 组织车展
    arrangeViewing: function(model, id) {
      return (
        "You have successfully booked a viewing of " +
        model +
        " ( " +
        id +
        " ) "
      );
    }
  }
})();
// 调用方法
carManager.requestInfo( "Ferrari", "14523" );
carManager.buyVehicle( "Ford Mondeo", "54323" );
carManager.arrangeViewing("Ford Escort", "34232" );
复制代码

然而在一些情况下,我们并不想直接调用对象内部的方法。这样会增加对象与对象间的依赖。现在我们来扩展一下这个CarManager, 使其能够接受任何来自包括model和car ID 的CarManager对象的处理请求。根据命令模式的定义,我们希望实现如下这种功能的调用:

CarManager.execute({ commandType: "buyVehicle", operand1: 'Ford Escort', operand2: '453543' });
复制代码

根据这样的需求,我们可以这样实现CarManager.execute方法:

CarManager.execute = function (command) {
    return CarManager[command.request](command.model, command.carID);
};
复制代码

改造以后,调用就简单多了,如下调用都可以实现

CarManager.execute({ request: "arrangeViewing", model: 'Ferrari', carID: '145523' });
CarManager.execute({ request: "requestInfo", model: 'Ford Mondeo', carID: '543434' });
CarManager.execute({ request: "requestInfo", model: 'Ford Escort', carID: '543434' });
CarManager.execute({ request: "buyVehicle", model: 'Ford Escort', carID: '543434' });
复制代码

参考资料:

关注下面的标签,发现更多相似文章
评论