前端面試之路六(Javascript設計模式篇)

kinshan發表於2018-12-21

設計原則(SOLID)

單一職責模式(S)

  • 一個程式只做好一件事
  • 如果功能過於複雜就拆分開,每個部分保持獨立

裡式替換原則(L)

  • 子類能覆蓋父類
  • 父類能出現的地方子類就能出現
  • JS中使用較少(弱型別&繼承使用較少)

開放封閉原則(O)

  • 對擴充套件開放對修改封閉
  • 增加需求時,擴充套件新程式碼,而非修改已有程式碼
  • 軟體設計的終極目標

介面隔離原則(I)

  • 保持介面的單一獨立,避免出現"胖介面"
  • JS中沒有介面(typescript例外),使用較少
  • 類似於單一職責所在,這裡更關注介面

依賴倒置原則(D)

  • 面向介面程式設計,依賴於抽象而不依賴於具體
  • 使用方只關注介面而不關注具體類的實現
  • JS中使用較少(沒有介面&弱型別)

設計模式

工廠模式

  • 將new操作單獨封裝
  • 遇到new時,就要考慮是否該使用工廠模式了

示例

你去購買漢堡,直接點餐、取餐,不會自己親手做
商店要“封裝”做漢堡的工作,做好直接給買者

UML類圖:

前端面試之路六(Javascript設計模式篇)

程式碼示例:

class Product {
    constructor(name) {
        this.name = name;
    }
    init() {
        console.log('init')
    }
    fn1() {
        console.log('fn1')
    }
    fn2() {
        console.log('fn2')
    }
}

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

let create = new Creator();
let p = create.create('p')
p.init()
p.fn1()
p.fn2()

複製程式碼

應用場景

  • jQuery:
    $('div')new $('div')有何區別?

  • 第一:書寫麻煩,jQuery的鏈式操作將成為噩夢

  • 第二:一旦jQuery名字變化,將是災難性的

//仿jQuery程式碼
class jQuery {
    constructor(selector) {
        let slice = Array.prototype.slice;
        let dom = slice.call(document.querySelectorAll(selector))
        let len = dom ? dom.length : 0
        for (let i = 0; i < len; i++) {
            this[i] = dom[i]
        }
        this.length = len
        this.selector = selector || ''
    }
    append() {
        console.log('append');
    }
    addClass() {
        console.log('addClass')
    }

}

window.$ = function(selector) {
    return new jQuery(selector);
}

var $p = $('p')
console.log($p)
console.log($p.addClass)

複製程式碼
  • React.crateElement:
var profile = <div>
    <img src="avater.png" className="profile"/>
    <h3>{[user.firstName,user.lastName].join('')}</h3>
    </div>;
複製程式碼

編譯完之後:

var profile = React.createElement("div",null,
    React.createElement("img",{src:"avater.png",className:"profile"}),
    React.createElement("h3",null,[user.firstName,user.lastName].join(" "))
);
複製程式碼
//原始碼實現
class vnode(tag, attrs, children) {
    //...省略內部程式碼...
}

React.createElement = function(tag,attrs,children){
    return new vnode(tag,attrs,children)
}
複製程式碼
  • Vue的非同步元件:
Vue.component('async-example', funciton(resolve, reject) {
    setTimeout(function() => {
        resolve({
            template: '<div>I am async!</div>'
        })
    }, 1000);
})
複製程式碼

設計原則驗證:

  • 建構函式和建立者分離
  • 符合開放封閉原則

單例模式

  • 系統中被唯一使用
  • 一個類中只有一個例項

 

例項:

登入框、購物車

傳統UML圖

前端面試之路六(Javascript設計模式篇)

說明

  • 單例模式需要用到java的特性(private)
  • ES6中沒有(typescript除外)
  • 只能用java程式碼來演示UML圖的內容(最後用js變相實現)

程式碼演示

java版的單例模式演示

public class SingleObject{
     //注意:私有化建構函式,外部不能new,只能內部new!!!!
     private SingleObject(){}
     //唯一被new出來的物件
     private SingleObject getInstance(){
         if(instance == null){
             //只new一次
             instance = new SingleObject();
         }
         return instance;
     }
     //物件方法
     public void login(username,password){
         System.out.println("login...")
     }
 }
 
 
 public class SingletonPatternDemo{
     public static void main(String[] args){
        //不合法的建構函式
        //編譯時報錯:建構函式 SingleObject()是不可見的!!!
        //SingleObject object = new SingleObject();
        //獲取唯一可用的物件
        SingleObject object = SingleObject.getInstance();
     }     
}
複製程式碼

