《JavaScript 设计模式》脱水版

面向对象

链式调用

  1. 在原型上添加方法

    //类式定义
    //**区别**
    Function.prototype.addMethods = function (name, fn) {
        this.prototype[name]=fn;
        return this;
    }
    //添加方法
    var Methods = function(){};
    Methods.addMethods('checkName', function () {})
     .addMethods('checkEmail', function () {});
    //使用
    //**区别**
    var md = new Methods();
    md.checkName();
    
  2. 在对象本身添加方法

    //函数式定义
    Function.prototype.addMethods = function (name, fn) {
        //**区别**
        this[name]=fn;
        return this;
    }
    //添加方法
    var methods = function(){};
    Methods.addMethods('checkName', function () {})
     .addMethods('checkEmail', function () {});
    //使用
    //**区别**
    methods.checkName();
    

区别:在原型上定义,多个实例共享一个方法;在对象上定义,每个实例复制一份方法,易造成资源浪费,产生多个副本。调用方式也不一样

封装

  1. 静态方法/私有方法/公有方法

    var Book = function (id, name, price) {
      //私有变量
      var num = 1;
      //私有方法
      function checkId(){};
      //公有变量和方法
      this.id = id;
      this.getName = function (){};
      this.copy = function (){};
    }
    //静态方法,通过类访问 `Book.belonged` `Book.resetTime()`
    //只能访问静态属性,不能访问私有变量和对象变量
    Book.belonged = 'Black Shop';
    Book.resetTime = function (){};
    //原型方法,通过实例化访问
    //var b = new Book(1, 'You don't know JS', '$100');
    //b.isChineseBook; b.display()
    Book.prototype.isChineseBook = true;
    Book.prototype.display = function (){};
    
  2. 闭包

    有权访问另一个作用域的变量的函数,函数内返回函数。

    var Book = (function () {
     var num = 0;
         return function (id, name, price) {
          num++;
         }
    })();
    

继承

  1. 类式继承

    缺点:不能向父类传递参数,并且父类属性共用,改一发而动所有实例

    var SuperClass = function () {}
    var SubClass = function () {}
    SubClass.prototype = new SuperClass();
    
  2. 构造函数继承

    缺点:父类的原型方法不能被子类继承,如果在此方法下要被继承,就要放在构造函数里,但每次创建实例都会拷贝一份,违背代码复用原则。

    var SuperClass = function (id) {
      this.id = id;
    }
    var SubClass = function (id){
      SuperClass.call(this, id);
    }
    
  3. 组合继承(方法和属性分别继承)

    var SuperClass = function (id) {
      this.id = id;
    }
    SuperClass.prototype.getId = function () {
      return this.id;
    }
    var SubClass = function (id){
      SuperClass.call(this, id);
    }
    SubClass.prototype = new SuperClass();
    
  4. 寄生式继承

...

多态

一个函数多种调用方法,自适应参数

创建型设计模式

工厂系列方法 [简单工厂/工厂方法/抽象工厂]

区别:对于分类的复杂度不同。简单工厂把各分类拆分到各类中去;工厂方法则将各分类集成到工厂方法本身里;抽象工厂可用于进一步分类,在工厂中创建产品类簇,再使产品子类去继承产品类簇。

// 抽象工厂方法例子

建造者模式

相比工厂模式,关注创建过程。如:人要关注穿的衣服,性别和爱好等,分别处理。

var Named = function (name) {
  this.wholeName = name;
  var that = this;
  (function (name) {
    const [firstName, lastName] = name.split(' ');
    if (lastName) {
      that.firstName = firstName;
      that.lastName = lastName;
    }
  })(name)
}

var Person = function (name, skill) {
  //存储基类信息
  var _person = new Human();
  //关注过程处理
  _person.name = new Named(name);
  _person.skill = new Skills(skill);
  return _person;
}

原型模式

可复用可共享耗时大的从基类中提取出来,放到原型中,通过组合/寄生继承复用,对需要重写的进行重写。

//例子

单例模式

用于管理命名空间和私有变量的私密性

var A = {
  Tool: {
    tool_1: function (){},
  },
  Util: {
    util_1: function (){},
    util_2: function (){},
  }
}
A.Tool.tool_1();
A.Util.util_2();

