JS设计模式

128 阅读12分钟

1. 面向对象

1.1 搭建开发环境

主要步骤如下所示:

  • 初始化 npm 环境
  • 安装 webpack
  • 安装 webpack-dev-server
  • 安装 babel

确保本地已经安装了 nodenpm,查看其是否已经安装: node -vnpm -v 如果没有安装,去node官网或者npm官网下载一个稳定版本即可。 现在,我们开始搭建开发环境:

  1. 初始化npm 环境
npm init
  1. 新建src文件夹,并在该文件夹下面新建index.js文件,内容为 alert('hello world')

  2. 为了提升安装速度,将npm镜像设置成淘宝镜像;再安装webpack,如果你使用 webpack 4+ 版本,你还需要安装 CLI

npm config set registry https://registry.npm.taobao.org; // 设置npm镜像
npm get registry; // 查看npm镜像
npm i webpack webpack-cli --save-dev  
  1. 在根目录下新建 webpack.dev.config.js,代码如下:
module.exports = {
    entry: './src/index.js',
    output: {
        path: __dirname, // 当前目录
        filename: './release/bundle.js'
    }
}
  1. 修改 package.json中的scripts 命令,新增如下命令行:
 "scripts": {
    "dev": "webpack --config ./webpack.dev.config.js --mode development"
  },
  1. 在终端运行命令 npm run dev,会生成 release/bundle.js文件
  2. 安装,npm i webpack-dev-server html-webpack-plugin
  npm i webpack-dev-server html-webpack-plugin --save-dev
  1. 在根目录下创建 index.html 文件,输入 html:5 Tab键快速生成html模板代码:
!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>前端js设计模式</title>
</head>
<body>
    <p>前端设计模式学习</p>
</body>
</html>
  1. 修改webpack.dev.config.js的代码如下:
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    entry: './src/index.js',
    output: {
        path: __dirname, // 当前目录
        filename: './release/bundle.js'
    },

    plugins: [
        new HtmlWebpackPlugin({
            template: './index.html'
        })
    ],

   
    devServer: {
        static: {
            directory: path.join(__dirname, "./release")
        },
        open: true, // 自动打开浏览器
        port: 8000,
    }
}
  1. 修改package.json文件中的 运行命令:
"scripts": {
    "dev": "webpack-dev-server --config ./webpack.dev.config.js --mode development"
  },
  1. 运行命令 npm run dev,浏览器会自动打开一个页面,如下图:

image.png

  1. 安装babel插件
npm i -D @babel/core babel-loader @babel/preset-env @babel/preset-react
  1. 在根目录下新建.babelrc文件,代码如下:
{
    "presets": [
        "@babel/preset-env",
        "@babel/preset-react"
    ],
    "plugins": []
}
  1. webpack.dev.config.js中,增加babel编译js代码的配置,代码如下:
module.exports = {
    ......省略很多代码
    module: {
        rules: [{
            test: /\.js?$/,
            exclude: /(node_modules)/,
            loader: 'babel-loader'
        }]
    },

    plugins: [
       ......省略很多代码
    ],
}
  1. 修改 src/index.js 代码如下:
class Animals {
    constructor(type) {
        this.type = type;
    }
    getType() {
        return this.type;
    }
}

const d = new Animals('dog');
alert(d.getType());

然后,npm run dev 即可运行项目。

1.2 面向对象

  • 面向对象的概念
  • 三要素:继承、封装、多态
  • JS的应用举例
  • 面向对象的意义
// 类
class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    eat() {
        alert(`${this.name} eat sth`);
    }
    speak() {
        alert(`My name is ${this.name}, I am ${this.age}`);
    }
}
// coco 和 joe 是 Person的实例
const coco = new Person('coco', 20);
coco.eat();
coco.speak();

const joe = new Person('joe', 30);
joe.eat();
joe.speak();
  • 继承: 子类继承父类
  • 封装: 数据的权限和保密
  • 多态: 同一接口不同实现

继承的例子如下:

// 父类
class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    eat() {
        alert(`${this.name} eat sth`);
    }
    speak() {
        alert(`My name is ${this.name}, I am ${this.age}`);
    }
}

// 子类继承父类
class Student extends Person {
    constructor(name, age, number) {
        super(name, age);
        this.number = number;
    }

    study() {
        alert(`${this.name} study`);
    }
}