Javascript版的單例模式演示

class SingleObject {
    login() {
        console.log('login...')
    }
}

//靜態方法
SingleObject.getInstance = (function() {
    let instance
    return function() {
        if (!instance) {
            instance = new SingleObject();
        }
        return instance;
    }
})()

var login = SingleObject.getInstance().login();
複製程式碼

javascript的單例模式缺點:

如果強制new也不會報錯:

var loginnew = new SingleObject();
loginnew.login()
複製程式碼

測試

//注意這裡只能用靜態函式getInstance,不能new SingleObject()!!!
let obj1 = SingleObject.getInstance()
obj1.login()
let obj2 = SingleObject.getInstance()
obj2.login()
console.log(obj1 === obj2); //兩者必須完全相同

複製程式碼

只有通過模組化完整實現

場景

  • jQuery 只有一個'$'
if(window.jQuery != null){
    return window.jQuery
}else{
    //初始化...
}
//引用多少次都只有一個'$'
複製程式碼
  • vuex 和 redux中的store

  • 購物車、登入框

class LoginForm {
    constructor() {
        this.state = 'hide'
    }
    show() {
        if (this.state === 'show') {
            alert('已經顯示了');
            return
        }
        this.state = 'show'
        console.log('登入框顯示成功')
    }
    hide() {
        if (this.state === 'hide') {
            alert('已經隱藏')
            return
        }
        this.state = 'hide'
        console.log('登入框隱藏成功')
    }
}
LoginForm.getInstance = (function() {
    let instance
    return function() {
        if (!instance) {
            instance = new LoginForm()
        }
        return instance
    }
})()

let login1 = LoginForm.getInstance()
login1.show()

let login2 = LoginForm.getInstance()
//lgoin2.show() //登入框已經顯示
login2.hide()

console.log(login1 === login2)
複製程式碼

設計原則驗證

  • 符合單一職責原則,只例項化唯一的物件
  • 沒法具體開放封閉原則,但是絕對不違反開放封閉原則

介面卡模式

  • 舊介面格式和使用者不相容
  • 中間加一個適配轉換介面

示例

macbookpro介面卡轉換 電源插座國家不統一需要轉換頭

UML圖

前端面試之路六(Javascript設計模式篇)

演示

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({
    url:'/getDate',
    type:'Post',
    dataType:'json',
    data:{
        id:123
    }
})
.done(function(){})
複製程式碼
//但因為歷史原因,程式碼中全都是:
//$.ajax({...})
複製程式碼

解決辦法:

//做一層介面卡
var $ = {
    ajax:function(options){
        return ajax(options)
    }
}
複製程式碼
  • vue computed
<div id="example">
    <p>Original message:"{{message}}"</p>
    <p>Computed reversed message:"{{reversedMessage}}"</p>
</div>

var vm = new Vue({
    el:"#example",
    data:{
        mesage:'Hello'
    },
    computed:{
        //計算屬性的getter
        reversedMessage:function(){
            //'this'指向vm例項
            return this.message.split('').reverse().join('')
        }
    }
})
複製程式碼

設計原則驗證

  • 將舊介面和使用者進行分離
  • 符合開放封閉原則

裝飾器模式

  • 為物件新增新功能
  • 不改變其原有的結構和功能

示例:

手機殼

UML類圖

前端面試之路六(Javascript設計模式篇)

程式碼演示

class Circle {
    draw() {
        console.log('畫一個圓形')
    }
}

class Decorator {
    constructor(circle) {
        this.circle = circle
    }
    draw() {
        this.circle.draw()
        this.setRedBorder(circle)
    }
    setRedBorder(circle) {
        console.log('設定紅色邊框')
    }
}

let circle = new Circle();
circle.draw()

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

複製程式碼

使用場景

  • ES7裝飾器
@testDec
class Demo {
    //...
}

function testDec(target) {
    target.isDec = true;
}
alert(Demo.isDec);
複製程式碼

裝飾器原理

@decorator
class A {}

