Published on

JavaScript设计模式(上篇)

Authors
  • avatar
    Name
    Hansuku
    Twitter

JavaScript 设计模式

  • 解析 ES6 所需要的几个 babel 插件
npm install babel-core babel-loader babel-polyfill babel-preset-es2015

面向对象三要素

继承

class Person{
    constructor(name,age){
        this.name = name
        this.age = age
    }
    eat(){
        console.log(`${this.name} eat something`)
    }
    speak(){
        console.log(`My name is ${this.name}, age ${this.age}`)
    }
}
//继承
class Student extends Person{
    constructor(name,age,number){
        super(name,age)
        this.number = number
    }
    study(){
        console.log(`${this.number}号${this.name}同学正在学习`)
    }
}

let han = new Student('han',20,20)
han.eat()
han.speak()
han.study()

封装

//ts运行环境
class People {
    name
    age
    protected weight
    constructor(name, age) {
        this.name = name
        this.age = age
        this.weight = 120
    }
    eat(){
        console.log(`${this.name} eat something`)
    }
    speak(){
        console.log(`My name is ${this.name}, age ${this.age}`)
    }
}
//继承
class Student extends People{
    number
    private girlfriend
    constructor(name,age,number){
        super(name,age)
        this.number = number
        this.girlfriend = 'xiaoli'
    }
    study(){
        console.log(`${this.number}号${this.name}同学正在学习`)
    }
    getWeight() {
        console.log(`weight ${this.weight}`)
    }
}

let xiaoming = new Student('xiaoming', 20, 'A1')
xiaoming.getWeight()
console.log(xiaoming.girlfriend)//这里会报错 girlfriend是私有的
console.log(xiaoming.weight)//这里会报错 weight只能由类本身和子类来读取

多态

class People{
    constructor(name){
        this.name = name
    }
}
class A extends People{
    constructor(name){
        super(name)
        this.name = name
    }
    saySomething(){
        alert('I am A')
    }
}
class B extends People{
    constructor(name){
        super(name)
        this.name = name
    }
    saySomething(){
        alert('I am B')
    }
}
let a = new A()
a.saySomething()//I am A
let b = new B()
b.saySomething()//I am B

ES6 模拟 jQuery

class jQuery{
    constructor(seletor){
        let slice = Array.prototype.slice
        let dom = slice.call(document.querySelectorAll(seletor))//遍历dom获取seletor节点
        let len = dom ? dom.length : 0
        for(let i = 0;i < len;i++){
            this[i] = dom[i]
        }
        this.length = len
        this.seletor = seletor || ''
    }
    append(node){

    }
    addClass(name){

    }
    html(node){

    }
    //此处省略N个API
}
window.$ = function(seletor){
    return new jQuery(seletor)
}
var $p = $('p')
console.log($p)
console.log($p.addClass)

UML 类图

属性、方法前的+#-代表着公共、继承和私有

设计原则

设计模式=设计+模式 设计=设计原则

何谓设计

  • 即按照哪一种四路或者标准来实现业务功能
  • 功能相同,可以有不同的设计方案来实现
  • 伴随需求增加,设计的作用才能体现出来

设计准则

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

SOLID 五大设计原则

  • S 单一职责原则(single)
  • O 开放封闭原则(open-close)
  • L 李氏置换原则(Liskov Substitution Principle: LSP)
  • I 接口独立原则(interface)
  • D 依赖导致原则(Dependence Inversion Principle)

用 Promise 来说明 SO

function loadImg(src){
    let promise =  new Promise((resolve, reject) => {
        let img = document.createElement('img')
        img.onload = function(){
            resolve(img)
        }
        img.onerror = function(){
            reject('图片加载失败')
        }
        img.src = src
    });
    return promise
}
let src = 'https://www.hansuku.com/wp-content/themes/Lover/images/thumbs/5.jpg'
let result = loadImg(src)

result.then(img=>{
    //part1
    alert(`width:${img.width}`)
    return img
}).then(img=>{
    //part2
    alert(`height:${img.height}`)
}).catch(ex=>{
    alert(ex)
})
//每增加一个需求就加一个then,一个then只做一件事

从设计到模式

创建型
  • 工厂模式(工厂方法模式,抽象工厂模式,建造者模式)
  • 单例模式
  • 原型模式
结构型
  • 适配器模式
  • 装饰器模式
  • 代理模式
  • 外观模式
  • 桥接模式
  • 组合模式
  • 享元模式
行为型
  • 策略模式
  • 模板方法模式
  • 观察者模式
  • 迭代器模式
  • 职责连模式
  • 命令模式
  • 备忘录模式
  • 状态模式
  • 访问者模式
  • 中介者模式
  • 解释器模式

拟真题 1

打车时,可以打专车或者快车,任何车都有车牌号和名称 不同车价格不同,快车每公里 1 元,专车每公里 2 元 行程开始时,显示车辆信息 行程结束时,显示打车金额(假定行程为 5 公里)