let zhangsan = new Student("zhangsan", 10, '1');
zhangsan.study();
alert(zhangsan.number);
zhangsan.eat();
let lisi = new Student("lisi", 11, '2');
lisi.study();
lisi.speak();

封装:

public完全开放,protected 对子类开放, private 对自己开放

  • 减少耦合,不该外露的不外露;
  • 利于数据、接口的耦合管理;
  • ES6目前不支持,一般认为_开头的属性是 private

多态:

  • 同一个接口,不同表现;
  • JS应用极少;
  • 需要结合java等语言的接口、重写、重载等功能;

JS 应用举例: jQuery 是一个class$('p') jQuery的一个实例

image.png

总结: 面向对象的意义:数据结构化,具体如下:

  • 程序执行:顺序、判断、循环 --- 结构化
  • 面向对象 --- 数据结构化
  • 对于计算机,结构化的才是最简单的
  • 编程 应该 简单 && 抽象

1.3 UML类图

画图工具:

用法:

image.png

2. 设计原则

2.1 SOLID 五大设计原则

  • S- 单一职责
  • O- 开闭
  • L- 里式替换
  • I- 接口独立
  • D- 依赖倒置

S- 单一职责原则:

  • 一个程序只做好一件事
  • 如果功能过于复杂就拆分开,每个部分保持独立

O- 开放封闭原则:

  • 对扩展开放,对修改封闭
  • 增加需求时,扩展新代码,而非修改已有代码
  • 这是软件设计的终极目标

L- 里式替换原则:

  • 子类能覆盖父类
  • 父类能出现的地方,子类就能出现
  • JS中使用较少(弱类型 & 继承使用较少)

I- 接口独立原则:

  • 保持接口的单一独立,避免出现“胖接口”
  • JS中没有接口(typescript除外),使用较少
  • 类似于单一职责原则,这里更关注接口

D- 依赖倒置原则:

  • 面向接口编程,依赖于抽象而不依赖于具体
  • 使用方只关注接口而不关注具体类的实现
  • JS中使用较少(没有接口 & 弱类型)

2.2 用Promise来说明SO(单一职责和开闭)原则

/**
 * 用Promise来说明SO(单一职责和开闭)原则
 * S-单一职责原则:每个then中的逻辑只做好一件事
 * O-开闭原则:如果新增需求,扩展then
 */
function loadImg(src) {
    let promise = new Promise(function (resolve, reject) {
        let img = document.createElement('img');
        img.onload = function () {
            resolve(img)
        }
        img.onerror = function () {
            reject('图片加载失败');
        }
        img.src = src;
    });
    return promise;
}

let src = "https://storage.360buyimg.com/taro-static/static/images/wj.png";
const result = loadImg(src);
result.then(function (img) {
    // part1
    alert(`width: ${img.width}`);
    return img;
}).then(function (img) {
    // part2
    alert(`height: ${img.height}`);
}).catch(function (err) {
    // 统一捕获异常
    alert(err);
})

then部分的功能如果复杂的话,还可以使用模块化写到单独的模块里。

3. 设计模式

3.1 例题1讲解

第一题题目描述:

  • 打车时,你可以打快车和专车
  • 无论什么车,都有车牌号和车辆名称
  • 打不同的车价格不同,快车每公里 1 元,专车每公里 2 元
  • 打车时,你要启动行程并显示车辆信息
  • 结束行程,显示价格(假定行驶了 5 公里)

画出UML图(在线画图软件 processon):

image.pngES6 语法写出该示例:

class Car {
    constructor(number, name) {
        this.number = number;
        this.name = name;
    }
}

class Kuaiche extends Car {
    constructor(number, name) {
        super(number, name);
        this.price = 1; // 快车每公里1元
    }
}

class Zhuanche extends Car {
    constructor(number, name) {
        super(number, name);
        this.price = 2; // 快车每公里2元
    }
}

class Trip {
    constructor(car, distance) {
        this.car = car;
        this.distance = distance; // 当前行使距离
    }

    start() {
        console.log(`行程开始:车辆的名称是${this.car.name}, 车牌号信息是${this.car.number}`);
    }

    end() {
        console.log(`行程结束:车程的金额是${this.car.price * this.distance}`);
    }
}

const car1 = new Kuaiche('浙Axxxxx', '特斯拉');
const trip1 = new Trip(car1, 5);
trip1.start(); // 行程开始:车辆的名称是特斯拉, 车牌号信息是浙Axxxxx
trip1.end(); // 行程结束:车程的金额是5