结构型设计模式

外观模式

用途:兼容底层接口

//浏览器兼容设计案例
function addEvent(dom, type, fn) {
    // 对于支持DOM2级事件处理程序的浏览器
    if(dom.addEventListener) {
        dom.addEventListener(type, fn, false);
    // 对于不支持addEventListener但支持attachEvent的浏览器
    } else if(dom.attachEvent) {
        dom.attachEvent('on' + type, fn);
    } else {
        dom['on' + type] = fn;
    }
}

适配器

用途:框架之间、方法参数之间、数据适配、服务器端 数据适配

框架之间:在 A 框架基础上引入 B 框架,则把 B 框架的方法适配到 A 上;

方法参数:例子为众插件

//ES6 参数赋值写法
function objAdaptor({name = '', title = '设计模式', author = 'Xiao Ming'}) {}

数据适配: 例如数组转对象

function arrAdaptor(arr) {
  // ES6 赋值解构写法
  const [name, type, title, data] = arr;
  const obj = {name, type, title, data};
  return obj;
}

服务器端数据适配:尽早 对接口返回格式化成前端组件需要的格式

代理模式

用途:跨域,站长统计,jsonP。解决耦合度和资源开销的问题,统一处理非同域网络访问。在两个不能互通的对象之间起到 中介 作用。

//动态加载 script
window.onload = function () {
  var url = 'XXX';
  var scp = document.createElement('script');
  scp.src = url;
  document.body.appendChild(scp);
}
//图片预览例子

装饰者模式

包装原有实现,与适配器的 区别 是不需要了解原有实现,只需要原封不动的调用。即 封装拓展

// 装饰者
var decorator = function(input, fn) {
    // 获取事件源
    var input = document.getElementById(input);
    // 若事件源已经绑定事件
    if(typeof input.onclick === 'function') {
        // 缓存事件源原有回调函数
        var oldClickFn = input.onclick;
        // 为事件源定义新的事件
        input.onclick = function() {
            // 事件源原有回调函数
            oldClickFn();
            // 执行事件源新增回调函数
            fn();
        }
    } else {
        input.onclick = fn;
    }
}

桥接模式

提取变化的单元,如果是桥接方法,则把变化主体通过参数传递到单元内;如果是把变化的单元集合到桥接类中,则把变化主体直接设置为 this。抽象实现层(元素绑定事件)和抽象层(修饰页面逻辑 UI)。

//桥接方法
var changeColor = function (dom, color) {
  dom.style.color = color;
}

XXX.onmouseover = function () {
  changeColor(this.getElementsByTagName('strong')[0], 'red');
}

//桥接类
class Person {
  constructor(x,y,f) {
    // Speed 和 Speak 为多维变量
    this.speed = new Speed(x, y);
    this.font = new Speak(f);
  }
  
  move() {
    this.speed.run();
    this.font.say();
  }
}

组合模式

拆分各模块,通过 容器+成员类 的形式进行组合。组合要有容器类容器类和成员类均继承同一基类,但成员类不能拥有子成员

