Javascript设计模式

148 阅读6分钟

Javascript 设计模式

面向对象

什么是面向对象

  • 概念

  • 三要素:继承 封装 多态

  • JS的应用举例

  • 面向对象的意义

概念-类


class People {

constructor(name, age) {

this.name = name

this.age = age

}

eat() {

alert(`${this.name} eat something`)

}

speak() {

alert(`${this.age} speak`)

}

}

三要素

  • 继承,子类继承父类

  • 封装, 数据的权限和保密

  • 多态,同一接口不同实现

三要素 - 继承

  • People 是父类,公共的,不仅仅服务于Student

  • 继承可将公共方法抽离出来,提高复用,减少冗余


// 子类继承父类

class Student extends People {

constructor(name, age, number) {

super(name, age)

this.number = number

}

study() {

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

}

}

三要素 - 封装

  • public 完全开放

  • protected 对子类开放

  • private 对自己开放

  • (ES6尚不支持,可以用typescript来掩饰)


// 父类

class People {

name

age

protected weight // 定义protected 属性

constructor(name, age) {

this.name = name

this.age = age

this.weight = 120

}

study() {

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

}

}

// 子类

class Student extends People {

number

private girlfriend // 定义 private属性

constructor(name, age, number) {

super(name, age)

this.number = number

this.girlfriend = 'xiaoli'

}

}

  • 减少耦合,不该外露的不外露

  • 利于数据、接口的权限管理

  • ES6目前不支持,一般认为_开头的属性是private

三要素 - 多态

  • 同一个接口,不同表现

  • JS应用极少

  • 需要结合java等语言的接口、重写、重载等功能

  • 保持子类的开放性和灵活性

  • 面向接口编程

  • (JS引用极少, 了解即可)

JS 应用举例

  • jQuery 是一个class

  • $('p') 是jQuery的一个实例

为何使用面向对象?

  • 程序执行:顺序、判断、循环 - 结构化

  • 面向对象 - 数据结构化

  • 对于计算机,结构化的才是最简单的

  • 编程应该 简单 & 抽象

UML类图

  • Unified Modeling Language 统一建模语言

  • 类图,UML包含很多种图,和本科相关的是类图

  • 关系,主要讲解泛化和关联

  • 演示,代码和类图结合

画图工具

类图


+ public 属性名A:类型

# protected属性名B:类型

- private 属性名C:类型

+ public 方法名:(参数1,参数2):返回值类型

# protected 方法名:(参数1,参数2):返回值类型

- private 方法名:(参数1,参数2):返回值类型

关系

  • 泛化,表示继承

  • 关联,表示引用

设计原则

  • 何为设计?

  • 五大设计原则

  • 从设计到模式

  • 介绍23种设计模式

何为设计?

  • 描述

  • 结合 《UNIX/LINUX设计哲学》

描述

  • 即按照哪一种思路或者标准来实现功能

  • 功能相同,可以有不同设计方案来实现

  • 伴随着需求增加,设计的作用才能体现出来

《UNIX/LINUX设计哲学》

  • 准则1:小即是美

  • 准则2:让每个程序只做好一件事

  • 准则3:快速建立原型

  • 准则4:舍弃高效率而取可移植性

  • 准则5:采用纯文本来存储数据

  • 准则6:充分利用软件的杠杆效应(软件复用)

  • 准则7:使用shell脚本来提高杠杆效应和可移植性

  • 准则8:避免强制性的用户界面

  • 准则9:让每个程序都称为过滤器

  • 小准则:允许用户定制环境

  • 小准则:尽量使操作系统内核小而轻量化

  • 小准则:使用小写字母并尽量简短

  • 小准则:沉默是金

  • 小准则:各部分之和大于整体

  • 小准则:寻求90%的解决方案

SOLID 五大设计原则

  • S - 单一职责原则

  • O - 开放封闭原则

  • L - 李氏置换原则

  • I - 接口独立原则

  • D - 依赖导致原则

S - 单一职责原则

  • 一个程序只做好一件事

  • 如果功能过于复杂就拆分开,每个部分保持独立

O - 开放封闭原则

  • 对扩展开放,对修改封闭

  • 增加需求时,扩展新代码,而非修改已有代码

  • 这是软件设计的终极目标

L - 李氏置换原则

  • 子类能覆盖父类

  • 父类能出现的地方子类就能出现

  • JS 中使用较少 (弱类型 & 继承使用较少)

I - 接口独立原则

  • 保持接口的单一独立,避免出现 "胖接口"

  • JS中没有接口(typescript例外),使用较少

  • 类似于单一职责原则,这里更关注接口

D - 依赖倒置原则

  • 面向接口编程,依赖于抽象而不依赖于具体

  • 使用方只关注接口而不关注具体类的实现

  • JS中使用较少(没有接口 & 弱类型)