const car2 = new Zhuanche('粤Axxxxx', '比亚迪');
const trip2 = new Trip(car2, 5);
trip2.start(); // 行程开始:车辆的名称是比亚迪, 车牌号信息是粤Axxxxx
trip2.end(); // 行程结束:车程的金额是10

3.2 例题2讲解

某停车场,分3层,每层100车位
每个车位都能监控到车辆的驶入和离开
车辆进入前,显示每层的空余车位数量
车辆进入时,摄像头可识别车牌号和时间
车辆出来时,出口显示器显示车牌号和停车时长

画出UML图(在线画图软件 processon):

image.png

代码:

// 车辆
class Car {
    constructor(num) {
        this.num = num;
    }
}

// 摄像头
class Camera {
    shot(car) {
        return {
            num: car.num,
            inTime: Date.now(),
        }
    }
}

// 出口显示屏
class Screen {
    show(car, inTime) {
        console.log(`车牌号:${car.num}, 停车时间${Date.now() - inTime}`);
    }
}

// 停车场
class Park {
    constructor(floors) {
        this.floors = floors || [];
        this.camera = new Camera();
        this.screen = new Screen();
        this.carList = {}; // 存储摄像头拍摄返回的车辆信息
    }

    // 车辆驶入
    in(car) {
        // 通过摄像头获取信息
        const info = this.camera.shot(car);
        // 停到第一层某个车位
        const i = parseInt(Math.random() * 100 % 100);
        const place = this.floors[0].places[i];
        place.in(); // 停车
        info.place = place;
        // 记录信息
        this.carList[car.num] = info;
    }
    out(car) {
        // 获取信息
        const info = this.carList[car.num];
        // 将停车位清空
        const place = info.place;
        place.out();
        // 显示时间
        this.screen.show(car, info.inTime);
        // 清空该车的记录
        delete this.carList[car.num];
    }
    emptyNum() {
        return this.floors.map((floor) => {
            return `${floor.index} 层还有 ${floor.emptyPlaceNum()} 个空闲车位`
        }).join('\n')
    }
}

// 层
class Floor {
    constructor(index, places) {
        this.index = index;
        this.places = places || [];
    }
    emptyPlaceNum() {
        let num = 0;
        this.places.forEach((p) => {
            if (p.empty) num = num + 1;
        })
        return num;
    }
}

// 车位
class Place {
    constructor() {
        this.empty = true;
    }
    in() {
        this.empty = false;
    }
    out() {
        this.empty = true;
    }
}

// 测试 --------------------------
// 初始化停车场
const floors = [];
for (let i = 0; i < 3; i++) {
    const places = [];
    for (let j = 0; j < 100; j++) {
        places[j] = new Place();
    }
    floors[i] = new Floor(i + 1, places);
}
const park = new Park(floors);

// 初始化车辆
const car1 = new Car('A1');
const car2 = new Car('A2');
const car3 = new Car('A3');
console.log("第1辆车准备进入");
console.log(park.emptyNum());
park.in(car1);
console.log("第2辆车准备进入");
console.log(park.emptyNum());
park.in(car2);
console.log("第1辆车离开");
park.out(car1);
console.log("第2辆车离开");
park.out(car2);
console.log("第3辆车准备进入");
console.log(park.emptyNum());
park.in(car3);
console.log("第3辆车进入停好后");
console.log(park.emptyNum());

结果:

image.png

4. 设计原则

4.1 工厂模式

介绍

  • 将new 操作单独封装
  • 遇到new时,就要考虑是否该使用工厂模式

对应到生活的例子:

  • 你去购买汉堡,直接点餐、取餐,不用自己亲手去做
  • 商店要“封装”做汉堡的工作,做好直接给买家

UML图

image.png

代码:

class Product {
    constructor(name) {
        this.name = name;
    }
    init() {
        alert('init');
    }
    func1() {
        alert('func1');
    }
    func2() {
        alert('func2')
    }
}

class Creator {
    create(name) {
        return new Product(name)
    }
}

const creator = new Creator();
const p = creator.create('p1');
p.init();
p.func1();

经典使用场景

场景1: jQuery$('xxx')

image.png

场景2:React.createElement()

image.png

image.png

4.2 单例模式

介绍

  • 系统中被唯一使用
  • 一个类只有一个实例
class SingleObject {
    login() {
        console.log('login......')
    }
}

