JavaScript的设计模式学习

关于面向对象

  • 概念

    • 类,即模板
    • 对象(实例)
  • 三要素: 继承、封装、多态

    • 子类集成父类
    • 封装,数据的权限和保密
    • 多态,同一接口不同实现

软件设计原则

  1. 小即美
  2. 让每个程序只做好一件事情
  3. 快速建立原型
  4. 舍弃高效率取可移植性
  5. 采用纯文本存储数据(可读性)
  6. 充分利用软件的杠杆效应(软件复用)
  7. 使用shell脚本来提高杠杆效应和可移植性
  8. 避免强制性的用户界面
  9. 让每个程序都称为过滤器
  10. 允许用户定制环境
  11. 尽量使操作系统内核小而美
  12. 使用小写字母并尽量简写
  13. 沉默是金
  14. 各部分之和大于整体
  15. 寻求百分之九十的解决方案

SOLID五大设计原则

  • S 单一职责原则

    • 一个程序只做好一件事情
    • 如果功能过于复杂就拆分开 每个部分保持独立
  • O 开放封闭原则

    • 对扩展开放 对修改封闭
    • 增加需求的时候 扩展新代码 而非修改已有代码
    • 设计软件的终极目标
  • L 李氏置换原则

    • 子类能够覆盖父类
    • 父类能够出现的地方子类就能够出现
  • I 接口独立原则

    • 保持接口的单一独立,避免出现“胖接口”
  • D 依赖倒置原则

    • 面向接口编程 依赖于抽象而不依赖于具体
    • 使用方只关注接口而不关注具体类的实现

23种设计模式(部分)

开胃菜

  • UML类图
  • 代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class Car {
constructor(name, number){
this.name = name;
this.number = number;
}
}

class ZCar extends Car {
constructor (name, number){
super(name,number);
this.price = 2;
}
}

class KCar extends Car {
constructor (name, number){
super(name,number);
this.prcie = 1;
}
}

class Trip {
constructor (car){
this.car = car
}
start(){
console.log(`车辆名称: ${this.car.name},车辆牌号:${this.car.number}`);
}
end(){
console.log(`本次行程的价格为:${this.car.price * 5}`)
}
}

let car = new ZCar('雷克萨斯', '川Axxxxx');
let trip = new Trip(car)
trip.start()
trip.end()

工厂模式(创建型)

工厂模式介绍

  • new操作单独封装
  • 当遇到new操作(即初始化对象或者初始化实例单独封装),就要考虑是否使用工厂模式

工厂模式情景

  • 买汉堡:
    当你去买汉堡的时候,并不用关心汉堡是如何加工好的,因为商店将加工汉堡的过程封装好了,只需要把做好的给客户

工厂模式UML类图

工厂模式代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// 汉堡包 (相当于 Product )
class Hamburger {
constructor(name) {
this.name = name;
}
// 拆开盒子
unboxing() {
console.log(this.name + ' now open the box ~ ');
}
// 拿走生菜
lettuce() {
console.log(this.name + ' take out the lettuce ! ');
}
// 拿走黄瓜
cucumber() {
console.log(this.name + ' take out the cucumber ! ');
}
eat() {
console.log('hahahahaah ~');
}
}

// KFC 商店 (相当于 Factory )
class KFC {
// 卖汉堡包
saleHamburger(name) {
// 返回一个汉堡包实例
return new Hamburger(name);
}
}

// Joie 走进了 KFC
let kfc = new KFC();

// 要了一份新奥尔良烤鸡腿堡
let hamburger = kfc.saleHamburger('新奥尔良烤鸡腿堡');

// 她拆开了汉堡盒子
hamburger.unboxing();

// 她拿出了生菜
hamburger.lettuce();

// 她拿出了黄瓜(可能烤腿堡没有黄瓜)
hamburger.cucumber();

// 最后她吃了汉堡
hamburger.eat();

工厂模式效果图