class Car{
    constructor(name,car_num){
        this.name = name
        this.car_num = car_num
    }
}
class special_car extends Car{
    constructor(name,car_num){
        super(name,car_num)
        this.km_price = 2
    }
}
class express_car extends Car{
    constructor(name,car_num){
        super(name,car_num)
        this.km_price = 1
    }
}
class trip{
    constructor(car){
        this.car = car
    }
    start(){
        console.log(`行程开始,车型${this.car.name},车牌号${this.car.car_num}`)
    }
    end(){
        console.log('行程结束,价格:'+(this.car.km_price * 5))
    }
}
let car = new special_car('五菱宏光','浙A10086')
let Trip = new trip(car)
Trip.start()
Trip.end()

拟真题 2

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

class Car{
    constructor(num){
        this.num = num
    }
}
class ParkLot{
    constructor(floors){
        this.floors = floors || []
        this.carList = {} //存储拍摄返回的信息
        this.camera = new Camera()
        this.screen = new Screen()
    }
    emptyNum(){
        return this.floors.map(floor=>{
            return `${floor.index} 层还有 ${floor.emptyPlaceNum()} 个空闲车位`
        }).join('\n')
    }
    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]
    }
}
class Camera{
    constructor(car){
        this.car = car
    }
    shot(car){
        return{
            num: car.num,
            inTime: Date.now()
        }
    }
}
class Screen{
    show(car,inTime){
        console.log('车牌号',car.num)
        console.log('停车时间',Date.now() - inTime)
    }
}
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 ParkLot(floors)

//初始化车辆
const car1 = new Car(100)
const car2 = new Car(200)
const car3 = new Car(300)

console.log('第一辆车进入')
console.log(park.emptyNum())
park.in(car1)
console.log('第二辆车进入')
console.log(park.emptyNum())
park.in(car2)
console.log('第一辆车离开')
park.out(car1)
console.log('第三辆车进入')
console.log(park.emptyNum())
park.in(car3)
console.log('第二辆车离开')
park.out(car2)
console.log('第三辆车离开')
park.out(car3)
console.log(park.emptyNum())

工厂模式

工程模式意在把所有类都封装到一个创建者上,通过创建者调用类 类似jQuerywindow.$ = function(seletor){return new jQuery(seletor)}这里就是构造者,使用者不需要new jQuery("p")而可以直接调用$('p')

class Product{
    constructor(name){
        this.name = name
    }
    init(){
        alert('init')
    }
    fn1(){
        alert('fn1')
    }
    fn2(){
        alert('fn2')
    }
}
class Creator{
    create(name){
        return new Product(name)
    }
}
let creator = new Creator()
let p = creator.create('p1')
p.init()
p.fn1()

单例模式

单例模式意在每个 class 只能 new 一次 比如下面的代码 obj1 和 obj2 虽然都 new 了 SingleObject 但是他们两个是完全相等的 而简单的 JS 不能够实现类似 java 那样完全拒绝重复 new 下面的代码中,obj3 是一个 new 出来的 SingleObject,虽然不会报错,但是这个时候会发现 obj1 != obj3

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)

let obj3 = new SingleObject() //无法完全控制
obj3.login()
console.log(obj1 === obj3) //false

类似的场景就像我们在 jQuery 中声明了多次**$**或者是错误的引用了 jQuery 好几遍,但是我们使用的时候都只是在使用那一个,内部会有一个处理,如果检测到了当前挂载的内存中有 window.jQuery 那么就直接使用当前内存的 window.jQuery,否则新生成一个 这样就能保证 jQuery 在当前页面生命周期里永远只有一个实例 类似的还有 vuex 和 redux 里的 store

// jQuery只有一个`$`
if(window.jQuery != null){
    return window.jQuery
}else{
    // do init...
}
class LoginForm{
    constructor(){
        this.state = 'hide'
    }
    show(){
        if(this.state === 'show'){
            alert('已经显示')
            return
        }
        this.state = 'show'
        console.log('now show')
    }
    hide(){
        if(this.state === 'hide'){
            alert('已经隐藏')
            return
        }
        this.state = 'hide'
        console.log('now hide')
    }
}
LoginForm.getInstance = (function(){
    let instance
    return function(){
        if(!instance){
            instance = new LoginForm()
        }
        return instance
    }
})()

let login1 = LoginForm.getInstance()
login1.show() //正常执行

let login2 = LoginForm.getInstance()
login2.show() //alert('已经显示')

适配器模式

上图中,Adaptee 是需要被适配、转换的类 Target 是转换器,需要使用 Adaptee,然后request()暴露给外部

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 当我们要替换掉 jQuery,就得需要一个适配去适配它

//新接口
ajax({
    url:'/123',
    type:'post',
    dataType:'json',
    data:{
        id:"123"
    }
})
.done(function(){})

//老接口
$.ajax({
    url: '/123',
    type: 'post',
    data: {
	    id:"123"
    },
    dataType: 'json',
    success: function(res) {
    },
    error: function() {
    }
});

// 做一层适配器
var $ = {
    ajax:function(options){
        return ajax(options)
    }
}
//完成兼容

装饰器模式

class Circle{
    draw(){
        console.log('画一个圆形')
    }
}
class Decorator{
    constructor(circle){
        this.circle = circle
    }
    draw(){
        this.circle.draw()
        this.setRedBorder(circle)
    }
    setRedBorder(circle){
        console.log('设置红色边框')
    }
}