//表单组合 缺点:代码好多呀 w(゚Д゚)w
//所有表单 容器和成员类 均继承基类
class Base {
  constructor() {
    this.element = null;
    this.children = [];
  }
  add() {throw Error('Should rewrite this function.');}
  init() {throw Error('Should rewrite this function.');}
  getElement() {throw Error('Should rewrite this function.');}
}
/*容器类*/
class FormItem extends Base{
  constructor(id, parent) {
    super();
    this.id = id;
    this.parent =parent;
    this.init();
  }
  init() {
    this.element =document.createElement('form');
    this.element.id = this.id;
    this.element.className = 'form-item';
  }
  add(child){
    this.children.push(child);
    this.element.appendChild(child.getElement());
    return this;
  }
  getElement() { return this.element; }
  show() { this.parent.appendChild(this.element); }
}
class Group extends Base{
  constructor() {
    super();
    this.init();
  }
  init() {
    this.element = document.createElement('ul');
    this.element.className = "group-item";
  }
  add(child){
    this.children.push(child);
    var liItem =document.createElement('li');
    var childItem = liItem.appendChild(child.getElement());
    this.element.appendChild(childItem);
    return this;
  }
  getElement() { return this.element; }
}
class FieldsetItem extends Base{
  constructor(id, name) {
    super();
    this.id = id;
    this.name = name;
    this.init();
  }
  init() {
    var fieldsetDom = document.createElement('fieldset');
    fieldsetDom.id = this.id;
    fieldsetDom.className = 'fieldset-item';
    var legendDom =document.createElement('legend');
    legendDom.innerHTML = this.name;
    fieldsetDom.appendChild(legendDom);
    this.element = fieldsetDom;
  }
  add(child){
    this.children.push(child);
    this.element.appendChild(child.getElement());
    return this;
  }
  getElement() { return this.element; }
}
/*成员类*/
class LabelItem extends Base {
  constructor(id, name) {
    super();
    this.id = id;
    this.name = name;
    this.init();
  }
  init() {
    this.element = document.createElement('label');
    // 不能使用 for 关键字
    //this.element.for = this.id;
    //this.element.setAttribute("for",this.id);
    this.element.htmlFor = this.id;
    this.element.innerHTML = this.name;
  }
  getElement() { return this.element; }
}
class InputItem extends Base {
  constructor(id) {
    super();
    this.id = id;
    this.init();
  }
  init() {
    this.element = document.createElement('input');
    this.element.name = this.id;
    this.element.id = this.id;
    this.element.type = 'text';

  }
  getElement() { return this.element; }
}
class SpanItem extends Base{
  constructor(content) {
    super();
    this.content = content;
    this.init();
  }
  init() {
    this.element = document.createElement('span');
    this.element.innerHTML = this.content;
  }
  getElement() { return this.element; }
}

var form = new FormItem('FormItem', document.body);
form.add(
  new FieldsetItem('account', '账号').add(
      new Group().add(
        new LabelItem('user_name', '用户名')
      ).add(
        new InputItem('user_name')
      ).add(
        new SpanItem('4-6位数字或字母')
      )
    ).add(
      new Group().add(
        new LabelItem('user_password','密  码')
      ).add(
        new InputItem('user_password')
      ).add(
        new SpanItem('6-12位数字或字母')
      )
    )
  ).add(
  new FieldsetItem('information', '信息').add(
      new Group().add(
        new LabelItem('nickname', ' 昵称')
      ).add(
        new InputItem('nickname')
      )
    ).add(
      new Group().add(
        new LabelItem('status','状态')
      ).add(
        new InputItem('status')
      )
    )
  )
  .show();

享元模式(共享模式)

共享数据或行为。把数据分为内部数据、内部方法和外部数据、外部方法。共享开销大的元素,例如 dom 元素等,数据是独立于 dom 的。相同的行为也可以提取出来作为公共元素,通过继承等方式,减少冗余代码。再例如弹窗。

行为型设计模式

模板方法模式

先实现基础模板,再在模板上添加别的功能,类似 装饰者模式,但不同是装饰者是作为第三方存在,模板方法直接产出一个对象或行为,在实例上做改动,并且装饰者不会修改原有对象或行为,模板方法会。案例就是弹层统一。父类中定义一组操作算法骨架,而将一些实现步骤延迟到子类,使得子类可以不改变父类算法结构的同时可重新定义算法中某些实现步骤。

// 弹层统一,继承后修改基类方法和元素, ES6

观察者模式

发布 - 订阅模式,和 dom 事件的异同。通过中转站传递消息。

//发布-订阅库
var Observer = (function() {
    // 防止消息队列暴露而被篡改,故将消息容器作为静态私有变量保存
    var __messages = {};
    return {
        // 注册信息接口
        regist: function() {},
        // 发布信息接口
        fire: function() {},
        // 移除信息接口
        remove: function() {}
    }
})();

状态模式

对条件状态判断的每一种情况独立管理,解决条件分支耦合问题。案例:超级玛丽。和 桥接模式 类似,拆分多维变量,对每一种变量进行独立管理。

//超级玛丽案例
class MarryState {
  constructor() {
    this._currentState = [];
    this.state = {
      jump: function (){},
      move: function (){},,
      shoot: function (){},
      squat: function (){},
    }
  }
  
  //??ES6 私有变量
  _changeState(...args) {
    this._currentState = [];
    args.forEach(action => {
      this._currentState.push(action);
    });
    return this;
  }
  
