設計原則(SOLID)
單一職責模式(S)
- 一個程式只做好一件事
- 如果功能過於複雜就拆分開,每個部分保持獨立
裡式替換原則(L)
- 子類能覆蓋父類
- 父類能出現的地方子類就能出現
- JS中使用較少(弱型別&繼承使用較少)
開放封閉原則(O)
- 對擴充套件開放對修改封閉
- 增加需求時,擴充套件新程式碼,而非修改已有程式碼
- 軟體設計的終極目標
介面隔離原則(I)
- 保持介面的單一獨立,避免出現"胖介面"
- JS中沒有介面(typescript例外),使用較少
- 類似於單一職責所在,這裡更關注介面
依賴倒置原則(D)
- 面向介面程式設計,依賴於抽象而不依賴於具體
- 使用方只關注介面而不關注具體類的實現
- JS中使用較少(沒有介面&弱型別)
設計模式
工廠模式
- 將new操作單獨封裝
- 遇到new時,就要考慮是否該使用工廠模式了
示例
你去購買漢堡,直接點餐、取餐,不會自己親手做
商店要“封裝”做漢堡的工作,做好直接給買者
UML類圖:
程式碼示例:
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圖
說明
- 單例模式需要用到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圖
演示
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類圖
程式碼演示
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
程式碼演示
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裝飾器模式
- 裝飾器模式:擴充套件功能,原有功能不變且可直接使用
- 代理模式:直接針對(顯示)原有功能,但是經過限制或者閹割之後的
外觀模式
- 為子系統中的一組介面提供了一個高層介面
- 使用者使用這個高層介面
示例:
去醫院看病,接待員去掛號、門診、劃價、取藥
UML類圖
程式碼演示
function bindEvent(elem,type,selector,fn){
if(fn == null){
fn = selector
selector = null
}
}
//呼叫
bindEvent(elem,'click','#div1',fn)
bindEvent(elem,'click',fn)
複製程式碼
設計原則驗證
- 不符合單一職責原則和開放封閉原則,因此謹慎使用,不可濫用
觀察者模式
- 釋出&訂閱
- 一對多(N)
示例
- 點咖啡,點好之後坐等被叫
UML類圖
前端設計最重要的一種模式程式碼演示
//儲存狀態,狀態變化之後觸發所有觀察者
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類圖
程式碼演示
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語法中,有序集合的資料型別已經有很多
Array
、Map
、Set
、String
、TypedArray
、argument
、NodeList
- 以上資料型別都有
[Symbol.Iterator]
屬性 - 屬性值是函式,執行函式返回一個迭代器
- 這個迭代器就有next方法可順序迭代子元素
- 可執行
Array.prototype[Symbol.iterator]
來測試 for...of
消費iterator
- ES6 Iterator與Generator
iterator
的價值不限於尚書幾個型別的遍歷,還有Generator
函式的使用- 即只要返回的資料符合
Iterator
介面的要求 - 即可使用
Iterator
語法,這就是迭代器模式
設計原則驗證
- 迭代器物件和目標物件分離
- 迭代器將使用者與目標物件隔離開
- 符合開放封閉原則
狀態模式
- 一個物件有狀態變化
- 每次狀態變化都會觸發一個邏輯
- 不能總是用if...else來控制
示例
交通訊號燈不同顏色的變化
UML類圖
程式碼演示
//狀態
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是實現物件導向的基礎,並不是服務於某個模式
橋接模式
- 用於把抽象化與實現化解耦
- 使得兩者可以獨立變化
- 在一些業務中比較常用
應用場景
//普通實現
class ColorShape {
yellowCircle() {
//...畫黃圓
}
redCircle() {
//...畫紅圓
}
yellowTriangle() {
//...畫黃三角形
}
redTriangle() {
//...畫紅三角形
}
}
//測試
let cs = new ColorShape()
cs.yellowCircle()
cs.redCircle()
cs.yellowTriangle()
cs.redTriangle()
複製程式碼
//橋接模式
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()
複製程式碼
顏色和圖形自由組合,複雜性少很多,後面增加圖形也很好處理
設計原則驗證
- 抽象和實現分離,解耦
- 符合開放封閉原則
組合模式
- 生成樹形結構,表示“整體-部分”關係
- 讓整體和部分都具有一致的操作方式
示例:
應用場景
- 虛擬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的鏈式操作
設計原則驗證
- 發起者和各個處理者進行隔離
- 符合開放封閉原則
命令模式
- 執行命令時,釋出者和執行者分開
- 中間加入命令物件,作為中轉站
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())
複製程式碼
設計原則驗證
- 狀態物件與使用者分開,解耦
- 符合開放封閉原則
中介者模式
演示
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)
複製程式碼
設計原則驗證
- 將各關聯物件通過中介者隔離
- 符合開放封閉原則
訪問者模式
- 將資料操作和資料結構分離
- 使用場景不多
直譯器模式
- 描述語言語法如何定義,如何解釋和編譯
- 用於專業場景
綜合應用
關於面試
- 能說出課程重點講解的設計模式即可
日常使用
- 瞭解重點設計模式,要強制自己模仿、掌握
- 非常用的設計模式,視業務場景選擇性使用
金三銀四,看見大家都在為了面試而努力 特開了一個前端模擬面試題,組織了面試的群友每天來群裡分享面試題,講題 急思眾議,共同進步,歡迎最近在面試或者準備面試的群友加入本群,加群格式: 工作年限-面試等級(初、中、高)-工作地點 (不在面試或者不準備面試或者不活躍的勿加本群,加了也會被清理)