//test
let circle = new Circle()
circle.draw()

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

ES7 中拥有装饰器的功能,而且有一些特别好的库已经封装了装饰器(core-decorators) ES7 配置环境 安装兼容npm install babel-plugin-transform-decorators-legacy --save-dev .babelrc

{
    "presets":["es2015","latest"],
    "plugins": ["transform-decorators-legacy"]
}

然后测试 ES7 环境

@testDec
class Demo{

}
function testDec(target){
    target.isDec = true
}
console.log(Demo.isDec)
@decorator
class A {}
//等同于
class A {}
A = decorator(A) || A;

只读装饰器

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 = function(){   //这里会报错,name是只读的
//     alert(100)
// }
function log(target,name,descriptor){
    let oldValue = descriptor.value
    descriptor.value = function(){
        console.log(`calling ${name} with`,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)//会在返回相加值前打印arguments

使用插件预设的装饰器

//安装 npm install core-decorators --save
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('该接口即将废除')
    name(){
        return 'zhang san'
    }
}

let p = new Person()
console.log(p.name())//提示该接口即将废除

代理模式

class RealImg{
    constructor(filename){
        this.filename = filename
        this.loadFromDisk() //从硬盘中加载 模拟
    }
    display(){
        console.log('display'+this.filename)
    }
    loadFromDisk(){
        console.log('loading...'+this.filename)
    }
}

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

let proimg = new ProxyImg('1.png')
proimg.display()

ES6 proxy

//明星
let star = {
    name: '马天宇',
    age: 25,
    phone: '1351212121',
}

let agent = new Proxy(star,{
    get:function(target,key){
        if(key === 'phone'){
            //返回经纪人电话
            return '1357777777'
        }
        if(key === 'price'){
            //明星不报价 经纪人报价
            return 120000
        }
        return target[key]
    },
    set:function(target,key,val){
        if(key === 'customerPirce'){
            if(val < 100000){
                //最低 10万
                throw new Error('价格太低')
            }else{
                target[key] == value
                return true
            }
        }
    }
})

//test
console.log(agent.name)//马天宇
console.log(agent.age)//25
console.log(agent.phone)//1357777777
console.log(agent.price)//120000

agent.customerPirce = 90000//报错 价格太低

外观模式

外观模式意在为底层接口做一个高层接口,比如去一个医院看病,有人帮你把挂号、取号、排队等等操作都给你做了 而具体可以在定义函数传参的时候 如果你想省缺某个中间的函数 即在内部做一个转换 对外部不影响

function bindEvent(elem, type, selector, fn){
    if(fn == null){
        fn = selector
        selector = null
    }
    //coding
}

//use
bindEvent(elem, 'click', '#div', fn)
bindEvent(elem, 'click', fn)

谨慎:外观模式不符合单一职责原则和开放封闭原则 不可滥用

观察者模式

//主题,保存状态,状态发生之后触发所有观察者对象
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.subject = subject
        this.name = name
        this.subject.attach(this)
    }
    update(){
        console.log(`${this.name} update, state: ${this.subject.getState()}`)
    }
}
//test
let s = new Subject()
let o1 = new Observer('o1',s)
let o2 = new Observer('o2',s)
let o3 = new Observer('o3',s)
s.setState(1)
s.setState(2)
s.setState(3)

网页事件绑定

<button id ="btn1">btn</btn>
<script>
	$('#btn1').click(function(){
		console.log(1)
	})
	$('#btn1').click(function(){
		console.log(2)
	})
	$('#btn1').click(function(){
		console.log(3)
	})
</script>

所有 UI 界面的时间监听机制都是观察者模式

Promise

var src = 'https://image.biadu.com/1.png'
var result = loadImg(src)
result.then(function(img){
	console.log('width',img.width)
	return img
}).then(function(img){
	console.log('height',img.height)
})

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','xxxx')
// 继承
const EventEmitter = require('events').EventEmitter
class Doc extends EventEmitter{
	constructor(name){
		super()
		this.name = name
	}
}
let simon = new Dog('simon')
simon.on('brak',function(){
	console.log(this.name, 'barked_1')
})
simon.on('brak',function(){
	console.log(this.name, 'barked_2')
})
setInterval(function(){
	simon.emit('bark')
},1000)
//stream 用到自定义事件 fs分段加载文件
const fs = require('fs')
const readStream = fs.createReadStream('./a_big_file')

let length = 0
readStream.on('data',function(chunk){
	let len =  chunk.toString().length
	length += len
	console.log(len)
})
readStream.on('end',function(){
	console.log('length',length)
})
//stream 用到自定义时间 fs分行加载文件
const fs = require('fs')
const readline = require('readline')
let rl = readline.createInterface({
	input: fs.createReadStream('./a_big_file')
})
let lineNum = 0
rl.on('line',function(line){
	lineNum ++
})
rl.on('close',function(){
	console.log(lineNum)
})

更多的情况是自定义回调,扩展多进程通讯 Vue 和 React 组件生命周期的触发也是依靠观察者模式,当然 vue watch 也是

*/}