  goes() {
    this._currentState.forEach(actionName => {
      const action = this.state[actionName];
      if (action) {action();}
    });
    return this;
  }
  
  change() { return this._changeState;}
}
//调用
var marry = new MarryState();
marry.change('jump', 'shoot').goes().goes().change('shoot').goes().goes();

//盒子移动
class BoxState {
  constructor(dom) {
    this.dom = dom;
    //this._currentState = [];
    this.step = 50;
    this.left = 0;
    this.top = 0;
    this.state = {
      37: this.moveLeft.bind(this),
      38: this.moveUp.bind(this),
      39: this.moveRight.bind(this),
      40: this.moveDown.bind(this),
    }
  }

  moveLeft() {
    this.left -= this.step;
    this.dom.style.left = this.left+'px';
  }

  moveRight() {
    this.left += this.step;
    this.dom.style.left = this.left+'px';
  }

  moveUp() {
    this.top -= this.step;
    this.dom.style.top = this.top+'px';
  }

  moveDown() {
    this.top += this.step;
    this.dom.style.top = this.top+'px';
  }

  goes(keyCode) {
    let action = this.state[keyCode];
    if (action) { action(); }
  }
}

let boxDom = document.getElementById('box');
let bx = new BoxState(boxDom);

window.addEventListener('keydown', function (e) {
  //改变状态
  let keyCode = e.which;
  bx.goes(keyCode);
});

策略模式

类似 状态模式,不同的是,状态模式 核心是对状态的控制来决定行为,策略模式 关注算法封装,通过策略名称调用来获得结果。另:对于各算法共用的部分,可以通过 享元模式 解决资源浪费问题。拓展:支持算法延伸。

//??表单验证策略统一

职责链模式

分解复杂模块,简化需求。例如:服务端拉取数据 -> 适配并解析新闻 -> 创建新闻模块 -> 为新闻添加交互 -> 展示新闻页。每个步骤都是一个模块,互相调用,且可测。

命令模式

解耦请求与实现并封装成独立对象。

//例如绘图调用方法
//但这样简单的使用其实用命令模式没什么意义
CanvasCommand.excute([
  {command: 'fillStyle', param: 'red'},
  {command: 'fillRect', param: [20, 20, 100, 100]},
]);
//??时钟案例

访问者模式

对原声对象构造器进行封装,对多变的操作类型进行原生行为封装。例如:对象也可以使用 push,splice 等方法,通过Array.prototype.splice.apply(arguments[0], args)

中介者模式

类似 观察者模式,区别是 观察者 是双向订阅模式,一个模块既可以是消息发送者也可以是接收者,而 中介者 是消息统一由中介发布,所有订阅者间接被管理。模块与模块之间的交互在中介者内部完成,保持了模块之间的独立性。

//键盘方向键控制盒子移动
//单例模式 中介者
class Mediator {
  static getInstance() {
    if (!Mediator.instance) {
      Mediator.instance = new Mediator();
    }
    return Mediator.instance;
  }
  constructor() {
    this._msg = {};
  }
  registry(type, cb) {
    this._msg[type] = this._msg[type] || [];
    if (cb) {this._msg[type].push(cb);}
  }
  send(type) {
    let funcs = this._msg[type];
    if (funcs) {
      funcs.forEach(func => {
        if (func) {func();}
      });
    }
  }
}

let md = Mediator.getInstance();
//盒子模型
class Box {
  constructor() {
    this.dom = document.getElementById('box');
    this.left = 0;
    this.top = 0;
    this.step = 50;
  }
  moveLeft() {
    this.left -= this.step;
    this.dom.style.left = this.left+'px';
  }

  moveRight() {
    this.left += this.step;
    this.dom.style.left = this.left+'px';
  }

  moveUp() {
    this.top -= this.step;
    this.dom.style.top = this.top+'px';
  }

  moveDown() {
    this.top += this.step;
    this.dom.style.top = this.top+'px';
  }
}

var b = new Box();

md.registry('moveLeft', b.moveLeft.bind(b));
md.registry('moveUp', b.moveUp.bind(b));
md.registry('moveRight', b.moveRight.bind(b));
md.registry('moveDown', b.moveDown.bind(b));