//等同於
class A{}
A = decorator(A)||A;

複製程式碼

可以加引數

function testDec(isDec){
    return function(target){
        target.isDec = isDec;
    }
}

@testDec(true)

class Demo{
    //....
}
alert(Demo.isDec) //true
複製程式碼
function mixin(...list) {
    return function(target) {
        Object.assign(target.prototype, ...list)
    }
}

const Foo = {
    foo() { alert('foo') }
}

@mixin(Foo)
class myClass() {}

let obj = new myClass();
obj.foo() //'foo'
複製程式碼

裝飾方法-例1

class Person {
    constructor() {
            this.first = 'A'
            this.last = 'B'
        }
        //裝飾方法
    @readonly
    name() {
        return `${this.first} ${this.last}`
    }
}

var p = new Person()
console.log(p.name())   //p.name=function(){} //這裡會報錯,因為name是隻讀屬性

function readonly(target, name, descriptor) {
    //descriptor 屬性描述物件(Object.defineProperty中會用到),原來的值如下
    //{
    //  value:specifiedFunction,
    //  enumerable:false,
    //  configurable:true,
    //  writable:true   
    //}
    descriptor.writable = false;
    return descriptor;
}
複製程式碼

裝飾方法-例2

class Math{
    //裝飾方法
    @log
    add(a,b){
        return a + b;
    }
}

const math = new Math();
const result = math.add(2,4);  //執行add時,會自動列印日誌,因為有@log裝飾器
console.log('result',result)

function log(target, name, descriptor) {
    var oldvalue = descriptor.value;

    descriptor.value = function() {
        console.log(`calling ${name} with`, arguments);
        return oldvalue.apply(this, arguments)
    }
    return descriptor;
}
複製程式碼
  • core-decorators
  • 第三方開源lib
  • 提供常用的裝飾器
//首先安裝npm i core-decorators --save

//開始編碼
import { readonly } from 'core-decorators'

class Person {
    @readonly
    name() {
        return 'zhang'
    }
}

let p = new Person()
alert(p.name())
    //p.name = function(){/*...*/} 此處會報錯
複製程式碼
import { deprecate } from 'core-decorators'

class Person {
    @deprecate
    name() {
        return 'zhang'
    }
}

let p = new Person()
alert(p.name())
    //this funciton will be removed in future Vue.version
    //也可以自己定義@deprecate("即將廢用")
    //也可以自己定義@deprecate("即將廢用",{url:"www.imooc.com"})
複製程式碼

設計原則驗證

  • 將現有物件和裝飾器進行分離,兩者獨立存在
  • 符合開放封閉原則

代理模式

  • 使用者無權訪問目標物件
  • 中間加代理,通過代理做授權和控制

示例:

  • 科學上網
  • 明星經紀人

UML

前端面試之路六(Javascript設計模式篇)

程式碼演示

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 proxyImg = new ProxyImg('1.png')
proxyImg.display()
複製程式碼

場景

  • 網頁事件代理
 var div1 = document.getElementById('div1')
 div1.addEventListener('click', funtion(e) {
    console.log(e)
    var target = e.target
    if (target.nodeName === "A") {
        alert(target.innerHtml)
    }
})
複製程式碼
  • jQuery $.proxy
$('#div1').click(function() {
    //this符合期望
    $(this).addClass('red')
})
$('#div1').click(function() {
    setTimeout(function() {
        //this不符合期望
        $(this).addClass('red')
    }, 1000);
})

複製程式碼
//可以用如下方式解決
$('#div1').click(function() {
    var _this = this
    setTimeout(funciton() {
        //_this符合期望
        $(_this).addClass('red')
    }, 1000)
})
複製程式碼

或者用$.proxy

//但推薦用$.proxy解決,這樣就少定義一個變數
$('#div1').click(function() {
    setTimeout($.proxy(function() {
        //this符合期望
        $(this).addClass('red')
    },this), 1000)
})
複製程式碼
  • ES6 Proxy
//明星
let star = {
    name: "zhangxx",
    age: 25,
    phone: '13910733521',
}