设计原则总结

  • S O 体现较多,详细介绍

  • L I D 体现较少,但是要了解其用意

用promise来说明S O

  • 单一职责原则:每个 then中的逻辑只做好一件事

  • 开放封闭原则:如果新增需求,扩展then

  • 对扩展开放,对修改封闭

23种设计模式

  • 创建型

  • 组合型

  • 行为型

创建型

  • 工厂模式 (工厂方法模式,抽象工厂模式,建造者模式)

  • 单例模式

  • 原型模式

结构型

  • 适配器模式

  • 装饰器模式

  • 代理模式

  • 外观模式

  • 桥接模式

  • 组合模式

  • 享元模式

行为型

  • 策略模式

  • 模板方法模式

  • 观察者模式

  • 迭代器模式

  • 职责连模式

  • 命令模式

  • 备忘录模式

  • 状态模式

  • 访问者模式

  • 中介者模式

  • 解释器模式

面试题示例

第一题

  • 打车时,可以打专车或者快车。任何车都有车牌号和名称

  • 不同车价格不同,快车每公里1元,专车每公里2元。

  • 行程开始时,显示车辆信息

  • 行程结束时,显示打车金额(假定行程5公里)

  • 画出UML类图

  • 用ES6语法写出该示

第二题

  • 某停车场,分3层,每层100车位

  • 每个车位都能监控到车辆的驶入和离开

  • 车辆进入前,显示每层的空余车位数量

  • 车辆进入时,摄像头可识别车牌号和时间

  • 车辆出来时,出口吸纳时期显示车牌号和停车时长

工厂模式

  • 将new 操作单独封装

  • 遇到 new 时,就要考虑是否该使用工厂模式

示例

  • 你去购买汉堡,直接点餐、取餐,不会自己亲手做

  • 商店要 "封装"做汉堡的工作,做好直接给买者

设计原则验证

  • 构造函数和创建者分离

  • 符合开放封闭原则

单例模式

介绍

  • 系统中被唯一使用

  • 一个类只有一个实例

场景

  • jQuery 只有一个$

  • 模拟登录框

  • vuex和redux中的store

设计原则验证

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

  • 没法具体开放封闭原则,但是绝对不违反开放封闭原则

适配器模式

介绍

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

  • 中间加一个适配器转换接口

场景

  • 封装旧接口

  • vue computed

装饰器模式

介绍

  • 为对象添加新功能

  • 不改变其原有的结构和功能

场景

  • ES7 装饰器

  • core-decorators

ES7装饰器

  • 装饰类

  • 装饰方法


// 装饰类

@testDec

class Demo {

// ...

}

function testDec(target) {

target.isDec = true;

}

alert(Demo.isDec) // true


// 装饰类 有参数

function testDec(isDec) {

return function (target) {

target.isDec = isDec

}

}

@testDec(false)

class Demo {

}

alert(Demo.isDec)


function mixins(...list) {

return function (target) {

Object.assign(target.prototype, ...list)

}

}

const Foo = {

foo() { alert('foo') }

}

@mixins(Foo)

class MyClass{}

let obj = new MyClass()