//发送层
window.addEventListener('keydown', function (e) {
  let keyCode = e.which;
  switch (keyCode) {
    case 37: md.send('moveLeft');break;
    case 38: md.send('moveUp');break;
    case 39: md.send('moveRight');break;
    case 40: md.send('moveDown');break;
  }
});

备忘录模式

缓存网络请求等其他耗时耗力的数据。配合 适配器模式 在早期格式化网络请求数据,便于今后的重复使用。

// Page备忘录类
var Page = function() {
    // 信息缓存对象
    var cache = {};
    return function(page, fn) {
        // 判断该页数据是否在缓存中
        if(cache[page]) {
            // 显示该页内容
            showPage(page, cache[page]);
            // 执行成功回调函数
            fn && fn();
        } else {
            // 否则异步请求
            $.post('./data/getNewsData.php', {
                page: page
            }, function(res) {
                // 成功返回
                if(res.errNo == 0) {
                    showPage(page, res.data);
                    cache[page] = res.data;
                    fn && fn();
                } else {
                    // 处理异常
                }
            })
        }
    }
}

迭代器模式

对数据自定义迭代器

解释器

技巧型设计模式

链模式

委托模式

冒泡 & 捕获的区别:捕获减少事件绑定,内存消耗,子元素委托给父元素处理。通过委托者将请求委托给被委托者去处理实现。通过被委托者对接受到的请求进行处理后,分发给相应委托者处理。应用有:事件处理,状态模式 中的状态对象,策略模式 中策略对象对接收到的算法处理,命令模式对接受到的命令处理。

数据访问对象模式

节流模式

用户在页面的操作是没有限制的,但是如果相应用户的频繁操作,比如:scroll、resize、move 等,会造成动作函数不停执行,性能上会产生很大的问题。所以要延迟执行,等用户操作稳定之后再处理。可用于 浮层 show & hide 可视范围优先加载,其他图片延迟加载统计打包

//简略版
function throttle(fn) {
  return function(...args) {
    let context = this
    clearTimeout(timer)
    timer = setTimeout(function() {
        fn.apply(context, args)
    }, 1000)
  }
}
//详细版
var throttle = function (...args) {
    let isCLear = args[0];
    let fn = null;
    if (typeof isCLear === 'boolean') {
      fn = args[1];
      fn.__throttleId && clearTimeout(fn.__throttleId);
    } else {
      fn = isClear;
      param = args[1];
      let p = Object.assign({
        context: null,
        options = [],
        time: 300
      }, param);
      args.callee(true, fn);
      fn.__throttleId = setTimeout(function () {
        fn.apply(p.context, p.options);
      }, p.time);
    }
}
//??搜索框 change 事件节流

简单模板模式

架构型设计模式

同步模块

即 CMD、AMD 方法的实现。对其他模块通过引用方式引入到自己的模块中,互不干扰。

//??模块化管理工具的实现

异步模块

对未加载模块的文件引用(前端限制)。

//async 方法
//实现对样式模块的依赖化加载

Widget 模式

即模板引擎。

MVC 模式

拆分数据、视图、业务逻辑三个层次,显性区分三个层次,这样还有一个好处是可以实现数据共享。

MVP 模式

MVVM 模式

参考

  1. 《JavaScript 设计模式》- 张榕铭
  2. 《JavaScript 设计模式》读后感觉很复杂
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,302评论 5 470
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,232评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,337评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,977评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,920评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,194评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,638评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,319评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,455评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,379评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,426评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,106评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,696评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,786评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,996评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,467评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,043评论 2 341

推荐阅读更多精彩内容

  • 面向对象编程 1.创建,使用函数 var CheckObject = {checkName : function(...
    依米花1993阅读 380评论 0 0
  • 工厂模式类似于现实生活中的工厂可以产生大量相似的商品,去做同样的事情,实现同样的效果;这时候需要使用工厂模式。简单...
    舟渔行舟阅读 7,710评论 2 17
  • 单例模式 适用场景:可能会在场景中使用到对象,但只有一个实例,加载时并不主动创建,需要时才创建 最常见的单例模式,...
    Obeing阅读 2,046评论 1 10
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,566评论 18 139
  • 原文: https://github.com/ecomfe/spec/blob/master/javascript...
    zock阅读 3,370评论 2 36