//經紀人
let agent = new Proxy(star, {
    get: function(target, key) {
        if (key === 'phone') {
            //返回經紀人自己的手機號
            return '13838383838'
        }
        if (key === "price") {
            //明星不報價,經紀人報價
            return 120000
        }
        return target[key]
    },
    set: function(target, key, val) {
        if (key === 'customPrice') {
            if (val < 100000) {
                throw new Error("價格太低")
            } else {
                target[key] = val
                return true
            }
        }
    }
})

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

agent.customPrice = 150000;
console.log('agent.customPrice', agent.customPrice)

複製程式碼

設計原則驗證

  • 代理類和目標類分離,隔離開目標類和使用者
  • 符合開放封閉原則

代理模式VS介面卡模式

  • 介面卡模式:提供一個不同的介面(如不同版本的插頭,無法使用)
  • 代理模式:提供一模一樣的介面(無權使用)

代理模式VS裝飾器模式

  • 裝飾器模式:擴充套件功能,原有功能不變且可直接使用
  • 代理模式:直接針對(顯示)原有功能,但是經過限制或者閹割之後的

外觀模式

  • 為子系統中的一組介面提供了一個高層介面
  • 使用者使用這個高層介面

示例:

去醫院看病,接待員去掛號、門診、劃價、取藥

前端面試之路六(Javascript設計模式篇)

UML類圖

前端面試之路六(Javascript設計模式篇)

程式碼演示

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

//呼叫
bindEvent(elem,'click','#div1',fn)
bindEvent(elem,'click',fn)
複製程式碼

設計原則驗證

  • 不符合單一職責原則和開放封閉原則,因此謹慎使用,不可濫用

觀察者模式

  • 釋出&訂閱
  • 一對多(N)

示例

  • 點咖啡,點好之後坐等被叫

UML類圖

前端面試之路六(Javascript設計模式篇)
前端設計最重要的一種模式

程式碼演示