// 定义静态方法 getIntance
SingleObject.getIntance = (function () {
    let instance;
    // 闭包
    return function () {
        if (!instance) {
            instance = new SingleObject();
        }
        return instance;
    }
})()

// 测试:注意这里只能使用静态函数 getInstance,不能使用 new SingleObject()
let obj1 = SingleObject.getIntance();
obj1.login();
let obj2 = SingleObject.getIntance();
obj2.login();
console.log("obj1 === obj2", obj1 === obj2) // true, 两者必须完全相等,否则就不是单例模式了

let obj3 = new SingleObject(); // 使用new 方式,就不是单例模式了
console.log("obj1 === obj3", obj1 === obj3); // false

应用场景

1. jQuery只有一个 $

image.png

2. 系统的登录

image.png

3. 其他场景(购物车(和登录框类似)、vuex和redux中的store)

  • 购物车(和登录框类似)
  • vuexredux中的store

4.3 适配器模式

旧接口格式和使用者不兼容,中间加一个适配转换接口,这就是适配器模式。

class Adaptee {
    specificRequest() {
        return '德国标准插头';
    }
}

class Target {
    constructor() {
        this.adaptee = new Adaptee();
    }
    request() {
        let info = this.adaptee.specificRequest();
        return `${info}-转换器-中国标准插头`
    }
}

let target = new Target();
let res = target.request();
console.log(res); // 德国标准插头-转换器-中国标准插头

使用场景:ajax场景

image.png

image.png

这样,既可以方便扩展ajax新功能,也不影响原来使用到$.ajax的地方。

4.4 装饰器模式

为对象添加新功能,不改变其原有的结构和功能。

/**
 * 装饰器模式
 */
class Circle {
    draw() {
        console.log('画一个圆形');
    }
}

class Decorator {
    constructor(circle) {
        this.circle = circle;
    }
    draw() {
        this.circle.draw();
        this.setRedBorder(circle);
    }
    setRedBorder() {
        console.log("设置红色边框")
    }
}

let circle = new Circle();
circle.draw(); // 画一个圆形

let decorator = new Decorator(circle);
decorator.draw(); // 画一个圆形    设置红色边框

装饰器模式之装饰类

// 1. 简单的装饰类的demo
function testDec(target) {
    target.isDesc = true;
}

@testDec
class Demo {
    // ... 省略代码
}
alert(Demo.isDesc); // true
// 2. 装饰器的原理
@decorator
class A { }

// 等同于
class A { }
A = decorator(A) || A;
// 3. 装饰类加参数
function testDec(isDesc) {
    return function (target) {
        target.isDesc = isDesc;
    }
}

@testDec(false)
class Demo {
    //......
}
alert(Demo.isDesc) // false

装饰类-mixin示例:

image.png

装饰器模式之装饰方法

/**
 * 装饰器模式
 * 装饰方法
 */
function readonly(target, name, descriptor) {
    descriptor.writable = false;
    return descriptor;
}
class Person {
    constructor() {
        this.first = 'A';
        this.last = 'B';
    }

    @readonly
    name() {
        return `${this.first} ${this.last}`;
    }
}

let p = new Person();
console.log(p.name());
p.name = '11111111';// 报错,name只读,不能修改

第三方的库 core-decorators

core-decorators 提供了常用的装饰器。 查阅文档:github.com/jayphelps/c…

  • 装饰器模式,将现有对象和装饰器进行分离,两者独立存在;
  • 符合开闭原则。

4.5 代理模式

使用者无法访问目标对象,那么就在中间加代理,通过代理做授权和控制。常见的就是翻墙上网、明星经纪人等。

/**
 * 代理模式
 */
class RealImg {
    constructor(fileName) {
        this.fileName = fileName;
        this.loadFromDisk();
    }
    loadFromDisk() {
        console.log('loading...' + this.fileName);
    }
    display() {
        console.log('display...' + this.fileName);
    }
}

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

let proxyImg = new ProxyImg('1.png');
proxyImg.display();

应用场景

1. 网页事件代理

image.png

2. jQuery $.proxy

image.png 为啥this不符合期望:因为this指向了Window。

image.png

3. ES6的Proxy

// 明星
let star = {
    name: '张三',
    age: 20,
    phone: '明星:18866668888'
};

