1. 面向对象
1.1 搭建开发环境
主要步骤如下所示:
- 初始化
npm环境 - 安装
webpack - 安装
webpack-dev-server - 安装
babel
确保本地已经安装了 node 和 npm,查看其是否已经安装: node -v 和 npm -v
如果没有安装,去node官网或者npm官网下载一个稳定版本即可。
现在,我们开始搭建开发环境:
- 初始化
npm环境
npm init
-
新建
src文件夹,并在该文件夹下面新建index.js文件,内容为alert('hello world') -
为了提升安装速度,将
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
- 在根目录下新建
webpack.dev.config.js,代码如下:
module.exports = {
entry: './src/index.js',
output: {
path: __dirname, // 当前目录
filename: './release/bundle.js'
}
}
- 修改
package.json中的scripts命令,新增如下命令行:
"scripts": {
"dev": "webpack --config ./webpack.dev.config.js --mode development"
},
- 在终端运行命令
npm run dev,会生成release/bundle.js文件 - 安装,
npm i webpack-dev-server html-webpack-plugin
npm i webpack-dev-server html-webpack-plugin --save-dev
- 在根目录下创建
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>
- 修改
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,
}
}
- 修改
package.json文件中的 运行命令:
"scripts": {
"dev": "webpack-dev-server --config ./webpack.dev.config.js --mode development"
},
- 运行命令
npm run dev,浏览器会自动打开一个页面,如下图:
- 安装
babel插件
npm i -D @babel/core babel-loader @babel/preset-env @babel/preset-react
- 在根目录下新建
.babelrc文件,代码如下:
{
"presets": [
"@babel/preset-env",
"@babel/preset-react"
],
"plugins": []
}
- 在
webpack.dev.config.js中,增加babel编译js代码的配置,代码如下:
module.exports = {
......省略很多代码
module: {
rules: [{
test: /\.js?$/,
exclude: /(node_modules)/,
loader: 'babel-loader'
}]
},
plugins: [
......省略很多代码
],
}
- 修改
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的一个实例
总结: 面向对象的意义:数据结构化,具体如下:
- 程序执行:顺序、判断、循环 --- 结构化
- 面向对象 --- 数据结构化
- 对于计算机,结构化的才是最简单的
- 编程 应该 简单 && 抽象
1.3 UML类图
画图工具:
- MS Office visio
- 在线流程图思维导图:www.processon.com/
用法:
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):
用
ES6 语法写出该示例:
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):
代码:
// 车辆
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());
结果:
4. 设计原则
4.1 工厂模式
介绍
- 将new 操作单独封装
- 遇到new时,就要考虑是否该使用工厂模式
对应到生活的例子:
- 你去购买汉堡,直接点餐、取餐,不用自己亲手去做
- 商店要“封装”做汉堡的工作,做好直接给买家
UML图
代码:
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')
场景2:React.createElement()
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只有一个 $
2. 系统的登录
3. 其他场景(购物车(和登录框类似)、vuex和redux中的store)
- 购物车(和登录框类似)
vuex和redux中的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场景
这样,既可以方便扩展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示例:
装饰器模式之装饰方法
/**
* 装饰器模式
* 装饰方法
*/
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. 网页事件代理
2. jQuery $.proxy
为啥this不符合期望:因为this指向了Window。
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 门面模式(外观模式)
为子系统中的一组接口提供了一个高层接口,使用者调用这个高层接口,这个高层接口就是门面模式。比如,去医院看病,会有接待员帮我们去挂号、门诊、划价、取药等,这个就类似于门面模式。
门前模式在前端的应用,兼容不同个数的参数,支持传4个参数或者3个参数:
注意:门面模式不符合单一职责和开闭原则,因此谨慎使用,不可滥用。
4.7 观察者模式
- 发布&订阅
- 一对多 (一对一也行,别纠结在此)
/**
* 观察者模式
*/
// 主题,保存状态,状态变化之后触发所有观察者对象
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. 网页事件绑定、网页事件监听
网页事件绑定:
2. Promise
promise中的.then部分也属于观察者模式,一开始先订阅,等到promise状态改变之后再去执行.then里面的代码。
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
结果如下:
4. nodejs自定义事件
事件监听:
nodeJs中的Stream 、readline读取文件也用到了观察者模式:
5. 其他场景
nodejs中:处理http请求;多进程通讯等;
vue和React组件生命周期触发:
不管是React还是Vue,它的一个组件就是一个构造函数,生成一个组件就是生成一个实例,其各个生命周期都是在组件的不同状态下触发的,这也是一种观察者模式。
vue watch
- ......
设计原则验证:
- 主题和观察者分离,不是主动触发而是被动监听,两者解耦;
- 符合开放封闭原则。
4.8 迭代器模式
- 顺序访问一个集合
- 使用者无需知道集合的内部结构(封装)
/**
* 遍历器模式
*/
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
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}
/**
* 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语法,这就是迭代器模式。
设计原则验证:
- 迭代器对象和目标对象分离
- 迭代器将使用者与目标对象隔离开
- 符合开放封闭原则
4.9 状态模式
介绍
- 一个对象有状态变化
- 每次状态变化都会触发一个逻辑
- 不能总是用
if...else来控制
比如红绿灯变化。
/**
* 状态模式
*/
// 状态,比如红灯、绿灯、黄灯
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()
}
})
安装 jquery和javascript-state-machine,然后运行 npm run dev 即可。
Promise就是有限状态机
promise三种状态:pending、fullfilled、rejectedpending -> 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中的经典应用)
以上方法实现直白,但是扩展性差。利用桥接模式抽象实现,将形状和颜色分开实现,如下所示:
这样,代码就变得清晰、可扩展了。
3. 组合模式
介绍:
- 生成树形结构,表示“整体-部分”关系;
- 让整体和部分具有一致的操作方式。
演示:
- JS 经典应用中,未找到这么复杂的数据类型;
- 虚拟DOM中的vnode是这种形式,但数据类型简单;
- (用JS实现一个菜单,不算经典应用,而是与业务相关)
从图中可以看到,整体和单个节点的操作是一致的,整体和单个节点的数据结构也保持一致。
设计原则验证:
- 将整体和单个节点的操作抽象出来;
- 符合开放封闭原则。
4. 享元模式
- 共享内存(主要考虑节省内存,而非效率)
- 相同的数据,共享使用。
- (JS中未找到经典应用场景)
无限下拉列表,将事件代理到高层节点上,也算是享元模式思想的一种应用吧。
设计原则验证:
- 将相同的部分抽象出来;
- 符合开放封闭原则。
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. 模板方法模式
对一些有特殊顺序、特殊逻辑等的党发做一些封装或合并,输出一个统一的对外的方法,这就是模板方法模式。如图中的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. 命令模式
- 执行命令时,发布者和执行者分开;
- 中间加入命令对象,作为中转站。
发送者:发出命令,调用命令对象;
命令对象:接收命令,调用接受者对应接口;
接受者:执行命令。
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中的应用:
- 网页富文本编辑器操作,浏览器封装了一个命令对象;
- document.execCommand('bold')
- document.execCommand('undo')
注意:document.execCommand()已废弃: 该特性已经从 Web 标准中删除,虽然一些浏览器目前仍然支持它,但也许会在未来的某个时间停止支持,请尽量不要使用该特性。
设计原则验证:
- 命令对象与执行对象分开,解耦
- 符合开放封闭原则。
9. 备忘录模式
- 随时记录一个对象的状态变化
- 随时可以恢复之前的某个状态(如撤销功能)
- 未找到JS中的经典应用,除了一些工具(如编辑器、在线笔记)
设计原则验证:
- 状态对象与使用者分开,解耦
- 符合开闭原则
10. 中介者模式
介模式定义了一个单独的(中介)对象,来封装一组对象之间的交互。
设计原则验证:
- 将各关联对象通过中介者隔离
- 符合开闭原则
11. 访问者模式和解释器模式
前后端基本都用不到。
5. 补充
5.1 什么是http-server?
http-server是一个超轻量级web服务器,当我们想要在服务器运行一些代码,但是又不会配置服务器的时候,就可以使用http-server就可以搞定了。http-server可以将任何一个文件夹当作服务器的目录供自己使用。
使用方法:
- 因为
http-server需要用npm安装,所以我们在使用前需要安装node.js; - 全局安装
http-server:npm i http-server -g - 用终端打开打开需要作为服务器的文件夹,使用
http-server启动,http-server -p 端口号