//儲存狀態,狀態變化之後觸發所有觀察者
class Subject {
    constructor() {
        this.state = 0
        this.observers = []
    }
    getState() {
        return this.state
    }
    setState(state) {
        this.state = state
        this.notifyAllObervers()
    }
    notifyAllObervers() {
        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()}`)
    }
}

let subject = new Subject();
let obs1 = new Observer('o1', subject);
let obs2 = new Observer('o2', subject);
let obs3 = new Observer('o3', subject);

subject.setState(1)
subject.setState(2)

複製程式碼

應用場景

  • 網頁事件繫結

所有的事件監聽用的都是觀察者模式

<button id="btn1">btn</button>

<script>
    $('#btn1').click(function () {
        console.log(1)
    })
    $('#btn1').click(function () {
        console.log(2)
    })
    $('#btn1').click(function () {
        console.log(2)
    })
</script>
複製程式碼
  • Promise
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 src = "https://www.xxx.com/img/dafdafdfdafdsafd.png"
var result = loadImg()
result.then(function(img){
    console.log('width',img.width)
}).then(function(img){
    console.log('width',img.height)
})

複製程式碼
  • jQuery callbacks
var callbacks = $.Callbacks() //注意大小寫
callbacks.add(function() {
    console.log('fn1', info)
})
callbacks.add(function() {
    console.log('fn2', info)
})
callbacks.add(function() {
    console.log('fn3', info)
})
callbacks.fire('gogoogogo')
callbacks.fire('fire')
複製程式碼
  • nodejs自定義事件

cosnt EventEmitter = require('events').EventEmitter
const emitter1 = new EventEmitter()
emitter1.on('some', () => {
    //監聽some事件
    console.log('some events is occured 1')
})

emitter1.on('some', () => {
        //監聽some事件
        console.log('some events is occured 2')
    })
    //觸發some事件
emitter1.emit('some')
複製程式碼
const EventEmitter = require('events').EventEmitter

//任何建構函式都可以繼承 EventEmitter的方法on emit

class Dog extends EventEmitter {
    constructor(name) {
        super()
        this.name = name
    }
}

var simon = new Dog('simon')
simon.on('bark', function() {
    console.log(this.name, 'barked')
})
setInterval(() => {
    simon.emit('bark')
}, 500)

複製程式碼
//Stream 用到了自定義事件

var fs = require('fs')
var readStream = fs.createReadStream('./data/file1.txt') //讀取檔案的stream

var length = 0
readStream.on('data', function(chunk) {
    length += chunk.toString().length
})

readStream.on('read', function() {
    console.log(length)
})

複製程式碼
//readline用到了自定義事件

var readline = require('readline')
var fs = require('fs')

var rl = readline.createInterface({
    input: fs.createReadStream('./data/file1.txt')
});

var lineNum = 0
rl.on('line', function(line) {
    lineNum++
})
rl.on('close', function() {
    console.log('lineNum', lineNum)
})

複製程式碼
  • nodejs中:處理http請求;多程式通訊
function serverCallback(req, res) {
    var method = req.method.toLowerCase() //獲取請求方法
    if (method === 'get') {
        //省略3行,上文程式碼示例中處理GET請求的程式碼
    }
    if (method === 'post') {
        //接受post請求的內容
        var data = ''
        req.on('data', function() {
            //"一點一點"接收內容
            data += chunk.toString()
        })
        req.on('end', function() {
            //接收完畢,將內容輸出
            res.writeHead(200, { 'Content-type': 'text/html' })
            res.write(data)
            res.end()
        })
    }
}
複製程式碼
//parent.js
var cp = require('child_process')
var n = cp.fork('./sub.js')
n.on('message', function(m) {
    console.log('PARENT got message:' + m)
})
n.send({ hello: 'workd' })

//sub.js
process.on('message', function(m) {
    console.log('CHILD got message:' + m)
})

process.send({ foo: 'bar' })

複製程式碼
  • vue和React元件生命週期觸發
class login extends React.component {
    constructor(prop, context) {
        super(props, context)
        this.shouldComponentUpate = PureRenderMixin.shouldComponentUpate.bind(this);
        this.state = {
            checking: true
        }
    }
    render() {
        return ( 
            <div>
                <header title = "登入" history = { this.props.history } ></header> 
            </div >
        )
    }
    componentDidMount() {
        //判斷是否已經登入
        this.doCheck()
    }
}
複製程式碼
  • vue watch
var vm = new Vue({
    el: "#demo",
    data: {
        firstName: 'Foo',
        lastName: 'Bar',
        fullName: 'Foo Bar'
    },
    watch: {
        firstName: function(val) {
            this.fullName = val + '' + this.lastName
        },
        lastName: function(val) {
            this.fullName = this.firstName + '' + val
        }
    }
})
複製程式碼

設計原則驗證

  • 主題和觀察者分離,不是主動觸發而是被動監聽,兩者解耦
  • 符合開放封閉原則

迭代器模式

  • 順序訪問一個集合
  • 使用者無需知道集合的內部結構(封裝)

示例

  • 沒有合適的示例,jQuery演示一下
<p>jQuery each</p>
<p>jQuery each</p>
<p>jQuery each</p>
複製程式碼
var arr = [1,2,3]
var nodeList = document.getElementsByTagName('p')
var $p = $('p')

//要對這三個物件進行遍歷,要寫三個遍歷方法
arr.forEach(function(item){
    console.log(item)
})

//nodeList不是純陣列
var i,length = nodeList.length;
for(i;i<length;i++){
    console.log(nodeList[i])
}

$p.each(function(key,p){
    console.log(key,p)
})

//順序遍歷有序集合
//使用者不必知道集合的內部結構
function each(data) {
    var $data = $(data) //生成迭代器
    $data.each(function(key, value) {
        console.log(key,value)
    })
}

each(arr)
each(nodeList)
each($p)


複製程式碼

UML類圖

前端面試之路六(Javascript設計模式篇)

程式碼演示

class Iterator {
    constructor(container) {
        this.list = container.list;
        this.index = 0;
    }
    next() {
        if (this.hasNext()) {
            return this.list[this.index++]
        }
    }
    hasNext() {
        if (this.index >= this.list.length) {
            return false;
        }
        return true;
    }
}

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

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

複製程式碼

應用場景

  • jQuery each
function each(data){
    var $data = $(data)   //生成迭代器
    $data.each(function(key,p){
        console.log(key,p)
    })
}
複製程式碼
  • ES6 Iterator
  • ES6語法中,有序集合的資料型別已經有很多
  • ArrayMapSetStringTypedArrayargumentNodeList
  • 以上資料型別都有[Symbol.Iterator]屬性
  • 屬性值是函式,執行函式返回一個迭代器
  • 這個迭代器就有next方法可順序迭代子元素
  • 可執行Array.prototype[Symbol.iterator]來測試
  • for...of 消費 iterator
  • ES6 Iterator與Generator
  • iterator的價值不限於尚書幾個型別的遍歷,還有Generator函式的使用
  • 即只要返回的資料符合Iterator介面的要求
  • 即可使用Iterator語法,這就是迭代器模式

設計原則驗證

  • 迭代器物件和目標物件分離
  • 迭代器將使用者與目標物件隔離開
  • 符合開放封閉原則

狀態模式

  • 一個物件有狀態變化
  • 每次狀態變化都會觸發一個邏輯
  • 不能總是用if...else來控制

示例

交通訊號燈不同顏色的變化

UML類圖

前端面試之路六(Javascript設計模式篇)

程式碼演示

//狀態
class State {
    constructor(state) {
        this.state = state
    }
    getState() {
        return this.state
    }
    handle(context) {
        console.log(`turn to ${this.state} 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')
let yellow = new State('yellow')
let red = new State('red')

green.handle(context);
console.log(context.getState())

yellow.handle(context);
console.log(context.getState())

red.handle(context);
console.log(context.getState())

複製程式碼

應用場景