// 经纪人
let agent = new Proxy(star, {
    get: function (target, key) {
        if (key === 'phone') {
            return '经纪人电话:1111111111';
        }
        if (key === 'price') {
            return 10000; // 经纪人报价
        }
        return target[key];
    },
    set: function (target, key, val) {
        if (key === 'customPrice') {
            if (val < 8000) {
                throw new Error('价格太低');
            } else {
                target[key] = val;
                return true;
            }
        }
    }
});
console.log(agent.name);// 张三
console.log(agent.age); // 20
console.log(agent.phone); // 经纪人电话:1111111111
console.log(agent.price); // 10000

// agent.customPrice = 7000;
// console.log(agent.customPrice);// 报错,价格太低
agent.customPrice = 9000;
console.log(agent.customPrice); // 9000

代理类和目标类分离,隔离开目标类和使用者;且符合开闭原则。

代理模式 VS 适配器模式

  • 适配器模式:提供一个不同的接口(如不版本的插头)
  • 代理模式:提供一模一样的接口

代理模式 VS 装饰器模式

  • 装饰器模式:扩展功能,原有功能不变且可直接使用
  • 代理模式:显示原有功能,但是经过限制或者阉割之后的(比如经纪人不透露明星的电话,只给出自己的电话)

4.6 门面模式(外观模式)

为子系统中的一组接口提供了一个高层接口,使用者调用这个高层接口,这个高层接口就是门面模式。比如,去医院看病,会有接待员帮我们去挂号、门诊、划价、取药等,这个就类似于门面模式。

image.png

门前模式在前端的应用,兼容不同个数的参数,支持传4个参数或者3个参数:

image.png

注意:门面模式不符合单一职责和开闭原则,因此谨慎使用,不可滥用。

4.7 观察者模式

  • 发布&订阅
  • 一对多 (一对一也行,别纠结在此)

image.png

/**
 * 观察者模式
 */