工厂模式实践

  • jQuery $(‘div’)

    • 为什么是$('div')而不是new $('div')呢?

      • 首先是因为如果全部都要写new的话,那么jQuery的链式操作成为了噩梦
      • 其次,一旦jQuery要更名对于开发者是致命性的打击
      • 结合jQuery的源码,我们可以很清晰的看见这种工厂模式

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        var  jQuery = function(selector){
        //通过new关键字第一步就可以找到构造函数
        return new jQuery.fn.init(selector);
        }

        //定义构造函数
        var init = jQuery.fn.init = function(selector){
        var slice = Array.prototype.slice;
        var dom = slice.call(document.querySelectorAll(selector));

        var i,len = dom ? dom.length : 0;
        for(i = 0; i < len; i++){
        this[i] = dom[i];
        }
        this.length = len;
        this.selector = selector || "";
        }
        window.$ = jQuery
      • 我们可以很清晰的看见,当我们使用jQuery或者($)去使用的时候,其实用的是new jQuery.fn.init(selector)这个实例

      • 这样写,无论实例的名字如何改变,jQuery可以不变,就像上文情景中,汉堡包的名字可以随意变,变成薯条等等,但是KFC不会变,我们只关心我们去KFC,而不用关心其他的。

  • React.createElement

    • 在react里的jsx

      1
      2
      3
      4
      const profile = <div>
      <p> 大家好!~ 我是工厂模式 </p>
      <p> { new Date() } </p>
      </div>
    • React.createElement处理语法糖

      1
      2
      3
      4
      const profile = React.createElement("div", null,
      React.createElement("p", null, "大家好!~ 我是工厂模式")
      React.createElement("p", null, new Date())
      )
    • 进入工厂模式

      1
      2
      3
      4
      5
      6
      7
      class Vnode (tag, attrs, chilren){
      // ... 省略内置代码
      }

      React.createElement = function (tag, attrs, chilren){
      return new Vnode(tag, attrs, chilren);
      }
    • 我们统一使用React.createElement来做收口,用户不需要关心我们使用的是什么构造函数

工厂模式设计原则验证

  • 构造函数和创建者分离

  • 符合开放封闭原则女


单例模式(创建型)

单例模式介绍

  • 整个项目或者系统中唯一被使用
  • 一个类只有一个实例

单例模式情景

  • 登录框:在一个项目中,无论我们跳转到了那个页面去登录,使用的登录框都总是一个唯一的
  • 购物车:如果是在商城类的项目中,整个项目的购物车都只能是一个购物车,否则数据不统一之后就是功能性的bug

单例模式备注

  • 因为在js里,不存在private,所以无法做到真正的去控制只实例化一次(全靠自觉)

单例模式代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class SingleObject {
login(){
console.log('login...')
}
}

SingleObject.getInstance = (function () {
let instance;
return function () {
if(!instance){
instance = new SingleObject();
}
return instance;
}
})()

let obj1 = SingleObject.getInstance();
obj1.login();

let obj2 = SingleObject.getInstance();
obj2.login();

console.log('obj1 === obj2', obj1 === obj2)

单例模式效果图

单例模式实践

  • 在jQuery中$只有一个 (思想一致但是和Java的实现不一致)

  • vuex 和 redux 中的store

单例模式设计原则

  • 符合单一职责原则,只实例化唯一的对象

  • 无法具体体现开放封闭原则,但绝不违反开放封闭原则


适配器模式(结构型)

适配器模式介绍

  • 旧接口格式和使用者不兼容

  • 中间加一个适配器兼容(类似手机转换头)

适配器模式情景

  • 旧接口封装(自定义封装ajax)

  • 插头和转换器的事例

适配器模式UML类图

适配器模式代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Adaptee {
specificRequest() {
return '德国标准插头';
}
}

class Target {
constructor() {
this.adaptee = new Adaptee();
}

request() {
const info = this.adaptee.specificRequest();
console.log(`${info} -> 转换器 -> 中国标准插头`);
}
}

class Client {
constructor() {
this.target = new Target();
}
do() {
this.target.request();
}
}

const client = new Client();
client.do();

适配器模式效果图