  • 有限狀態機
  • 有限個狀態,以及在這些狀態之間的變化,如交通訊號燈
  • 使用開源lib:javascript-state-machine
//狀態機模型
import StateMachine from 'javascript-state-machine'

var fsm = new StateMachine({
    init: '收藏', //初始狀態,待收藏
    transitions: [{
            name: 'doStore',
            from: '收藏',
            to: '取消收藏'
        },
        {
            name: 'deleteStore',
            from: '取消收藏',
            to: '收藏'
        }
    ],
    method: {
        //執行收藏
        onDoStore: function() {
            alert('收藏成功')
            updateText()
        },
        onDeleteStore: function() {
            alert('取消收藏')
            updateText()
        }
    }

})


var $btn = $('#btn');
    //點選事件
$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,不可逆向變化
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)
    }
}

//模型
var fsm = new StateMachine({
    init: 'pending',
    transitions: [{
        name: 'resolve',
        from: 'pending',
        to: 'fullfilled'
    }, {
        name: 'reject',
        from: 'pending',
        to: 'rejected'
    }],
    methods: {
        onResolve: function(state, data) {
            //引數state - 當前狀態示例;data - fsm,resolve(xxx)執行時傳遞過來的引數
            data.successList.forEach(fn => fn());
        },
        onReject: function(satte, data) {
            //引數state - 當前狀態示例;data-fsm.reject(xxx)執行時傳遞過來的引數
            data.failList.forEach(fn => fn())
        }
    }
})


function loadImg(src) {
    const promise = new MyPromise(function(resolve, reject) {
        let img = document.createElement('img');
        img.onload = function() {
            resolve(img)
        }
        img.onerror = function() {
            reject(img)
        }
        img.src = src
    })
    return promise
}

let src = "https://www.xxxx.com/dsadfa/dafdafd.png";
let result = loadImg(src);
result.then(function() {
    console.log('ok1')
}, function() {
    console.log('fail1')
})
result.then(function() {
    console.log('ok2')
}, function() {
    console.log('fail2')
})
複製程式碼

設計模式驗證

  • 將狀態物件和主題物件分離,狀態的變化邏輯單獨處理
  • 符合開放封閉原則

其他設計模式

  • 不常用
  • 對應不到經典場景

建立型:

  • 原型模式

結構型:

  • 橋接模式
  • 組合模式
  • 享元模式

行為型

  • 策略模式
  • 模板方法模式
  • 職責鏈模式
  • 命令模式
  • 備忘錄模式
  • 中介者模式
  • 訪問者模式
  • 直譯器模式

原型模式

  • clone自己,生成新物件(new開銷比較大)
  • java預設有clone介面,不用自己實現

使用場景

Object.create

  • Object.create用到了原型模式的思想(雖然不是java中的clone)
//基於一個原型建立一個物件
const prototype = {
    getName: function() {
        return this.first + '' + this.last;
    },
    say: function() {
        console.log('hello')
    }
}


//基於原型建立x
var x = Object.create(prototype)
x.first = 'A'
x.last = 'B'
console.log(x.getName())
x.say()

//基於原型建立y
var y = Object.create(prototype)
y.first = 'C'
y.last = 'D'
console.log(y.getName())
y.say()
複製程式碼