obj.foo() // 'foo'


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}`

}

}


function log(target, name, descriptor) {

let oldValue = descriptor.value

descriptor.value = function () {

console.log(`calling ${name} width`, arguments)

return oldValue.apply(this, arguments)

}

return descriptor

}

class Math {

@log

add(a, b) {

return a + b

}

}

let math = new Math()

const result = math.add(2, 4)

console.log(result)

core-decorators

  • 第三方开源lib

  • 提供常用的装饰器


import { readonly } from 'core-decorators'

class Person {

@readonly

name() {

return 'zhang san'

}

}

let p = new Person()

alert(p.name())

p.name = function () {} // 报错


import { deprecate } from 'core-decorators'

class Person {

@deprecate('即将废除', {url: 'www.baidu.com'})

name() {

return 'zhangsan'

}

}

let p = new Person()

p.name()

设计原则验证

  • 将现有对象和装饰器进行分离,两者独立存在

  • 符合开放封闭原则

代理模式

介绍

  • 使用者无权访问目标对象

  • 中间加代理,通过代理做授权和控制

示例

  • 科学上网,访问github.com

  • 明星经纪人

场景

  • 网页事件代理

  • jQuery $.proxy

  • ES6 Proxy

网页事件代理

ES6 Proxy


// 明星

let star = {

name: '张XX',

age: 25,

phone: '13900001111'

}

// 经纪人

let agent = new Proxy(star, {

get: function (target, key) {

if (key === 'phone') {

// 返回经纪人自己的电话

return '16899997777'

}

if (key === 'price') {

// 明星不报价,经纪人报价

return 120000

}

return target[key]

},

set: function (target, key, val) {

if (key === 'customPrice') {

if (val < 100000) {

// 最低 10W

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 = 150000

console.log('agent.customPrice', agent.customPrice)

设计原则验证

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

  • 符合开放封闭原则

代理模式 vs 适配器模式

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

  • 代理模式:提供一模一样的接口

代理模式 vs 装饰器模式

  • 装饰器模式:扩展功能,原有功能不变且可直接使用

  • 代理模式:显示原有功能,但是经过限制或者阉割之后的

外观模式

介绍

  • 为子系统中的一组接口提供了一个高层接口

  • 使用者使用这个高层接口

观察者模式

介绍

  • 发布 & 订阅

  • 一对多

示例

  • 点咖啡,点好之后坐等被叫

场景

  • 网页事件绑定

  • Promise

  • jQuery callbacks

  • nodejs 自定义事件

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

  • vue和React 组件生命周期触发

  • vue watch

jQuery callbacks


// 自定义事件, 自定义函数

var callbacks = $.Callbacks() //注意大小写

callbacks.add(function (info) {

console.log('fn1', info)

})

callbacks.add(function (info) {

console.log('fn1', info)

})

callbacks.add(function (info) {

console.log('fn1', info)

})

callbacks.fire('gogogo')

callbacks.fire('fire')

nodejs 自定义事件


const EventEmitter = require('events').EventEmitter

const emitter1 = new EventEmitter()

// 监听 some 事件

emitter1.on('some', info => {

console.log('fn1', info)

})

// 监听 some 事件

emitter1.on('some', info => {

console.log('fn2', info)

})

// 触发 some 事件

emitter1.emit('some', 'xxxxx')

设计原则验证

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

  • 符合开放封闭原则

迭代器模式

介绍

  • 顺序访问一个集合

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

场景

  • jQuery each

  • ES6 Iterator

jQuery each


var arr = [1, 2, 3]

var nodeList = document.getElementsByTagName('a')

var $a = $('a')

function each(data) {

var $data = $(data) // 生成迭代器

$data.each(function (key, val) {

console.log(key, val)

})

}

each(arr)

each(nodeList)

each($a)

ES6 Iterator 为何存在?

  • ES6语法中,有序集合的数据类型已经有很多

  • Array Map Set String TypedArray arguments NodeList

  • 需要有一个统一的遍历接口来遍历所有数据类型

  • (注意,object不是有序集合,可以用Map代替)

ES6 Iterator 是什么?

  • 以上数据类型,都有[Symbol.iterator]属性

  • 属性值是函数,执行函数返回一个迭代器

  • 这个迭代器就有 next 方法可顺序迭代子元素

  • 可运行 Array.prototype[Symbol.iterator]来测试


function each(data) {

// 生成遍历器

let iterator = data[Symbol.iterator]()

// console.log(iterator.next())

// console.log(iterator.next())

// console.log(iterator.next())

// console.log(iterator.next())

// console.log(iterator.next())

// console.log(iterator.next())

// console.log(iterator.next())

let item = {done: false}

while(!item.done) {

item = iterator.next()

if (!item.done) {

console.log(item.value)

}

}

}

let arr = [1, 2, 3, 4]

let nodeList = document.getElementsByTagName('p')

let m = new Map()

m.set('a', 100)

m.set('b', 100

each(arr)


function each2(data) {

for(let item of data) {

console.log(item)

}

}

let arr = [1, 2, 3, 4]

let nodeList = document.getElementsByTagName('p')

let m = new Map()

m.set('a', 100)

m.set('b', 100

each2(arr)

ES6 Iterator 与 Generator

  • Iterator 的价值不限于上述几个类型的遍历

  • 还有Generator 函数的使用

  • 即只要返回的数据符合Iterator 接口的要求

  • 即可使用Iterator语法,这就是迭代器模式

设计原则验证

  • 迭代器对象和目标对象分离

  • 迭代器将使用者与目标对象隔离开

  • 符合开放封闭原则

状态模式

介绍

  • 一个对象有状态变化

  • 每次状态变化都会触发一个逻辑

  • 不能总是用if...else来控制

场景

  • 有限状态机

  • 写一个简单的Promise

有限状态机

  • 有限个状态、以及在这些状态之间的变化

  • 如交通信号灯

  • 使用开源 lib: javascript-state-machine


import StateMachine from 'javascript-state-machine'

import $ from 'jquery'

// 初始化状态机模型

let fsm = new StateMachine({

init: '收藏',

transitions: [

{

name: 'doStore',

from: '收藏',

to: '取消收藏'

},

{

name: 'deleteStore',

from: '取消收藏',

to: '收藏'

}

],

methods: {

// 监听执行收藏

onDoStore: function () {

alert('收藏成功') // 可以 post 请求

updateText()

},

// 监听取消收藏

onDeleteStore: function () {

alert('已经取消收藏') // 可以 post 请求

updateText()

}

}

})

let $btn = $('#btn1')

// 按钮点击事件

$btn.click(function () {

if (fsm.is('收藏')){

fsm.doStore()

} else {

fsm.deleteStore()

}

})

// 更新按钮的文案

function updateText() {

$btn.text(fsm.state)

}

// 初始化文案

updateText()

写一个简单的Promise

Promise 就是有限状态机

  • Promise 三种状态:pending fullfilled rejected

  • pending -> fullfilled 或者 pending -> rejected

  • 不能逆向变化


import StateMachine from 'javascript-state-machine'

// 模型

let fsm = new StateMachine({

init: 'pending', // 初始化状态

transitions: [

{

name: 'resolve', // 事件名称

from: 'pending',

to: 'fullfilled'

},

{

name: 'reject', // 事件名称

from: 'pending',

to: 'rejected'

}

],

methods: {

// 监听 resolve

onResolve: function (state, data) {

// state - 当前状态机实例; data - fsm.resolve(xxx) 传递的参数

},

// 监听 reject

onReject: function (state, data) {

// state - 当前状态机实例;data - fsm.reject(xxx) 传递的参数

}

}

})

// 定义 Promise

class MyPromise {

constructor(fn) {

this.successList = []

this.failList = []

fn(() => {

// resolve 函数

fsm.resolve(this)

}, () => {

// reject 函数

fsm.reject(this)

})

}

then(successFn, failFn) {

this.successList.push(successFn)

this.failList.push(failFn)

}

}

// 测试代码

function loadImg(src) {

const 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 = 'xxxx.jpg'

let result = loadImg(src)

result.then(function (img) {

console.log('ok1')

}, function () {

console.log('fail1')

})

result.then(function (img) {

console.log('ok2')

}, function () {

console.log('fail2')

})

其他设计模式

优先级划分依据

  • 不常用

  • 对应不到经典实现方式

原型模式

概念

  • clone 自己,生成一个新对象

  • java 默认有clone接口,不用自己实现

JS中的应用 - Object.create


// 一个原型对象

let prototype = {

getName: function () {

return this.first + ' ' + this.last

},

say: function () {

alert('hello')

}

}

// 基于原型创建 x

let x = Object.create(prototype)

x.first = 'A'

x.last = 'B'

alert(x.getName())

x.say()

// 基于原型创建 y

let y = Object.create(prototype)

y.first = 'C'

y.last = 'D'

alert(y.getName())

y.say()

对比JS中的原型 prototype

  • prototype 可以理解为ES6 class的一种底层原理

  • 而class 是实现面向对象的基础,并不是服务于某个模式

  • 若干年后ES6 全面普及,大家可能会忽略掉prototype

  • 但是Object.create 却会长久存在

桥接模式

概念

  • 用于把抽象化与实现化解耦

  • 使得二者可以独立变化

组合模式

概念

  • 生成树形结构,表示 "整体-部分" 关系

  • 让整体和部分都具有一致的操作方式

享元模式

概念

  • 共享内存(主要考虑内存,而非效率)

  • 相同的数据,共享使用

策略模式

概念

  • 不同策略分开处理

  • 避免出现大量 if ... else 或者 switch...case

模板方式模式和职责连模式

职责连模式-概念

  • 一步操作可能分为多个职责角色来完成

  • 把这些角色都分开,然后用一个链串起来

  • 将发起者和各个处理者隔离

JS中的链式操作

  • 职责链模式和业务结合较多,JS中能联想到链式操作

  • jQuery的链式操作 Promise.then 的链式操作

命令模式

概念

  • 执行命令时,发布者和执行者分开

  • 中间加入命令对象,作为中转站

备忘录模式

概念

  • 随时记录一个对象的状态变化

  • 随时可以恢复之前的某个状态(如撤销功能)

  • 未找到JS中经典应用,除了一些工具(如编辑器)

中介者模式


class A {

constructor() {

this.number = 0

}

setNumber(num, m) {

this.number = num

if (m) {

m.setB()

}

}

}

class B {

constructor() {

this.number = 0

}

setNumber(num, m) {

this.number = num

if (m) {

m.setA()

}

}

}

// 中介者

class Mediator {

constructor(a, b) {

this.a = a

this.b = b

}

setA() {

let number = this.b.number

this.a.setNumber(number * 100)

}

setB() {

let number = this.a.number

this.b.setNumber(number / 200)

}

}

// 测试

let a = new A()

let b = new B()

let m = new Mediator(a, b)

a.setNumber(100, m)

console.log(a.number, b.number)

b.setNumber(100, m)

console.log(a.number, b.number)

访问者模式

  • 数据操作和数据结构进行分离

  • 使用场景不多

解释器模式

  • 描述语言语法如何定义,如何解释和编译

  • 用于专业场景

关于面试

  • 能说出重点讲解的设计模式即可