适配器模式实践

  • $.ajax({...})因为历史原因,在维护旧的代码的时候会出现很多$.ajax在这种使用jQuery的地方,但是我们想换成直接使用ajax({...})这种形式。

    • 原始

      1
      2
      3
      4
      $.ajax({
      method: 'xxx',
      data: xxx
      })
  • 适配

    1
    2
    3
    4
    5
    6
    var $ = {
    ajax: function (options) {
    // 新定义的ajax方法
    return ajax(options)
    }
    }
  • vue 的 computed

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    <body>
    <div id="app">
    <p>源数据:{{message}}</p>
    <p>倒置数据:{{reverseMessage}}</p>
    </div>
    <script src="https://cdn.bootcss.com/vue/2.6.10/vue.common.dev.js"></script>
    <script>
    var vm = new Vue({
    el: '#app',
    data: {
    message: 'hello'
    },
    computed: {
    // target
    reverseMessage: function () {
    // this.message 源数据
    return this.message.split('').reverse().join('');
    }
    }
    })
    </script>
    </body>
  • 效果图

适配器模式设计原则

  • 将旧接口和开发者分离

  • 符合开放封闭原则


装饰器模式(结构型)

装饰器模式介绍

  • 原本基础上进行扩展(增强功能),本身的功能不会受影响

装饰器模式情景

  • 例如给手机加一个手机壳

装饰器模式UML类图

装饰器模式代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Circle {
draw() {
console.log('画了一个圆');
}
}

class Decorator {
constructor(circle) {
this.circle = circle;
}

draw() {
this.circle.draw();
this.setBorderColor(this.circle);
}

setBorderColor(circle) {
console.log('加了一个紫色边框');
}
}

let circle = new Circle();
circle.draw();
console.log('---分割线---');

let dec = new Decorator(circle);
dec.draw();

装饰器模式效果图

装饰器模式实践

  • ES7 的装饰器

装饰器模式设计原则验证

  • 符合开放封闭原则

代理模式(结构型)

代理模式介绍

代理模式情景

  • 科学上网

  • 明星经纪人(明星与经纪人的关系)

代理模式UML类图

代理模式代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class RealImg {
constructor(fileName) {
this.fileName = fileName;
this.loadImgFromDisk(fileName); // 模拟从硬盘读取图片信息
}
loadImgFromDisk(fileName) {
console.log('loading' + fileName + '...');
}
display() {
console.log('display' + this.fileName + '...');
}
}

class ProxyImg {
constructor(fileName) {
this.realImg = new RealImg(fileName);
}
display() {
this.realImg.display();
}
}

const user = new ProxyImg('1.png');
user.display();

代理模式效果图

代理模式实践

  • 网页事件代理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    <body>
    <div id="app">
    <a href="#">1</a>
    <a href="#">2</a>
    <a href="#">3</a>
    <a href="#">4</a>
    <a href="#">5</a>
    <a href="#">6</a>
    </div>
    <script>
    var oDiv = document.getElementById('app');
    oDiv.addEventListener('click', function(e){
    var target = e.target;
    if(target.nodeName === 'A'){
    alert('ok')
    }
    })
    </script>
    </body>
    • 效果图


  • jQuery的$.proxy

    • 日常开发中,我们总会遇到this的指向不是我们理想的对象的情况

      1
      2
      3
      4
      5
      6
      $('div').click(function () {
      setTimeout(function () {
      // 此时的 this 不是外层的 div 对象是全局的window对象
      $(this).addClass('red')
      }, 1000)
      })
    • 多数情况我们是这样解决的

      1
      2
      3
      4
      5
      6
      7
      $('div').click(function () {
      var _this = this;
      setTimeout(function () {
      // 此时的 _this 即为外层的 div 对象
      $(_this).addClass('red')
      }, 1000)
      })
    • 使用jQuery的proxy

      1
      2
      3
      4
      5
      6
      $('div').click(function () {
      setTimeout($.proxy(function () {
      $(this).addClass('red')
      // this 从 proxy 的第二个参数传入
      }, this), 1000)
      })

  • ES6的Proxy

    • 以明星和经纪人为例

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      const star = {
      name: 'Elva',
      age: 40,
      phone: 'star: 13600008888',
      };

      const agent = new Proxy(star, {
      get: function(target, key) {
      if (key === 'phone') {
      // 不能直接获取明星的电话号码
      return 'agent: 13900007777';
      } else if (key === 'price') {
      // 明星是搞艺术的 不谈钱 由经纪人代理
      return 40000;
      } else {
      return target[key];
      }
      },
      set: function(target, key, val) {
      if (key === 'customPrice') {
      if (val < 12000) {
      throw new Error('价格太低');
      } else {
      target[key] = val;
      return true;
      }
      }
      },
      });

      console.log(agent.name);
      console.log(agent.age);
      console.log(agent.phone);
      console.log(agent.price);

      agent.customPrice = 2000;
    • 效果图:

代理模式设计原则验证

  • 代理类和目标类分离,隔离开目标类的使用者

  • 符合开放封闭原则


观察者模式(行为型)

观察者模式介绍

  • 发布 & 订阅

  • 一对多(一对n)

观察者模式情景

  • 点咖啡

  • 订报纸、订牛奶

观察者模式UML类图

观察者模式代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class Subject {
constructor() {
this.state = 0;
this.observers = [];
}

getState() {
return this.state;
}

setState(state) {
this.state = state;
this.notifyAllObservers();
}

notifyAllObservers() {
this.observers.forEach(observer => {
observer.update();
});
}

attach(observer) {
this.observers.push(observer);
}
}

class Observer {
constructor(name, subject) {
this.name = name;
this.subject = subject;
this.subject.attach(this);
}

update() {
console.log(`${this.name} observer is update, state is ${this.subject.getState()}`);
}
}

let s = new Subject();
let yjt = new Observer('yjt', s);
let qyj = new Observer('qyj', s);
let i = 0;
setInterval(() => {
s.setState(i++);
}, 1000);

观察者模式效果图

观察者模式实践

  • 网页事件绑定

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <button id="btn">提交</button>

    $('#btn).click(function(){
    console.log(1)
    })

    $('#btn).click(function(){
    console.log(2)
    })

  • Promise

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    function loadImg (src){
    var promise = new Promise(function (resolve, reject){
    var img = document.createElement('img')
    img.onload = function () {
    resolve(img)
    }
    img.onerror = function () {
    reject('图片加载失败')
    }
    img.src = src
    })
    return promise
    }

    var img = loadImg('xxxxx.png')
    img
    .then((img) => {
    console.log('width', img.width)
    return img
    })
    .then((img) => {
    console.log('height', img.height)
    })

  • jQuery callbacks

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    var callbacks = $.Callbacks()
    callbacks.add = function (info) {
    console.log('fn1', info)
    }

    callbacks.add = function (info) {
    console.log('fn2', info)
    }

    callbacks.add = function (info) {
    console.log('fn3', info)
    }

    callbacks.fire('gogogo')

  • nodejs自定义事件

    • 读取文件流
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    const fs = require('fs');
    const readStream = fs.createReadStream('../data/file.txt');

    let lens = 0;

    readStream.on('data', function(chunck) {
    let len = chunck.toString().length;
    console.log('len', len);
    lens += len;
    });

    readStream.on('end', function() {
    console.log('length', lens);
    });
  • 读取文件行

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    const fs = require('fs');
    const readline = require('readline');

    const rl = readline.createInterface({
    input: fs.createReadStream('../data/file.txt'),
    });

    let rds = 0;

    rl.on('line', function() {
    rds++;
    });

    rl.on('close', function() {
    console.log('close', rds);
    });

  • 其他case

  • vue 和 react 中的生命周期函数

  • vue watch


观察者模式设计原则验证

  • 主题和观察者分离,不是主动触发而是被动监听,两者解耦

  • 符合开放封闭原则


迭代器模式(行为型)

迭代器模式介绍

  • 顺序访问一个集合

  • 使用者无需知道集合内部的结构(封装)

迭代器模式情景

  • jQuery中

迭代器模式UML类图

迭代器模式代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var arr = [1, 2, 3]
var nodeList = document.getElementsByTagName('a')
var $a = $('a')

function each(data) {
var $data = $(data) // 生成迭代器
$data.each(function (index, el) {
console.log(index, el, '迭代器')
})
}

each(arr)
each(nodeList)
each($a)

迭代器模式效果图

迭代器模式实践

  • jQuery each

  • ES6 Iterator

    • ES6 Iterator 是什么


  • 实现ES6迭代器

    1
    2
    3
    4
    5
    6
    7
    8
    function each(data) {
    let iterator = data[Symbol.iterator]();
    let item = iterator.next();
    while (!item.done) {
    console.log(item.value);
    item = iterator.next();
    }
    }

  • ES6

    1
    2
    3
    4
    5
    function each(data) {
    for (let item of data) {
    console.log(item);
    }
    }