對比JS中的原型prototype

  • prototype 可以理解為ES6 class的一種底層原理
  • class是實現物件導向的基礎,並不是服務於某個模式

橋接模式

  • 用於把抽象化與實現化解耦
  • 使得兩者可以獨立變化
  • 在一些業務中比較常用

應用場景

前端面試之路六(Javascript設計模式篇)

//普通實現
class ColorShape {
    yellowCircle() {
        //...畫黃圓
    }
    redCircle() {
        //...畫紅圓
    }
    yellowTriangle() {
        //...畫黃三角形
    }
    redTriangle() {
        //...畫紅三角形
    }
}
//測試
let cs = new ColorShape()
cs.yellowCircle()
cs.redCircle()
cs.yellowTriangle()
cs.redTriangle()
複製程式碼

前端面試之路六(Javascript設計模式篇)

//橋接模式
class Color {
    constructor(color) {
        this.color = color;
    }
}
class Shape {
    constructor(name, color) {
        this.name = name;
        this.color = color;
    }
    draw() {
        //畫圖...
    }
}
//測試程式碼
let red = new Color("red")
let yellow = new Color("yellow")
let circle = new Shape('circle', red)
circle.draw()
let triangle = new Shape('triangle', yellow)
triangle.draw()

複製程式碼

顏色和圖形自由組合,複雜性少很多,後面增加圖形也很好處理

設計原則驗證

  • 抽象和實現分離,解耦
  • 符合開放封閉原則

組合模式

  • 生成樹形結構,表示“整體-部分”關係
  • 讓整體和部分都具有一致的操作方式

示例:

前端面試之路六(Javascript設計模式篇)

應用場景

  • 虛擬DOM中的vnode是這種形式,但資料型別簡單
  • 用JS實現一個選單資料夾管理,不算經典應用,與業務相關
<div id="div1" class="container">
    <p>123</p>
    <p>456</p>
</div> 
複製程式碼
{
    tag: 'div',
    attr: {
        id: 'div1',
        className: 'container'
    },
    children: [{
        tag: 'p',
        attr: {},
        children: ['123']
    }, {
        tag: 'p',
        attr: {},
        children: ['456']
    }]
}
複製程式碼
  • 整體和單個節點的操作是一致的
  • 整體和單個節點的資料結構也保持一致
  • 設計原則驗證
  • 將整體和單個節點的操作抽象出來
  • 符合開放封閉原則

享元模式

  • 共享記憶體(主要考慮記憶體,而非效率)
  • 相同的資料,共享使用

JS中不用太多考慮記憶體開銷

演示

//無限下拉選單,將事件代理到高層次節點上
//如果都繫結到'<a>'標籤,對記憶體開銷大

<div id="div1">
    <a href="#">a1</a>
    <a href="#">a2</a>
    <a href="#">a3</a>
    <a href="#">a4</a>
    <!--無限下拉選單-->
</div>

< script >
    var div1 = document.getElementById('div1')
    div1.addEventListener('click', function(e) {
        var target = e.target
        if (e.nodeName === 'A' {
            alert(target.innerHtml)
        })
    }) 
</script>
複製程式碼

設計原則驗證

  • 將相同的部分抽象出來
  • 符合開放封閉原則

策略模式

  • 不同策略分開處理
  • 避免出現大量if...else或者switch...case

演示

class User {
    constructor(type) {
        this.type = type
    }
    buy() {
        if (this.type === 'oridinary') {
            console.log('普通使用者購買')
        } else if (this.type === 'member') {
            console.log('會員使用者購買')
        } else if (this.type === 'vip') {
            console.log('vip使用者購買')
        }
    }
}

//測試程式碼
var u1 = new User('oridinary')
u1.buy()
var u2 = new User('member')
u2.buy()
var u3 = new User('vip')
u3.buy()

複製程式碼

改成下面這種形式:

class OrdinaryUser {
    buy() {
        console.log('普通使用者購買')
    }
}
class MemberUser {
    buy() {
        console.log('會員使用者購買')
    }
}
class VipUser {
    buy() {
        console.log('vip使用者購買')
    }
}

var u1 = new OrdinaryUser()
u1.buy()

var u2 = new MemberUser()
u2.buy()