// 主题,保存状态,状态变化之后触发所有观察者对象
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} update, state:${this.subject.getState()}`);
    }
}

const s = new Subject();
const o1 = new Observer('o1', s);
const o2 = new Observer('o2', s);
s.setState(1);
s.setState(2);

// 结果:
o1 update, state:1
o2 update, state:1
o1 update, state:2
o2 update, state:2

应用场景

1. 网页事件绑定、网页事件监听

网页事件绑定:

image.png

2. Promise

promise中的.then部分也属于观察者模式,一开始先订阅,等到promise状态改变之后再去执行.then里面的代码。

image.png

image.png

3. jQuery callbacks

在根目录下新建 01-jquery-callbacks.html,代码如下:

// 01-jquery-callbacks.html文件
<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <title>jQuery callbacks 观察者模式</title>
</head>

<body>
    <p>jQuery callbacks</p>

    <script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script>
    <script>
        // 自定义事件,自定义回调
        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')
        callbacks.fire('fire')
    </script>
</body>

</html>

执行 http-server -p 8081,在浏览器中打开页面:http://localhost:8082/01-jquery-callbacks.html

结果如下:

image.png

4. nodejs自定义事件

事件监听:

image.png

nodeJs中的Stream 、readline读取文件也用到了观察者模式:

image.png

image.png

5. 其他场景

  • nodejs中:处理 http 请求;多进程通讯等;

image.png

  • vueReact 组件生命周期触发:

不管是React还是Vue,它的一个组件就是一个构造函数,生成一个组件就是生成一个实例,其各个生命周期都是在组件的不同状态下触发的,这也是一种观察者模式。

  • vue watch

image.png

  • ......

设计原则验证:

  • 主题和观察者分离,不是主动触发而是被动监听,两者解耦;
  • 符合开放封闭原则。

4.8 迭代器模式

  • 顺序访问一个集合
  • 使用者无需知道集合的内部结构(封装)

image.png

/**
 * 遍历器模式
 */
class Iterator {
    constructor(container) {
        this.list = container.list;
        this.index = 0;
    }
    next() {
        if (this.hasNext()) {
            return this.list[this.index++];
        }
        return null;
    }
    hasNext() {
        if (this.index >= this.list.length) {
            return false;
        }
        return true;
    }
}

class Container {
    constructor(list) {
        this.list = list;
    }
    // 生成遍历器
    getIterator() {
        return new Iterator(this);
    }
}

let container = new Container([1, 2, 3, 4, 5, 6]);
let iterator = container.getIterator();
while (iterator.hasNext()) {
    console.log(iterator.next()); // 1 2 3 4 5 6
}

应用场景

1. jQuery each

image.png

2. ES6 Iterator

ES6 Iterator 为何存在?

因为

  • ES6语法中,有序集合的数据类型有很多
  • Array、Map、Set、String、TypedArray、arguments、NodeList
  • 需要有一个统一的遍历接口来遍历所有数据类型
  • 注意,object不是有序集合,可以用Map代替
  • 以上数据类型,都有[Symbol.iterator] 属性
  • [Symbol.iterator] 属性值是函数,执行函数返回一个迭代器
  • 这个迭代器就有 next 方法可顺序迭代子元素
  • 可运行 Array.prototype[Symbol.iterator] 来测试

去浏览器的控制台验证一下:

Array.prototype[Symbol.iterator]; // ƒ values() { [native code] }
Array.prototype[Symbol.iterator](); // Array Iterator {}
Array.prototype[Symbol.iterator]().next(); // {value: undefined, done: true}

image.png

/**
 * ES6的Iterator示例
 */
function each(data) {
    // 生成遍历器
    let iterator = data[Symbol.iterator]();

   // console.log(iterator.next()); // 有数据时返回 { value: 1, done: false }
   // console.log(iterator.next());
   // console.log(iterator.next());
   // console.log(iterator.next());
  // console.log(iterator.next()); // 没有数据时返回 { value: undefined, done: true }

    let item = { done: false };
    while (!item.done) {
        item = iterator.next();
        if (!item.done) {
            console.log(item.value)
        }
    }
}

// 测试代码
let arr = [1, 2, 3, 4];
each(arr); // 1 2 3 4 

let m = new Map();
m.set('a', 100);
m.set('b', 200);
each(m);
// 结果如下:
// {value: Array(2), done: false}
// {value: Array(2), done: false}
// {value: undefined, done: true}
// {value: undefined, done: true}
// {value: undefined, done: true}

// let nodeList = document.getElementsByTagName('p');
// each(nodeList);

但是,Symbol.iterator并不是人人都知道,也不是每个人都需要封装一个 each 方法,因此有了 for...of语法。

function each(data) {
    // 带有遍历器特性的对象:data[Symbol.iterator] 有值
    for (let item of data) {
        console.log(item);
    }
}
// 测试代码
let arr = [1, 2, 3, 4];
each(arr); // 1 2 3 4

3. Generator

Iterator的价值不限于上述几个类型的遍历,还有Generator函数的使用。 即只要返回的数据符合Iterator接口的要求,即可使用Iterator语法,这就是迭代器模式。

image.png

设计原则验证:

  • 迭代器对象和目标对象分离
  • 迭代器将使用者与目标对象隔离开
  • 符合开放封闭原则

4.9 状态模式

介绍

  • 一个对象有状态变化
  • 每次状态变化都会触发一个逻辑
  • 不能总是用 if...else来控制

比如红绿灯变化。

image.png

/**
 * 状态模式
 */
// 状态,比如红灯、绿灯、黄灯
class State {
    constructor(color) {
        this.color = color;
    }
    handle(context) {
        console.log(`turn to ${this.color} light`);
        context.setState(this);
    }
}

// 主体,可以获取状态和设置状态
class Context {
    constructor() {
        this.state = null;
    }
    getState() {
        return this.state;
    }
    setState(state) {
        this.state = state;
    }
}

// 测试代码
let context = new Context();

let green = new State('green');
green.handle(context); // turn to green light
console.log(context.getState()); // State {color: 'green'}

let red = new State('red');
red.handle(context); // turn to red light
console.log(context.getState()); // State {color: 'red'}

let yellow = new State('yellow');
yellow.handle(context); // turn to yellow light
console.log(context.getState()); // State {color: 'yellow'}

有限状态机

  • 有限个状态、以及在这些状态之间的变化
  • 如交通信号灯
  • 使用开源lib:javascript-state-machine
  • github.com/jakesgordon…

开源库:javascript-state-machine

// index.html
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>前端js设计模式</title>
</head>

<body>
    <p>前端设计模式学习</p>
    <button id="btn"></button>
</body>

</html>
/**
 * 状态模式
 * javascript-state-machine-demo.js
 * https://github.com/jakesgordon/javascript-state-machine/blob/master/README.md
 */
import StateMachine from "javascript-state-machine";
import $ from 'jquery';

let $btn = $('#btn')
// 状态机模型
let fsm = new StateMachine({
    init: '收藏',  // 初始状态,待收藏
    transitions: [
        {
            name: 'doStore',
            from: '收藏',
            to: '取消收藏'
        },
        {
            name: 'deleteStore',
            from: '取消收藏',
            to: '收藏'
        }
    ],
    methods: {
        // 执行收藏
        onDoStore: function () {
            alert('收藏成功')
            updateText()
        },
        // 取消收藏
        onDeleteStore: function () {
            alert('已取消收藏')
            updateText()
        }
    }
})

// 更新文案
function updateText() {
    $btn.text(fsm.state)
}

// 初始化文案
updateText()

// 点击事件
$btn.click(function () {
    if (fsm.is('收藏')) {
        fsm.doStore(1)
    } else {
        fsm.deleteStore()
    }
})

安装 jqueryjavascript-state-machine,然后运行 npm run dev 即可。

Promise就是有限状态机

  • promise三种状态:pending、fullfilled、rejected
  • pending -> fullfilled 或者 pending -> rejected
  • 不能逆向变化

设计原则验证:

  • 将状态对象和主题对象分离,状态的变化逻辑单独处理;
  • 符合开放封闭原则。

4.10 其他设计模式

1. 原型模式

  • 克隆自己,生成一个新对象
  • java默认有clone接口,不用自己实现。

根据已有对象新的对象时,如果使用new 的方式开销大或者有别的问题,就可以使用原型模式进行创建。

在JS中的应用:Object.create()

Object.create()  方法用于创建一个新对象,使用现有的对象来作为新创建对象的原型(prototype)。

/**
 * 原型模式
 */
// Object.create() 用到了原型模式的思想,虽然不是java中的clone
// 基于一个原型创建一个对象
const prototype = {
    getName: function () {
        return this.first + '' + this.last;
    },
    say: function () {
        console.log('hello');
    }
}

// 基于原型创建 x
let x = Object.create(prototype);
x.first = 'A';
x.last = 'B';
console.log(x.getName()); // AB
x.say(); // hello


// 基于原型创建y
let y = Object.create(prototype);
y.first = 'Y';
y.last = 'Z';
console.log(y.getName()); // YZ
y.say(); // hello

对比JS中的原型 prototype

  • prototype 可以理解为 ES6 class 的一种底层原理;
  • 而class是实现面向对象的基础,并不是服务于某个模式;
  • 当ES6全面普及时,大家可能会忽略掉prototype;
  • 但是 Object.create却会长久存在。

2. 桥接模式

  • 用于把抽象化与实现化解耦;
  • 使得两者可以独立变化;
  • (未找到JS中的经典应用)

image.png

image.png

以上方法实现直白,但是扩展性差。利用桥接模式抽象实现,将形状和颜色分开实现,如下所示:

image.png

image.png

这样,代码就变得清晰、可扩展了。

3. 组合模式

介绍:

  • 生成树形结构,表示“整体-部分”关系;
  • 让整体和部分具有一致的操作方式。

image.png

演示:

  • JS 经典应用中,未找到这么复杂的数据类型;
  • 虚拟DOM中的vnode是这种形式,但数据类型简单;
  • (用JS实现一个菜单,不算经典应用,而是与业务相关)

image.png

从图中可以看到,整体和单个节点的操作是一致的,整体和单个节点的数据结构也保持一致。

设计原则验证:

  • 将整体和单个节点的操作抽象出来;
  • 符合开放封闭原则。

4. 享元模式

  • 共享内存(主要考虑节省内存,而非效率)
  • 相同的数据,共享使用。
  • (JS中未找到经典应用场景)

image.png

无限下拉列表,将事件代理到高层节点上,也算是享元模式思想的一种应用吧。

设计原则验证:

  • 将相同的部分抽象出来;
  • 符合开放封闭原则。

5. 策略模式

  • 不同策略分开处理
  • 避免使用大量 if...else 或者 switch...case
  • (JS中未找到经典应用场景)

演示例子:

class User {
    constructor(type) {
        this.type = type;
    }
    buy() {
        if (this.type === 'ordinary') {
            console.log('普通用户购买');
        } else if (this.type === 'member') {
            console.log('会员用户购买');
        } else if (this.type === 'vip') {
            console.log('vip 用户购买');
        }
    }
}

// 测试代码
let u1 = new User('ordinary');
u1.buy(); // 普通用户购买
let u2 = new User('member');
u2.buy(); // 会员用户购买
let u3 = new User('vip');
u3.buy(); // vip 用户购买

将这段代码的if ...else改成策略模式实现:

class OrdinaryUser {
    buy() {
        console.log('普通用户购买')
    }
}
class MemberUser {
    buy() {
        console.log('会员用户购买')
    }
}
class VipUser {
    buy() {
        console.log('vip 用户购买')
    }
}

let u1 = new OrdinaryUser();
u1.buy(); // 普通用户购买
let u2 = new MemberUser();
u2.buy(); // 会员用户购买
let u3 = new VipUser();
u3.buy(); // vip 用户购买

设计原则验证:

  • 不同策略,分开处理,而不是混合在一起
  • 符合开放封闭原则。

6. 模板方法模式

image.png

对一些有特殊顺序、特殊逻辑等的党发做一些封装或合并,输出一个统一的对外的方法,这就是模板方法模式。如图中的handle1、handle2、handle3需要顺序执行,那么用handle方法处理一下,调用handle方法即可实现。

7. 职责链模式

  • 一步操作可能分为多个职责角色来完成
  • 把这些角色都分开,然后用一个链串起来
  • 将发起者和各个处理者进行隔离。

演示例子:请假

/**
 * 职责链模式
 */
class Action {
    constructor(name) {
        this.name = name;
        this.nextAction = null;
    }
    setNextAction(action) {
        this.nextAction = action;
    }
    handle() {
        console.log(`${this.name}审批`);
        if (this.nextAction != null) {
            this.nextAction.handle();
        }
    }
}

let a1 = new Action('组长');
let a2 = new Action('经理');
let a3 = new Action('人力');
a1.setNextAction(a2);
a2.setNextAction(a3);
a1.handle(); // 组长审批   经理审批  人力审批

JS中的链式操作:

  • 职责链模式和业务结合较多,JS中能联想到链式操作;
  • jQuery的链式操作、Promise.then的链式操作。

设计原则验证:

  • 发起者与各个处理者进行隔离;
  • 符合开放封闭原则。

8. 命令模式

  • 执行命令时,发布者和执行者分开;
  • 中间加入命令对象,作为中转站。

image.png

发送者:发出命令,调用命令对象;

命令对象:接收命令,调用接受者对应接口;

接受者:执行命令。

class Receiver {
    exec() {
        console.log('执行');
    }
}

class Command {
    constructor(receiver) {
        this.receiver = receiver;
    }
    cmd() {
        console.log('触发命令');
        this.receiver.exec();
    }
}

class Invoker {
    constructor(command) {
        this.command = command;
    }
    invoke() {
        console.log('开始');
        this.command.cmd();
    }
}

// 测试代码
// 士兵
let soldier = new Receiver();
// 小号手
let trumpeter = new Command(soldier);
// 将军
let general = new Invoker(trumpeter);
general.invoke(); // 开始  触发命令  执行

JS中的应用:

image.png

  • 网页富文本编辑器操作,浏览器封装了一个命令对象;
  • document.execCommand('bold')
  • document.execCommand('undo')

注意:document.execCommand()已废弃:  该特性已经从 Web 标准中删除,虽然一些浏览器目前仍然支持它,但也许会在未来的某个时间停止支持,请尽量不要使用该特性。

设计原则验证:

  • 命令对象与执行对象分开,解耦
  • 符合开放封闭原则。

9. 备忘录模式

  • 随时记录一个对象的状态变化
  • 随时可以恢复之前的某个状态(如撤销功能)
  • 未找到JS中的经典应用,除了一些工具(如编辑器、在线笔记)

image.png

image.png

设计原则验证:

  • 状态对象与使用者分开,解耦
  • 符合开闭原则

10. 中介者模式

介模式定义了一个单独的(中介)对象,来封装一组对象之间的交互。 image.png

image.png

image.png

设计原则验证:

  • 将各关联对象通过中介者隔离
  • 符合开闭原则

11. 访问者模式和解释器模式

前后端基本都用不到。

image.png

image.png

5. 补充

5.1 什么是http-server

http-server是一个超轻量级web服务器,当我们想要在服务器运行一些代码,但是又不会配置服务器的时候,就可以使用http-server就可以搞定了。http-server可以将任何一个文件夹当作服务器的目录供自己使用。

使用方法:

  • 因为http-server 需要用npm安装,所以我们在使用前需要安装node.js
  • 全局安装 http-servernpm i http-server -g
  • 用终端打开打开需要作为服务器的文件夹,使用http-server启动,http-server -p 端口号

5.2 项目代码

github.com/zlh56201196…