var u3 = new VipUser()
u3.buy()
複製程式碼

設計原則驗證

  • 不同策略,分開處理,而不是混合在一起
  • 符合開放封閉原則

模板方法模式和職責鏈模式

模板方法模式:

class Action {
    handle() {
        handle1();
        handle2();
        handle3();
    }
    handle1() {
        console.log('1')
    }
    handle2() {
        console.log('2')
    }
    handle3() {
        console.log('3')
    }
}
複製程式碼

職責鏈模式

  • 一步操作可能分為多個職責角色來完成
  • 把這些角色都分開,然後用一個鏈串起來
  • 將發起者和各個處理者進行隔離
演示:
//請假審批,需要組長審批、經理審批、最後總監審批
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的鏈式操作

設計原則驗證

  • 發起者和各個處理者進行隔離
  • 符合開放封閉原則

命令模式

  • 執行命令時,釋出者和執行者分開
  • 中間加入命令物件,作為中轉站

前端面試之路六(Javascript設計模式篇)

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

class Command {
    constructor(receiver) {
        this.receiver = receiver
    }
    cmd() {
        console.log('觸發命令')
        this.receiver.exec()
    }
}

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

let soldier = new Receiver()
let trumpeter = new Command(soldier)
let general = new Invoke(trumpeter)
general.invoke()
複製程式碼

應用場景

  • 網頁富文字編輯器操作,瀏覽器封裝了一個命令物件
  • document.execCommand("bold")
  • document.execCommand("undo")

設計原則驗證

  • 命令物件與執行物件分開,解耦
  • 符合開放封閉原則

備忘錄模式

  • 隨時記錄一個物件的狀態變化
  • 隨時可以恢復之前的某個狀態(如撤銷功能)

演示

一個編輯器

//備忘類
class Memento {
    constructor(content) {
        this.content = content;
    }
    getContent() {
        return this.content;
    }
}

//備忘列表
class CareTaker {
    constructor() {
        this.list = [];
    }
    add(memento) {
        this.list.push(memento)
    }
    get(index) {
        return this.list[index]
    }
}

//編輯器
class Editor {
    constructor() {
        this.content = null
    }
    setContent(content) {
        this.content = content
    }
    getContent(content) {
        return this.content
    }
    saveContentToMemento() {
        return new Memento(this.content)
    }
    getContentFromMenmeto(memento) {
        this.content = memento.getContent()
    }
}
複製程式碼

//測試程式碼
let editor = new Editor()
let careTaker = new CareTaker()
editor.setContent('111')
editor.setContent('222')
careTaker.add(editor.saveContentToMemento()) //儲存備忘錄
editor.setContent('333')
careTaker.add(editor.saveContentToMemento()) //儲存備忘錄
editor.setContent('444')
複製程式碼

console.log(editor.getContent())
editor.getContentFromMenmeto(careTaker.get(1)) //撤銷
console.log(editor.getContent())
editor.getContentFromMenmeto(careTaker.get(0)) //撤銷
console.log(editor.getContent())
複製程式碼

設計原則驗證

  • 狀態物件與使用者分開,解耦
  • 符合開放封閉原則

中介者模式

前端面試之路六(Javascript設計模式篇)

演示

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 / 100);
    }
}

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();
        }
    }
}


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(300, m);
console.log(a.number, b.number)
複製程式碼

設計原則驗證

  • 將各關聯物件通過中介者隔離
  • 符合開放封閉原則

訪問者模式

  • 將資料操作和資料結構分離
  • 使用場景不多

直譯器模式

  • 描述語言語法如何定義,如何解釋和編譯
  • 用於專業場景

綜合應用

關於面試

  • 能說出課程重點講解的設計模式即可

日常使用

  • 瞭解重點設計模式,要強制自己模仿、掌握
  • 非常用的設計模式,視業務場景選擇性使用

金三銀四,看見大家都在為了面試而努力 特開了一個前端模擬面試題,組織了面試的群友每天來群裡分享面試題,講題 急思眾議,共同進步,歡迎最近在面試或者準備面試的群友加入本群,加群格式: 工作年限-面試等級(初、中、高)-工作地點 (不在面試或者不準備面試或者不活躍的勿加本群,加了也會被清理)

前端面試之路六(Javascript設計模式篇)

相關文章