1. 面向?qū)ο?/h2>
1.1 封裝
封裝的目的在于將信息隱藏鸭叙。廣義的封裝不僅包括封裝數(shù)據(jù)和封裝實現(xiàn),還包括封裝類型和封裝變化生百。
1.1.1 封裝數(shù)據(jù)
- 在
Java
中递雀,封裝數(shù)據(jù)是由語法解析來實現(xiàn)的,提供了public
蚀浆、protected
缀程、private
等關(guān)鍵字來提供不同的訪問權(quán)限。 - 在
JavaScript
中市俊,只能依賴變量作用域來模擬實現(xiàn)封裝性杨凑。
var myObj = (function() {
var _name = 'sven'
return {
getName: function() {
return _name
}
}
})()
console.log(myObj.getName()) // sven
console.log(myObj._name) // undefined
1.1.2 封裝實現(xiàn)
- 封裝實現(xiàn)細節(jié)使對象內(nèi)部的變化對其他對象而言是透明的,即不可見的摆昧。
- 封裝實現(xiàn)細節(jié)使對象之間的耦合變松散撩满,對象之間只通過暴露的
API
接口來通訊。當我們修改一個對象時绅你,可以隨意修改它的內(nèi)部實現(xiàn)伺帘。 - 封裝實現(xiàn)細節(jié)的例子有很多。例如:迭代器
each
函數(shù)忌锯。
1.1.3 封裝類型
- 對于靜態(tài)類型語言伪嫁,封裝類型是通過抽象類和接口來進行的。將對象的真正類型隱藏在抽象類或者接口之后偶垮。
-
JavaScript
是一門類型模糊語言张咳。在封裝類型方面,JavaScript
沒有能力似舵,也沒有必要做得更多脚猾。 - 對于
JavaScript
設(shè)計模式實現(xiàn)來說,不區(qū)分類型是一種失色砚哗,也可以說是一種解脫龙助。
1.1.4 封裝變化
- 考慮你的設(shè)計中哪些地方可能變化,找到并封裝蛛芥,這是許多設(shè)計模式的主題泌参。
- 設(shè)計模式被劃分為創(chuàng)建型模式脆淹、結(jié)構(gòu)型模式以及行為型模式。其中沽一,創(chuàng)建型模式的目的就是封裝創(chuàng)建對象的變化,結(jié)構(gòu)型模式封裝的是對象之間的組合關(guān)系漓糙,行為型模式封裝的是對象的行為變化铣缠。
- 通過封裝變化的方式,把系統(tǒng)中穩(wěn)定不變的部分和容易變化的部分隔離開來昆禽,在系統(tǒng)的演變過程中蝗蛙,我們只需要替換那些容易變化的部分,如果這些部分是已經(jīng)封裝好的醉鳖,替換起來也相對容易捡硅。這可以最大限度的保證程序的穩(wěn)定性和可擴展性。
1.2 繼承
-
JavaScript
選擇了基于原型的面向?qū)ο笙到y(tǒng)盗棵。在原型編程的思想中壮韭,類并不是必須的,對象未必從類中創(chuàng)建而來纹因,一個對象可以通過克隆另一個對象而得到喷屋。 - 雖然
JavaScript
的對象最初都是由Object.prototype
對象克隆而來的,但對象構(gòu)造器的原型可以動態(tài)指向其它對象瞭恰。這樣一來屯曹,當對象a
需要借用對象b
的能力時,可以有選擇性地把對象a
的構(gòu)造器的原型指向?qū)ο?code>b惊畏,從來達到繼承的效果恶耽。 - 原型繼承
var obj = {name: 'sven'}
var A = function() {}
A.prototype = obj
var a = new A()
console.log(a.name) //sven
name
屬性查找:a → a.__proto__→ obj
- 原型繼承鏈
var obj = {name: 'sven'}
var A = function() {}
A.prototype = obj
var B = function() {}
B.prototype = new A()
var b = new B()
console.log(b.name) //sven
name
屬性查找鏈:b → b.__proto__ → new A() → A.prototype → obj
1.3 多態(tài)
- 多態(tài)將“做什么”和“誰去做”分離開來。實現(xiàn)多態(tài)的關(guān)鍵在于消除類型之間的耦合關(guān)系颜启。
- 在
Java
中偷俭,可以通過向上轉(zhuǎn)型來實現(xiàn)多態(tài)。由于JavaScript
的變量類型在運行期是可變的农曲,所以JavaScript
對象的多態(tài)性是與生俱來的咒林。 - 多態(tài)的最根本好處在于,你不必再向?qū)ο笤儐枴澳闶鞘裁搭愋汀倍蟾鶕?jù)得到的答案調(diào)用對象的某個行為——你只管調(diào)用該行為就是了忍捡,其他的一切多態(tài)機制都會為你安排妥當擦秽。
- 多態(tài)將過程化的條件分支語句轉(zhuǎn)化為對象的多態(tài)性,從而消除這些條件分支語句暮的。
- 代碼演示
class GoogleMap {
show() {
console.log('開始渲染谷歌地圖')
}
}
class BaiduMap {
show() {
console.log('開始渲染百度地圖')
}
}
const renderMap = map => {
if(map.show instanceof Function) {
map.show()
}
}
renderMap(new GoogleMap())
renderMap(new BaiduMap())
1.4 UML類圖
-
類圖
類圖 -
類與類之間的關(guān)系
(1) 泛化表示繼承笙以,用空心箭頭表示。
(2) 關(guān)聯(lián)表示引用冻辩,用實心箭頭表示猖腕。
類與類關(guān)系圖
1.5 示例
- 使用
class
簡單實現(xiàn)jQuery
中的$
選擇器
class jQuery {
constructor(selector) {
const slice = Array.prototype.slice
const dom = slice.call(document.querySelectorAll(selector))
this.selector = selector || ''
const len = dom ? dom.length : 0;
this.length = len
for(let i = 0; i < len; i++) {
this[i] = dom[i]
}
}
append(node) {}
addClass(name) {}
html(data) {}
}
window.$ = selector => new jQuery(selector)
-
打車時可以打?qū)\嚮蚩燔嚥鹌怼H魏诬嚩加熊嚺铺柡兔Q√雀校快車每公里1元放坏,專車每公里2元。行程開始時老玛,顯示車輛信息淤年。行程結(jié)束時,顯示打車金額蜡豹。行程距離為5公里麸粮。
UML類圖
//父類 - 車
class Car {
constructor(name, number) {
this.name = name
this.number = number
}
}
//子類 - 快車
class KuaiChe extends Car {
constructor(name, number) {
super(name, number)
this.price = 1
}
}
//子類 - 專車
class ZhuanChe extends Car {
constructor(name, number) {
super(name, number)
this.price = 2
}
}
//行程
class Trip {
constructor(car, distance) {
this.car = car
this.distance = distance
}
start() {
console.log(`行程開始 車名為${this.car.name},車牌號為${this.car.number}`)
}
end() {
console.log(`行程結(jié)束 車費為${this.distance * this.car.price}`)
}
}
const zhuanChe = new ZhuanChe('專車', '299567')
const trip = new Trip(zhuanChe, 5)
trip.start()
trip.end()
注意:將行程抽象為一個類镜廉,而不是車的一個屬性弄诲。
-
某停車廠分3層,每層100個車位娇唯。每個車位都能夠檢測到車輛的駛?cè)牒碗x開齐遵。車輛進入前,顯示每層空余車位數(shù)量视乐。車輛進入時洛搀,攝像頭可識別車牌號和時間。車輛出來時佑淀,出口顯示器顯示車牌號和停車時間留美。
類: 停車場、層伸刃、車位谎砾、車輛、攝像頭捧颅、顯示器景图。
UML類圖
//車輛
class Car {
constructor(number) {
this.number = number
}
}
//停車位
class Stall {
constructor() {
this.empty = true
}
in() {
this.empty = false
}
out() {
this.empty = true
}
}
//停車層
class Floor {
constructor(index, stalls) {
this.index = index
this.stalls = stalls || []
}
emptyNum() {
return this.stalls.filter(stall => stall.empty).length
}
}
//出口顯示屏
class Screen {
show(car, inTime) {
console.log(`車牌號為${car.number},停留時間為${Date.now() - inTime}`)
}
}
//入口攝像頭
class Camera {
shoot(car) {
return {
number: car.number,
inTime: Date.now()
}
}
}
//停車場
class Park {
constructor(floors, camera, screen) {
this.camera = camera
this.screen = screen
this.floors = floors || []
this.carList = {};
}
emptyNum() {
let num = 0
this.floors.forEach(floor => {
const emptyNum = floor.emptyNum()
num += emptyNum
})
return num;
}
showMsg() {
let info = ''
for(let i = 1; i < this.floors.length; i++) {
const floor = this.floors[i]
info += `第${floor.index}層還有${floor.emptyNum()}個空位`
}
console.log(info)
}
in(car) {
if(this.emptyNum() > 0) {
const info = this.camera.shoot(car)
for(let i = 1; i < this.floors.length; i ++) {
const floor = this.floors[i]
const allNum = floor.stalls.length
const emptyNum = floor.emptyNum()
if(emptyNum > 0) {
let index = 1;
while(!floor.stalls[index].empty) {
index++
}
const stall = floor.stalls[index]
stall.in()
//保存停車位信息
info.stall = stall
break
}
}
this.carList[car.number] = info
} else {
console.log('停車場已滿')
}
}
out(car) {
const info = this.carList[car.number]
info.stall.out()
this.screen.show(car, info.inTime)
delete this.carList[car.number]
}
}
//測試代碼
const floors = []
for(let i = 1; i <= 3; i ++) {
const stalls = []
for(let j = 1; j <= 100; j++) {
stalls[j] = new Stall()
}
floors[i] = new Floor(i, stalls)
}
const camera = new Camera()
const screen = new Screen()
const park = new Park(floors, camera, screen)
const car1 = new Car('100')
const car2 = new Car('200')
const car3 = new Car('300')
park.in(car1)
park.showMsg()
park.in(car2)
park.showMsg()
park.out(car1)
park.showMsg()
park.in(car3)
park.showMsg()
park.out(car2)
park.showMsg()
park.out(car3)
park.showMsg()
注意:① 引用指的是一個類持有另一個類,而不是一個類的方法以另一個類為參數(shù)碉哑。 ② 類與類之間應(yīng)該盡量減少耦合挚币,能夠通過將另一個類作為參數(shù)實現(xiàn),就不要持有另一個類扣典。
2. 設(shè)計模式
- 簡介
在面向?qū)ο筌浖O(shè)計過程中針對特定問題的簡潔而優(yōu)雅的解決方案妆毕。即設(shè)計模式是在某種場合下對某個問題的一種解決方案。 - 分類
(1) 創(chuàng)建型
工廠模式贮尖、單例模式笛粘、原型模式。
(2) 結(jié)構(gòu)型
適配器模式、裝飾器模式薪前、代理模式润努、外觀模式、橋接模式示括、組合模式铺浇、享元模式。
(3) 行為型
策略模式垛膝、模板方法模式随抠、觀察者模式、迭代器模式繁涂、職責鏈模式、命令模式二驰、備忘錄模式扔罪、狀態(tài)模式、訪問者模式桶雀、中介者模式矿酵、解釋器模式。
2.1 工廠模式
- 介紹
對new
創(chuàng)建對象的封裝矗积。 -
UML
類圖
UML類圖 - 使用場景
(1) jQuery
中的$('div')
class jQuery{
// ...
}
window.$ = function(selector) {
return new jQuery(selector)
}
(2) React
中的React.createElement
class Vnode(tag, attrs, children) {
// ...
}
React.createElement = function(tag, attrs, children) {
return new Vnode(tag, attrs, children)
}
(3) Vue
異步組件
Vue.component('async-example', function(resolve, reject) {
setTimeout(function() {
resolve({
template: '<div>I am async!</div>'
})
}, 1000)
})
工廠方式創(chuàng)建對象與new
創(chuàng)建對象相比全肮,書寫簡便并且封裝性更好。
2.2 單例模式
- 介紹
保證一個類僅有一個實例棘捣,并提供一個訪問它的全局訪問點辜腺。
注釋:單例模式的核心是確保只有一個實例,并提供全局訪問乍恐。 - 代碼演示
class Single {
login() {
console.log('login')
}
}
Single.getInstance = (function() {
let instance
return () => {
if(!instance) {
instance = new Single()
}
return instance
}
})()
let obj1 = Single.getInstance()
obj1.login()
let obj2 = Single.getInstance()
obj1.login()
console.log(obj1 === obj2) //true
注釋:① Single
類的使用者必須知道這是一個單例類评疗,與以往通過new Single()
的方式不同,這里必須使用Single.getInstance()
來創(chuàng)建單例對象茵烈。② 該示例為惰性單例百匆,在使用的時候才創(chuàng)建對象。
-
JavaScript
中的單例模式
在傳統(tǒng)面向?qū)ο笳Z言中呜投,單例對象從類中創(chuàng)建而來加匈。JavaScript
其實是一門無類語言,創(chuàng)建唯一對象并不需要先創(chuàng)建一個類仑荐。傳統(tǒng)的單例模式實現(xiàn)在JavaScript
中并不適用雕拼。
const single = {
a() {
console.log('1')
}
}
- 創(chuàng)建對象與單例模式分離
const getSingle = fn => {
let result
return () => result || (result = fn(arguments))
}
const createObj = () => new Object();
const createSingleObj = getSingle(createObj)
const s1 = createSingleObj()
const s2 = createSingleObj()
console.log(s1) //{}
console.log(s1 === s2) //true
注釋:創(chuàng)建對象與管理單例的職責被分布在兩個不同的方法中。
- 使用場景
(1) jQuery
中只有一個$
if(window.jQuery) {
return window.jQuery
} else {
// 初始化...
}
(2) vuex
和redux
中的store
2.3 適配器模式
- 介紹
舊接口格式和使用者不兼容释漆,使用適配器轉(zhuǎn)換接口悲没。 -
UML
類圖
image.png - 代碼演示
class Adaptee {
specifiRequest() {
return '德國標準插頭'
}
}
class Target {
constructor() {
this.adaptee = new Adaptee()
}
request() {
let info = this.adaptee.specifiRequest()
return `${info} - 轉(zhuǎn)換器 - 中國標準插頭`
}
}
let target = new Target()
console.log(target.request())
- 使用場景
(1) 封裝舊接口
//適配jQuery中的$.ajax()
const $ = {
ajax: function(options) {
return ajax(options)
}
}
(2) vue computed
<div id="app">
<p>順序: {{message}}</p>
<p>逆序: {{reverseMessage}}</p>
</div>
<script src = "https://cdn.bootcss.com/vue/2.5.14/vue.js"></script>
<script>
const vm = new Vue({
el: '#app',
data: {
message: 'hello'
},
computed: {
reverseMessage: function() {
return this.message.split('').reverse().join('')
}
}
})
</script>
2.4 裝飾器模式
- 介紹
裝飾器模式可以動態(tài)地給某個對象添加額外的職責,而不會影響從這個類中派生的其他對象。 - 模式分析
在傳統(tǒng)的面向?qū)ο笳Z言中示姿,給對象添加功能常常使用繼承的方式甜橱。繼承的方式并不靈活,會導致超類和子類存在強耦合性栈戳,并且在完成一些功能復(fù)用的同時岂傲,有可能創(chuàng)建出大量的子類。
裝飾器模式能夠在不改變對象自身的基礎(chǔ)上子檀,在程序運行期間給對象動態(tài)地添加職責镊掖。與繼承相比,裝飾者是一種更輕便靈活的做法褂痰,這是一種“即用即付”的方式亩进。 -
UML
類圖
image.png - 代碼演示
class Plane{
fire() {
console.log('發(fā)射普通子彈')
}
}
class MissileDecorator {
constructor(plane) {
this.plane = plane
}
fire() {
this.plane.fire()
console.log('發(fā)射導彈')
}
}
class AtomDecorator {
constructor(plane) {
this.plane = plane
}
fire() {
this.plane.fire()
console.log('發(fā)射原子彈')
}
}
let plane = new Plane()
plane = new MissileDecorator(plane)
plane = new AtomDecorator(plane)
plane.fire() //發(fā)射普通子彈 發(fā)射導彈 發(fā)射原子彈
-
before
和after
鉤子函數(shù)
Function.prototype.before = function(beforefn) {
var _self = this
return function() {
beforefn.apply(this, arguments)
return _self.apply(this, arguments)
}
}
Function.prototype.after = function(afterfn) {
var _self = this
return function() {
var ret = _self.apply(this, arguments)
afterfn.apply(this, arguments)
return ret
}
}
var fun = function() {
console.log(1)
}
var beforeFun = fun.before(function() {
console.log(0)
})
beforeFun() //0 1
var afterFun = fun.after(function() {
console.log(2)
}).after(function() {
console.log(3)
})
afterFun() //1 2 3
-
ES7
中的裝飾器
(1) 配置環(huán)境
① 安裝插件
? design npm i babel-plugin-transform-decorators-legacy --save-dev
② 修改webpack
打包配置文件
module.exports = {
//...
module: {
rules: [{
test: /\.js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
presets: ['babel-preset-env'],
plugins: ['babel-plugin-transform-decorators-legacy']
}
}
}]
},
//...
}
(2) 裝飾類
@testDec1
@testDec2(false)
class Demo {}
function testDec1( target) {
target.isDec1 = true
}
function testDec2( bool ) {
return function(target) {
target.isDec2 = bool
}
}
console.log(Demo.isDec1) //true
console.log(Demo.isDec2) //false
(3) 裝飾對象
function mixins(...list) {
return function(target) {
Object.assign(target.prototype, ...list)
}
}
const Foo = {
foo() {
console.log('foo')
}
}
@mixins(Foo)
class B {}
const b = new B()
b.foo() //foo
(4) 裝飾方法
① 設(shè)置方法只可執(zhí)行,不可修改
class Person {
constructor() {
this.first = 'A'
this.last = 'B'
}
@readonly
name() { return `${this.first} ${this.last}` }
}
function readonly(target, name, descriptor){
// descriptor對象原來的值如下
// {
// value: specifiedFunction,
// enumerable: false,
// configurable: true,
// writable: true
// };
descriptor.writable = false;
return descriptor;
}
const p = new Person()
//A B
console.log(p.name())
//Uncaught TypeError: Cannot assign to read only property 'name' of object '#<Person>'
p.name = () => {}
② 方法執(zhí)行添加日志
class Math{
@log
add(a, b) {
return a + b
}
}
function log(target, name, descriptor) {
let oldValue = descriptor.value
descriptor.value = function() {
console.log(`calling ${name} wich`, arguments)
return oldValue.apply(this, arguments)
}
return descriptor
}
const math = new Math()
const result = math.add(2, 4)
console.log(result)
-
core-decorators
第三方庫
提供常用的裝飾器缩歪。
import { readonly } from 'core-decorators'
class Person {
constructor() {
this.first = 'A'
this.last = 'B'
}
@readonly
name() {
return `${this.first} ${this.last}`
}
@deprecate()
getName() {
return `${this.first} ${this.last}`
}
}
const p = new Person()
console.log(p.name())
//A B
console.log(p.getName())
//DEPRECATION Person#getName: This function will be removed in future versions.
//A B
p.name = () => {}
//Uncaught TypeError: Cannot assign to read only property 'name' of object '#<Person>'
2.5 代理模式
- 介紹
使用者無權(quán)訪問目標對象归薛,通過代理做授權(quán)和控制。 -
UML
類圖
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()
}
}
const proxyImg = new ProxyImg('1.png')
proxyImg.display()
注釋:代理和本體接口必須一致匪蝙,在任何使用本體的地方都可以替換成使用代理主籍。
- 使用場景
(1) 事件代理
var ul = document.getElementById('ul')
ul.addEventListener('click', function(e) {
var target = e.target;
if(target.nodeName === 'LI') {
alert(target.innerHTML)
}
})
(2) jQuery
中的$.proxy
$('ul').click(function() {
var self = this
setTimeout(function() {
$(self).css('background', 'red')
}, 1000)
})
//使用$.proxy保存this
$('ul').click(function() {
setTimeout($.proxy(function() {
$(this).css('background', 'red')
}, this), 1000)
})
(3) 圖片預(yù)加載
var myImg = (function() {
var imgNode = document.createElement('img')
document.body.appendChild(imgNode)
return function(src){
imgNode.src = src
}
})()
var proxyImg = (function() {
var img = new Image()
img.onload = function() {
myImg(this.src)
}
return function(src) {
myImg('loading.gif')
img.src = src
}
})()
proxyImg('1.png')
(4) 緩存代理
//計算乘積
const mult = function(){
let a = 1
debugger;
for(let i = 0; i < arguments.length; i++) {
a *= arguments[i]
}
return a
}
//緩存代理工廠
const createProxyFactory = fn => {
let cache = {}
return function(){
const args = Array.prototype.join.call(arguments, ',')
if(cache[args]) {
return cache[args]
}
return cache[args] = fn.apply(this, arguments)
}
}
var proxyMult = createProxyFactory(mult)
console.log(proxyMult(1,2,3,4)) //24
console.log(proxyMult(1,2,3,4)) //24
(5) ES6
中的Proxy
//明星
let star = {
name: '張XX',
age: 25,
phone: '13900001111'
}
//經(jīng)紀人
let agent = new Proxy(star, {
get: function (target, key, receiver) {
if(key === 'phone') {
//返回經(jīng)紀人自己的電話
return '13922225555'
}
if(key === 'price') {
//明星不報價,經(jīng)紀人報價
return 120000
}
return target[key]
},
set: function (target, key, value, receiver) {
if(key === 'customPrice') {
if(value < 100000) {
throw new Error('價格太低')
} else {
target[key] = value
return true
}
}
}
})
console.log(agent.name) //張XX
console.log(agent.age) //25
console.log(agent.phone) //13922225555
console.log(agent.price) //120000
agent.customPrice = 150000
console.log(star.customPrice) //150000
agent.customPrice = 90000 //Uncaught Error: 價格太低
- 與其他設(shè)計模式對比
(1) 代理模式與適配器模式對比
適配器模式:提供一個不同的接口逛球。
代理模式:提供一模一樣的接口千元。
(2) 代理模式與裝飾器模式
裝飾器模式:擴展功能,原有功能不變且可直接使用颤绕。
代理模式:使用原有功能幸海,但是經(jīng)過限制和約束。
2.6 外觀模式
- 介紹
為子系統(tǒng)中的一組接口提供了一個高層接口屋厘,使用者使用這個高層接口涕烧。 - 使用場景
function bindEvent(elem, type, selector, fn) {
if(fn == null) {
fn = selector
selector = null
}
//...
}
// 調(diào)用
bindEvent(elem, 'click', '#div1', fn)
bindEvent(elem, 'click', fn)
2.7 觀察者模式
- 介紹
觀察者模式又叫做發(fā)布-訂閱模式,它定義對象間的一種一對多的依賴關(guān)系汗洒,當一個對象的狀態(tài)發(fā)生變化時议纯,所有依賴于它的對象都將得到通知。 -
UML
類圖
image.png - 代碼演示
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(this.state)
})
}
attach(observer) {
this.observers.push(observer)
observer.subject = this
}
}
class Observer {
constructor(name) {
this.name = name
this.subject = null
}
update(state) {
console.log(`${this.name} update state to ${state}`)
}
}
const sub = new Subject()
const obs1 = new Observer('obs1')
const obs2 = new Observer('obs2')
sub.attach(obs1)
sub.attach(obs2)
sub.setState(1)
sub.setState(2)
sub.setState(3)
- 使用場景
(1) DOM
事件
document.body.addEventListener('click', function() {
console.log(1)
}, false)
document.body.addEventListener('click', function() {
console.log(2)
}, false)
document.body.click() //模擬用戶點擊
(2) Promise
(3) jQuery
中callbacks
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('go')
callbacks.fire('fire')
(4) nodejs
自定義事件
① EventEmitter
類
const EventEmitter = require('events').EventEmitter
const emitter = new EventEmitter()
emitter.on('some', info => {
console.log('fn1', info)
})
emitter.on('some', info => {
console.log('fn2', info)
})
emitter.emit('some', 'fire1')
② EventEmitter
應(yīng)用
const EventEmitter = require('events').EventEmitter
class Dog extends EventEmitter {
constructor(name) {
super()
this.name = name
}
}
const simon = new Dog('simon')
simon.on('bark', function(voice) {
console.log(this.name, voice)
})
setInterval(function() {
simon.emit('bark', '汪')
}, 1000)
③ fs
文件系統(tǒng)讀取文件字符數(shù)
const fs = require('fs')
const readStream = fs.createReadStream('./node_modules/accepts/index.js')
let length = 0
readStream.on('data', function(chunk) {
let len = chunk.toString().length
console.log('len', len) //len 5252
length += len
})
readStream.on('end', function() {
console.log('length', length) //length 5252
})
④ fs
文件系統(tǒng)讀取文件行數(shù)
const fs = require('fs')
const readLine = require('readline')
let rl = readLine.createInterface({
input: fs.createReadStream('./node_modules/accepts/index.js'),
})
let lineNum = 0
rl.on('line', function(line) {
lineNum ++
})
rl.on('close', function() {
console.log('lineNum', lineNum) //lineNum 238
})
2.8 迭代器模式
- 介紹
提供一種方法順序訪問一個聚合對象中的各個元素溢谤,而又不需要暴露該對象的內(nèi)部表示瞻凤。
注釋:object
不是有序數(shù)據(jù)集合,ES6
中的map
是有序數(shù)據(jù)集合世杀。 -
UML
類圖
UML類圖 - 代碼演示
class Iterator {
constructor(container) {
this.list = container.list
this.index = 0
}
next() {
if(this.hasNext()) {
return this.list[this.index++]
}
return null
}
hasNext() {
return this.index < this.list.length
}
}
class Container {
constructor(list) {
this.list = list
}
getIterator() {
return new Iterator(this)
}
}
var arr = [1, 2, 3, 4, 5]
var container = new Container(arr)
var iterator = container.getIterator()
while(iterator.hasNext()) {
console.log(iterator.next())
}
- 使用場景
(1) jQuery
中的each
方法
var arr = [1,2,3]
var nodeList = document.getElementsByTagName('li')
var $a = $('li')
function each(data) {
var $data = $(data)
$data.each(function(key, val) {
console.log(key, val)
})
}
each(arr)
each(nodeList)
each($a)
(2) 迭代器模式替換if...else
const getActiveUploadObj = () => {
try{
return new ActiveXObject('TXFTNActiveX.FTNUpload')
} catch (e) {
return false
}
}
const getFlashUploadObj = () => {
if(supportFlash()) {
const str = '<object type="application/x-shockwave-flash"></object>'
return $(str).appendTo($('body'))
}
}
const getFormUploadObj = () => {
const str = '<input name="file" type="file" class="ui-file" />'
return $(str).appendTo($('body'))
}
const interatorUploadObj = function(){
for(let i = 0, fn; fn=arguments[i++];) {
var uploadObj = fn()
if(uploadObj !== false) {
return uploadObj
}
}
}
const uploadObj = interatorUploadObj(getActiveUploadObj, getFlashUploadObj, getFormUploadObj)
(3) ES6
中的Iterator
ES6
語法中阀参,有序數(shù)據(jù)集合的數(shù)據(jù)類型有多個。例如:Array
瞻坝、Map
蛛壳、Set
、String
、TypedArray
等衙荐。需要有一個統(tǒng)一的遍歷接口來遍歷所有數(shù)據(jù)類型捞挥。以上數(shù)據(jù)類型都有一個[Symbol.iterator]
屬性。屬性值是函數(shù)忧吟,執(zhí)行函數(shù)返回一個迭代器砌函。此迭代器有next()
方法可順序迭代子元素。
Array.prototype[Symbol.iterator]
? values() { [native code] }
Array.prototype[Symbol.iterator]()
Array Iterator {}
Array.prototype[Symbol.iterator]().next()
{value: undefined, done: true}
Iterator
使用示例
function each(data) {
let iterator = data[Symbol.iterator]()
let item = { done: false }
while(!item.done) {
item = iterator.next()
if(!item.done) {
console.log(item.value)
}
}
}
var map = new Map()
map.set(1, 'a')
map.set(2, 'b')
var arr = [1,2,3]
var str = 'abc'
each(arr)
each(str)
each(map)
ES6中的for...of
語法是對Iterator
的封裝
function each(data) {
for(let item of data) {
console.log(item)
}
}
var map = new Map()
map.set(1, 'a')
map.set(2, 'b')
var arr = [1,2,3]
var str = 'abc'
each(arr)
each(str)
each(map)
2.9 狀態(tài)模式
- 介紹
將狀態(tài)封裝成獨立的類溜族,并將請求委托給當前的狀態(tài)對象讹俊,當對象的內(nèi)部狀態(tài)改變時,會帶來不同的行為變化煌抒。
注釋:狀態(tài)模式的關(guān)鍵是把事物的每種狀態(tài)都封裝成單獨的類仍劈,跟此種狀態(tài)有關(guān)的行為都被封裝在這個類的內(nèi)部。 -
UML
類圖
image.png - 代碼演示
class OffLightState {
constructor(light) {
this.light = light
}
switch() {
console.log('微光') //offLightState對應(yīng)的行為
this.light.setState(this.light.weakLightState) //切換狀態(tài)到weakLightState
}
}
class WeakLightState {
constructor(light) {
this.light = light
}
switch() {
console.log('強光')
this.light.setState(this.light.strongLightState)
}
}
class StrongLightState {
constructor(light) {
this.light = light
}
switch() {
console.log('關(guān)燈')
this.light.setState(this.light.offLightState)
}
}
class Light {
constructor() {
this.offLightState = new OffLightState(this)
this.weakLightState = new WeakLightState(this)
this.strongLightState = new StrongLightState(this)
}
init() {
this.currState = this.offLightState
}
setState(newState) {
this.currState = newState
}
press() {
this.currState.switch()
}
}
const light = new Light()
light.init()
light.press() //微光
light.press() //強光
light.press() //關(guān)燈
注釋:狀態(tài)的切換規(guī)律事先被完好定義在各個狀態(tài)類中寡壮。在Context
中再也找不到任何一個跟狀態(tài)切換相關(guān)的條件分支語句耳奕。
- 使用場景
(1) 有限狀態(tài)機
有限個狀態(tài)知之間切換。
使用開源庫:javascript-state-machine
import StateMachine from 'javascript-state-machine'
import $ from 'jquery'
let fsm = new StateMachine({
init: '收藏',
transitions: [
{
name: 'doStore',
form: '收藏',
to: '取消收藏'
},
{
name: 'deleteStore',
form: '取消收藏',
to: '收藏'
}
],
methods: {
onDoStore: function() {
alert('收藏成功')
updateText()
},
onDeleteStore: function() {
alert('取消收藏成功')
updateText()
}
}
})
let $btn = $('#btn')
$btn.click(function() {
if(fsm.is('收藏')) {
fsm.doStore()
} else {
fsm.deleteStore()
}
})
// 更新按鈕文案
function updateText() {
$btn.text(fsm.state)
}
//初始化文案
updateText()
(2) Promise
實現(xiàn)
import StateMachine from 'javascript-state-machine'
const fsm = new StateMachine({
init: 'pending',
transitions: [
{
name: 'resolve',
from: 'pending',
to: 'fullfilled'
},
{
name: 'reject',
from: 'pending',
to: 'rejected'
}
],
methods: {
onResolve(state, data){
//state - 當前狀態(tài)機實例; data - fsm.resolve(xxx) 傳遞的參數(shù)
data.successList.forEach(fn => fn())
},
onReject(state, data){
//state - 當前狀態(tài)機實例; data - fsm.reject(xxx) 傳遞的參數(shù)
data.failList.forEach(fn => fn())
}
}
})
class MyPromise {
constructor(fn) {
this.successList = []
this.failList = []
fn(() => {
// resolve 函數(shù)
fsm.resolve(this)
}, () => {
// reject 函數(shù)
fsm.reject(this)
})
}
then(successFn, failFn) {
this.successList.push(successFn)
this.failList.push(failFn)
}
}
function loadImg(src) {
const mp = new MyPromise((resolve, reject) => {
const img = document.createElement('img')
img.onload = () => resolve(img)
img.onerror = () => reject()
img.src = src
})
return mp
}
let src = 'https://www.baidu.com/img/bd_logo1.png'
let mp = loadImg(src)
mp.then(() => {
console.log('success1')
}, () => {
console.log('fail1')
})
mp.then(() => {
console.log('success2')
}, () => {
console.log('fail2')
})
- 與策略模式對比
策略模式中各個策略類之間是平等又平行的诬像,他們之間沒有任何聯(lián)系,客戶必須熟知這些策略類的所用闸婴,以便隨時主動切換算法坏挠;而在狀態(tài)模式中,狀態(tài)之間的切換早已被預(yù)先設(shè)定邪乍,“改變行為”這件事情發(fā)生在狀態(tài)模式內(nèi)部降狠,客戶并不需要了解這些細節(jié)。
2.10 原型模式
JavaScript
選擇了基于原型的面向?qū)ο笙到y(tǒng)庇楞。在原型編程的思想中榜配,類并不是必須的,對象未必從類中創(chuàng)建而來吕晌,一個對象可以通過克隆另一個對象而得到蛋褥。
2.10.1 使用克隆的原型模式
- 原型模式是用于創(chuàng)建對象的一種模式。我們不再關(guān)心對象的具體類型睛驳,而是找到一個對象烙心,然后通過克隆來創(chuàng)建一個一模一樣的對象
-
ES5
中提供了Object.create
方法,可以用來克隆對象乏沸。
let obj = {
getName() {
return this.first + ' ' + this.last
},
say() {
alert('hello')
}
}
let x = Object.create(obj)
x.first = 'A'
x.last = 'B'
alert(x.getName())
x.say()
在不支持Object.create
方法的瀏覽器中淫茵,則可以使用以下代碼:
//兼容不支持瀏覽器
Object.create = Object.create || function(obj) {
var F = function() {}
F.prototype = obj
return new F()
}
- 原型模式提供了一種便捷的方式去創(chuàng)建某個類型的對象,克隆只是創(chuàng)建這個對象的過程和手段蹬跃。通過克隆匙瘪,我們不再關(guān)心對象的具體類型。
2.10.2 JavaScript中的原型繼承
- 原型編程遵循以下基本規(guī)則
① 所有的數(shù)據(jù)都是對象。
② 要得到一個對象丹喻,不是通過實例化類薄货,而是找到一個對象作為原型并克隆它。
③ 對象會記住它的原型驻啤。
④ 如果對象無法響應(yīng)某個請求菲驴,它會把這個請求委托給自己的原型。 -
JavaScript
中的根對象是Object.prototype
骑冗,Object.prototype
對象是一個空的對象赊瞬。JavaScript
中的每一個對象,實際上都是從Object.prototype
對象克隆而來的贼涩。
var obj1 = new Object()
var obj2 = {}
console.log(Object.getPrototypeOf(obj1) === obj1.__proto__) //true
console.log(Object.getPrototypeOf(obj1) === Object.prototype) //true
console.log(Object.getPrototypeOf(obj2) === Object.prototype) //true
2.11 橋接模式
- 介紹
把抽象化與實現(xiàn)化解耦巧涧,使兩者可以獨立變化。 - 代碼演示
class Color {
constructor(name) {
this.name = name
}
}
class Shape {
constructor(name, color) {
this.name = name
this.color = color
}
draw() {
console.log(`${this.color.name} ${this.name}`)
}
}
let red = new Color('red')
let circle = new Shape('circle', red)
circle.draw() //red circle
2.12 組合模式
- 介紹
生成樹形結(jié)構(gòu)遥倦,表示“整體-部分”關(guān)系谤绳。讓整體和部分具有一致的操作方式。
注釋:組合模式最大的優(yōu)點在于可以一致地對待組合對象和基本對象袒哥∷跎福客戶不需要知道當前處理的是宏任務(wù)還是普通任務(wù)。 - 特點
① 使用樹形方式創(chuàng)建對象的結(jié)構(gòu)堡称。
② 把相同操作應(yīng)用在組合對象和單個對象上瞎抛。 - 使用場景
虛擬DOM
中的vnode
是這種形式。 - 組合模式注意事項
① 組合對象和葉對象是聚合關(guān)系而非父子關(guān)系却紧。
② 組合對象和葉對象擁有相同的接口桐臊。
③ 對組合對象和葉對象的操作必須具有一致性。
2.13 享元模式
- 介紹
享元模式是一種用于性能優(yōu)化的模式晓殊,享元模式的核心是運用共享技術(shù)來有效支持大量細粒度的對象断凶。 - 代碼演示
class Model {
constructor(sex) {
this.sex = sex
this.underwear = ''
}
setUnderwear(underwear) {
this.underwear = underwear
}
takePhoto() {
console.log(`sex: ${this.sex}; underwear: ${this.underwear}`)
}
}
const maleModel = new Model('male')
const femaleModel = new Model('female')
for(let i = 1; i<= 50; i++) {
maleModel.setUnderwear('underwear' + i)
maleModel.takePhoto()
}
for(let j = 1; j<= 50; j++) {
femaleModel.setUnderwear('underwear' + j)
femaleModel.takePhoto()
}
- 內(nèi)部狀態(tài)與外部狀態(tài)
享元模式要求將對象的屬性劃分為內(nèi)部狀態(tài)和外部狀態(tài)(狀態(tài)在這里通常指屬性)。
① 內(nèi)部狀態(tài)存儲于對象內(nèi)部巫俺。
② 內(nèi)部狀態(tài)可以被一些對象共享认烁。
③ 內(nèi)部狀態(tài)獨立于具體的場景,通常不會改變介汹。
④ 外部狀態(tài)取決于具體的場景砚著,并根據(jù)場景而變化,外部狀態(tài)不能被共享痴昧。
2.14 策略模式
- 介紹
定義一系列的算法稽穆,把不同策略封裝起來,并且使他們可以相互替換赶撰。
注釋:策略模式可以替代if...else...
語句舌镶。 - 代碼演示
//不使用策略模式
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 用戶購買')
}
}
}
let u1 = new User('member')
u1.buy() //會員用戶購買
//使用策略模式
class OridinaryUser {
buy() {
console.log('普通用戶購買')
}
}
class MemberUser {
buy() {
console.log('會員用戶購買')
}
}
class VipUser {
buy() {
console.log('vip 用戶購買')
}
}
class UserManager {
constructor() {
this.user = null
}
setUser(user) {
this.user= user
}
userBuy() {
this.user.buy()
}
}
const m = new UserManager()
const u = new OridinaryUser()
m.setUser(u)
m.userBuy() //普通用戶購買
注釋:策略模式的實現(xiàn)至少由兩部分組成柱彻。第一個部分是一組策略類(OridinaryUser
、MemberUser
餐胀、VipUser
)哟楷,策略類封裝了具體的算法,并負責具體的計算過程否灾。第二個部分是環(huán)境類(UserManager
)卖擅,環(huán)境類接受客戶的請求,隨后把請求委托給某一個策略類墨技。
-
JavaScript
中的策略模式
策略對象從各個策略類中創(chuàng)建而來惩阶,環(huán)境對象從環(huán)境類中創(chuàng)建而來,這是模擬一些傳統(tǒng)面向?qū)ο笳Z言的實現(xiàn)扣汪。在JavaScript
中断楷,函數(shù)也是對象,所以更簡單和直接的做法是把策略對象和環(huán)境對象直接定義為函數(shù)崭别。
const strategies = {
oridinary() {
console.log('普通用戶購買')
},
member() {
console.log('會員用戶購買')
},
vip() {
console.log('vip 用戶購買')
}
}
const userBuy = user => strategies[user]()
userBuy('member') //會員用戶購買
2.15 模板方法模式
- 介紹
在抽象父類中封裝了子類的算法框架冬筒,包括實現(xiàn)一些公共方法以及封裝子類中所有方法的執(zhí)行順序。子類通過繼承這個抽象類茅主,也繼承了整個算法結(jié)構(gòu)舞痰,并且可以選擇重寫父類的方法。 - 代碼演示
class Beverage {
init() {
this.boilWater()
this.brew()
this.pourInCup()
this.addCondiments()
}
boilWater() {
console.log('把水煮沸')
}
brew(){} //沸水沖泡飲料
pourInCup(){} //飲料倒進杯子
addCondiments(){} //加調(diào)料
}
class Coffee extends Beverage {
brew(){
console.log('用沸水沖泡咖啡')
}
pourInCup(){
console.log('把咖啡倒進杯子')
}
addCondiments(){
console.log('加糖和牛奶')
}
}
class Tea extends Beverage {
brew(){
console.log('用沸水浸泡茶葉')
}
pourInCup(){
console.log('把茶倒進杯子')
}
addCondiments(){
console.log('加檸檬')
}
}
const coffee = new Coffee()
coffee.init()
const tea = new Tea()
tea.init()
- 模式對比
策略模式和模板方法是一對競爭者诀姚。在大多數(shù)情況下匀奏,他們可以相互替換使用。模板方法模式基于繼承的思想学搜,而策略模式則偏重于組合和委托。
2.16 職責鏈模式
- 介紹
使多個對象都有機會處理請求论衍,從而避免請求的發(fā)送者和接收者之間的耦合關(guān)系瑞佩,將這些對象連成一條鏈,并沿著這條鏈傳遞該請求坯台,直到有一個對象處理它為止炬丸。 - 代碼演示
① 流程審批(職責鏈每個節(jié)點都處理)
class Action {
constructor(name) {
this.name = name
this.nextAction = null
}
setNextAction(action) {
this.nextAction = action
}
handle() {
console.log(`${this.name} 審批`)
this.nextAction && this.nextAction.handle()
}
}
const action1 = new Action('組長')
const action2 = new Action('經(jīng)理')
const action3 = new Action('總監(jiān)')
action1.setNextAction(action2)
action2.setNextAction(action3)
action1.handle()
② 購買商品(職責鏈只有一個節(jié)點處理)
const order500 = (orderType, pay, stock) => {
if(orderType === 1 && pay === true){
console.log('500元定金預(yù)購,得到100優(yōu)惠券')
return true
}
return false
}
const order200 = (orderType, pay, stock) => {
if(orderType === 2 && pay === true){
console.log('200元定金預(yù)購蜒蕾,得到50優(yōu)惠券')
return true
}
return false
}
const orderNormal = (orderType, pay, stock) => {
if(stock > 0){
console.log('普通購買稠炬,無優(yōu)惠券')
} else {
console.log('手機內(nèi)存不足')
}
return true
}
class Chain {
constructor(fn) {
this.fn = fn
this.successor = null
}
setNextSuccessor(successor){
this.successor = successor
}
passRequest() {
const res = this.fn.apply(this, arguments)
return res ? res : this.successor && this.successor.passRequest.apply(this.successor, arguments)
}
}
const chainOrder500 = new Chain(order500)
const chainOrder200 = new Chain(order200)
const chainOrderNormal = new Chain(orderNormal)
chainOrder500.setNextSuccessor(chainOrder200)
chainOrder200.setNextSuccessor(chainOrderNormal)
chainOrder500.passRequest(1, true, 500) //500元定金預(yù)購,得到100優(yōu)惠券
chainOrder500.passRequest(2, true, 500) //200元定金預(yù)購咪啡,得到50優(yōu)惠券
chainOrder500.passRequest(3, false, 500) //普通購買首启,無優(yōu)惠券
chainOrder500.passRequest(3, false, 0) //手機內(nèi)存不足
- 使用場景
① 作用域鏈
② 原型鏈
③ 事件冒泡
④jQuery
的鏈式操作
⑤Promise.then
的鏈式操作
2.17 命令模式
- 介紹
執(zhí)行命令時,發(fā)布者和執(zhí)行者分開撤摸。中間加入命令對象毅桃,作為中轉(zhuǎn)站褒纲。 - 應(yīng)用場景
有時候需要向某些對象發(fā)送請求,但是并不知道請求的接收者是誰钥飞,也不知道被請求的操作是什么莺掠。此時希望用一種松耦合的方式來設(shè)計程序,使得請求發(fā)送者和請求接受者能夠消除彼此之間的耦合關(guān)系读宙。 - 代碼演示
//接受者
class Receiver{
exec() {
console.log('執(zhí)行')
}
}
//命令者
class Command {
constructor(receiver) {
this.receiver = receiver
}
cmd() {
console.log('執(zhí)行命令')
this.receiver.exec()
}
}
//觸發(fā)者
class Invoker {
constructor(command) {
this.command = command
}
invoke() {
console.log('開始')
this.command.cmd()
}
}
//士兵
const soldier = new Receiver()
//小號手
const trumpeter = new Command(soldier)
//將軍
const general = new Invoker(trumpeter)
general.invoke()
-
JavaScript
中的命令模式
命令模式的由來彻秆,其實是回調(diào)(callback
)函數(shù)的一個面向?qū)ο蟮奶娲贰?code>JavaScript將函數(shù)作為一等對象,與策略模式一樣结闸,命令模式早已融入到了JavaScript
語言中唇兑。運算塊可以封裝在普通函數(shù)中。函數(shù)作為一等對象膀估,本身就可以被四處傳遞幔亥。
const bindClick = (button, func) => {
button.onclick = func
}
const MenuBar = {
refresh() {
console.log('刷新菜單界面')
}
}
const SubMenu = {
add() {
console.log('增加子菜單')
},
del() {
console.log('刪除子菜單')
}
}
bindClick(button1, MenuBar.refresh)
bindClick(button2, SubMenu.add)
bindClick(button3, SubMenu.del)
2.18 備忘錄模式
- 介紹
隨時記錄一個對象的狀態(tài)變化。隨時可以恢復(fù)之前的某個狀態(tài)(如撤銷功能)察纯。 - 代碼演示
//備忘類
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 {
constructot() {
this.content = null
}
setContent(content) {
this.content = content
}
getContent() {
return this.content
}
saveContentToMemento() {
return new Memento(this.content)
}
setContentFromMemento(mement) {
return this.content = mement.getContent()
}
}
//測試代碼
const editor = new Editor()
const careTaker = new CareTaker()
editor.setContent('1')
editor.setContent('2')
careTaker.add(editor.saveContentToMemento())
editor.setContent('3')
careTaker.add(editor.saveContentToMemento())
editor.setContent('4')
console.log(editor.getContent()) // 4
editor.setContentFromMemento(careTaker.get(1)) //撤銷
console.log(editor.getContent()) // 3
editor.setContentFromMemento(careTaker.get(0)) //撤銷
console.log(editor.getContent()) // 2
2.19 中介者模式
- 介紹
解除對象與對象之間的緊耦合關(guān)系帕棉。增加一個中介者對象后,所有的相關(guān)對象都通過中介者對象來通訊饼记,而不是互相引用香伴。當一個對象發(fā)生改變時,只需要通知中介者對象即可具则。 -
中介者模式使網(wǎng)狀的多對多關(guān)系變成了相對簡單的一對多關(guān)系
image.png - 現(xiàn)實中的中介者
① 機場指揮塔
② 博彩公司 - 代碼演示
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
}
setB() {
let num = this.a.number
this.b.setNumber(num / 10)
}
setA() {
let num = this.b.number
this.a.setNumber(num + 5)
}
}
const a = new A()
const b = new B()
const 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)
2.20 訪問者模式
- 介紹
將數(shù)據(jù)操作與數(shù)據(jù)結(jié)構(gòu)進行分離即纲。
2.21 解釋器模式
- 介紹
描述語言語法如何定義,如何解釋和編譯博肋。
3. 設(shè)計原則和編程技巧
3.1 設(shè)計原則概述
- 《
UNIX/LINUX
設(shè)計哲學》設(shè)計準則
① 小既是美低斋。
② 每個程序只做一件事情。
③ 快速建立原型匪凡。
④ 舍棄高效率而取可移植性膊畴。
⑤ 避免強制性的圖形化界面交互。
⑥ 讓每個程序都成為過濾器病游。
⑦ 尋求90%的解決方案唇跨。
注釋:花20%的成本解決80的需求。 - 五大設(shè)計原則(
SOLID
)
①S
- 單一職責原則
②O
- 開放封閉原則
③L
- 李氏置換原則
④I
- 接口獨立原則
⑤D
- 依賴倒置原則 - 單一職責原則
一個程序只做好一件事情衬衬。 - 開放封閉原則
對擴展開放买猖,對修改封閉。
增加需求時滋尉,擴展新代碼玉控,而非修改已有代碼。 - 李氏置換原則
子類能覆蓋父類狮惜。
父類能出現(xiàn)的地方子類就能出現(xiàn)奸远。 - 接口獨立原則
保持接口的獨立既棺,避免出現(xiàn)“胖接口”。 - 依賴倒置原則
面向接口編程懒叛,依賴于抽象而不依賴于具體丸冕。
3.2 單一職責原則
- 簡介
就一個類、對象以及方法而言薛窥,應(yīng)該僅有一個引起它變化的原因胖烛。
注釋:單一職責原則定義為“引起變化的原因”。如果我們有兩個動機去改寫一個方法诅迷,那么這個方法就具有兩個職責佩番。 - 原則
一個對象(方法)只做一件事情。 - 設(shè)計模式驗證
① 代理模式
圖片預(yù)加載代理模式中罢杉,代理對象負責預(yù)加載職責趟畏,本體對象負責圖片加載職責。
② 迭代器模式
迭代器模式提供遍歷訪問聚合對象的職責滩租。
③ 單例模式
將創(chuàng)建對象與管理單例分別封裝在兩個方法中赋秀,兩個方法相互獨立互不影響。
④ 裝飾者模式
讓類或者對象一開始只具有一些基礎(chǔ)的職責律想,更多的職責在代碼運行時被動態(tài)裝飾到對象上面猎莲。裝飾者模式可以為對象動態(tài)增加職責,從另一個角度來看技即, 這也是分離職責的一種方式著洼。 - 應(yīng)用場景
① 如果有兩個職責總是同時變化,那就不必分離他們而叼。即使兩個職責已經(jīng)被耦合在一起身笤,但它們還沒有發(fā)生改變的征兆,那么也許沒有必要主動分離它們葵陵。
② 在方便性與穩(wěn)定性之間要有一些取舍液荸。具體是選擇方便性還是穩(wěn)定性,并沒有標準答案埃难,而是要取決于具體的應(yīng)用環(huán)境。例如:jQuery
的attr
是個非常龐大的方法涤久,既負責賦值涡尘,又負責取值,這對于jQuery
的維護者來說响迂,會帶來一些困難考抄,但對于jQuery
的用戶來說莱预,卻簡化了用戶的使用苍日。 - 優(yōu)缺點
① 優(yōu)點
按照職責把對象分解成更小的粒度,降低了單個類或者對象的復(fù)雜度,有助于代碼的復(fù)用铃诬,也有利于進行單元測試。
② 缺點
增加了編寫代碼的復(fù)雜度擂达,也增大了這些對象之間相互聯(lián)系的難度蹋肮。
3.3 最少知識原則
- 簡介
最少知識原則(LKP
)指一個軟件實體應(yīng)當盡可能少地與其他實體發(fā)生相互作用。這里的軟件實體不僅包括對象丢早,還包括系統(tǒng)姨裸、類、模塊怨酝、函數(shù)傀缩、變量等。 - 減少對象之間的聯(lián)系
單一職責原則指導我們把對象劃分成較小的粒度农猬,提高對象的可復(fù)用性赡艰。但越來越多的對象之間可能會產(chǎn)生錯綜復(fù)雜的聯(lián)系,如果修改了其中一個對象斤葱,很可能會影響到跟它相互引用的其他對象慷垮。
最少知識原則要求我們盡量減少對象之間的交互。如果兩個對象之間不必彼此直接通信苦掘,那么這兩個對象就不要發(fā)生直接的相互聯(lián)系换帜。 - 設(shè)計模式驗證
① 中介者模式
增加一個中介者對象,讓所有的相關(guān)對象都通 過中介者對象來通信鹤啡,而不是互相引用惯驼。當一個對象發(fā)生改變時,只需要通知中介者對象即可递瑰。
② 外觀模式
外觀模式對客戶提供一個簡單易用的高層接口祟牲,高層接口會把客戶的請求轉(zhuǎn)發(fā)給子系統(tǒng)來完成具體的功能實現(xiàn)。
3.4 開放-封閉原則
- 簡介
軟件實體(類抖部、模塊说贝、函數(shù))等應(yīng)該是可以擴展的,但是不可修改慎颗。 - 原則
當需要改變一個程序的功能或者給這個程序增加新功能的時候乡恕,可以使用增加代碼的方式,但是不允許改動程序的源代碼俯萎。 - 實現(xiàn)方式
通過封裝變化的方式傲宜,可以把系統(tǒng)中穩(wěn)定不變的部分和容易變化的部分隔離開來。
(1) 利用對象多態(tài)性
利用對象的多態(tài)性來消除條件分支語句夫啊。
(2) 放置掛鉤
在程序有可能發(fā)生變化的地方放置一個掛鉤函卒,掛鉤的返回結(jié)果決定了程序的下一步走向。
(3) 回調(diào)函數(shù)
把一部分易于變化的邏輯封裝在回調(diào)函數(shù)里撇眯,然后把回調(diào)函數(shù)當作參數(shù)傳入一個穩(wěn)定和封閉的函數(shù)中报嵌。 - 設(shè)計模式驗證
① 觀察者模式
當有新的訂閱者出現(xiàn)時虱咧,發(fā)布者的代碼不需要進行任何修改;同樣當發(fā)布者需要改變時锚国,也不會影響到之前的訂閱者腕巡。
② 模板方法模式
子類的方法種類和執(zhí)行順序都是不變的,所以我們把這部分邏輯抽出來放到父類的模板方法里面跷叉;而子類的方法具體怎么實現(xiàn)則是可變的逸雹,于是把這部分變化的邏輯封裝到子類中。通過增加新的子類云挟,便能給系統(tǒng)增加新的功能梆砸,并不需要改動抽象父類以及其他的子類。
③ 策略模式
策略模式將各種算法都封裝成單獨的策略類园欣,這些策略類可以被交換使用帖世。策略和使用策略的客戶代碼可以分別獨立進行修改而互不影響。
④ 代理模式
圖片預(yù)加載示例中沸枯,代理函數(shù)proxyMyImage
負責圖片預(yù)加載日矫,myImage
圖片加載函數(shù)不需要任何改動。
⑤ 職責鏈模式
新增處理函數(shù)時绑榴,不需要改動原有的鏈條節(jié)點代碼哪轿,只需要在鏈條中增加一個新的節(jié)點。
3.5 代碼重構(gòu)
- 提煉函數(shù)
如果在函數(shù)中有一段代碼可以被獨立出來翔怎,那我們最好把這些代碼放進另外一個獨立的函數(shù)中窃诉。
var getUserInfo = function () {
ajax('http:// xxx.com/userInfo', function (data) {
console.log('userId: ' + data.userId);
console.log('userName: ' + data.userName);
console.log('nickName: ' + data.nickName);
})
}
//改成
var getUserInfo = function () {
ajax('http:// xxx.com/userInfo', function (data) {
printDetails(data);
});
};
var printDetails = function (data) {
console.log('userId: ' + data.userId);
console.log('userName: ' + data.userName);
console.log('nickName: ' + data.nickName);
};
- 合并重復(fù)的條件片段
如果一個函數(shù)體內(nèi)有一些條件分支語句,而這些條件分支語句內(nèi)部散布了一些重復(fù)的代碼赤套,那么就有必要進行合并去重工作飘痛。
var paging = function (currPage) {
if (currPage <= 0) {
currPage = 0;
jump(currPage); // 跳轉(zhuǎn)
} else if (currPage >= totalPage) {
currPage = totalPage;
jump(currPage);
} else {
jump(currPage);
}
};
//改成
var paging = function (currPage) {
if (currPage <= 0) {
currPage = 0;
} else if (currPage >= totalPage) {
currPage = totalPage;
}
jump(currPage); // 把 jump 函數(shù)獨立出來
};
- 把條件分支語句提煉成函數(shù)
在程序設(shè)計中,復(fù)雜的條件分支語句是導致程序難以閱讀和理解的重要原因容握,而且容易導致一個龐大的函數(shù)宣脉。
var getPrice = function (price) {
var date = new Date();
if (date.getMonth() >= 6 && date.getMonth() <= 9) {
return price * 0.8;
}
return price;
};
//改成
var isSummer = function () {
var date = new Date();
return date.getMonth() >= 6 && date.getMonth() <= 9;
};
var getPrice = function (price) {
if (isSummer()) { // 夏天
return price * 0.8;
}
return price;
};
- 合理使用循環(huán)
在函數(shù)體內(nèi),如果有些代碼實際上負責的是一些重復(fù)性的工作剔氏,那么合理利用循環(huán)不僅可以 完成同樣的功能塑猖,還可以使代碼量更少。
var createXHR = function () {
var xhr;
try {
xhr = new ActiveXObject('MSXML2.XMLHttp.6.0');
} catch (e) {
try {
xhr = new ActiveXObject('MSXML2.XMLHttp.3.0');
} catch (e) {
xhr = new ActiveXObject('MSXML2.XMLHttp');
}
}
return xhr;
};
var xhr = createXHR();
//改成
var createXHR = function () {
var versions = ['MSXML2.XMLHttp.6.0ddd', 'MSXML2.XMLHttp.3.0', 'MSXML2.XMLHttp'];
for (var i = 0, version; version = versions[i++];) {
try {
return new ActiveXObject(version);
} catch (e) {
}
}
};
var xhr = createXHR();
- 提前讓函數(shù)退出代替嵌套條件分支
嵌套的條件分支語句絕對是代碼維護者的噩夢谈跛。嵌套的條件分支往往是由一些深信“每個函數(shù)只能有一個出口的”程序員寫出的羊苟。但實際上,如果對函數(shù)的剩余部分不感興趣币旧,那就應(yīng)該立即退出践险。
var del = function (obj) {
var ret;
if (!obj.isReadOnly) { // 不為只讀的才能被刪除
if (obj.isFolder) { // 如果是文件夾
ret = deleteFolder(obj);
} else if (obj.isFile) {
ret = deleteFile(obj);
}
}
return ret;
};
var del = function (obj) {
if (obj.isReadOnly) {
return;
}
if (obj.isFolder) {
return deleteFolder(obj);
}
if (obj.isFile) {
return deleteFile(obj);
}
};
- 傳遞對象參數(shù)代替過長的參數(shù)列表
有時候一個函數(shù)有可能接收多個參數(shù)猿妈,而參數(shù)的數(shù)量越多吹菱,函數(shù)就越難理解和使用巍虫。在使用的時候,還要小心翼翼鳍刷,以免少傳了某個參數(shù)或者把兩個參數(shù)搞反了位置占遥。
這時我們可以把參數(shù)都放入一個對象內(nèi),不用再關(guān)心參數(shù)的數(shù)量和順序输瓜,只要保證參數(shù)對應(yīng)的key
值不變就可以了瓦胎。 - 盡量減少參數(shù)數(shù)量
在實際開發(fā)中,向函數(shù)傳遞參數(shù)不可避免尤揣,但我們應(yīng)該盡量減少函數(shù)接收的參數(shù)數(shù)量搔啊。 - 少用三目運算符
如果條件分支邏輯簡單且清晰,這無礙我們使用三目運算符北戏;但如果條件分支邏輯非常復(fù)雜负芋,我們最好的選擇還是按部就班地編寫if
、else
嗜愈。 - 合理使用鏈式調(diào)用
經(jīng)常使用jQuery
的程序員相當習慣鏈式調(diào)用方法旧蛾,在JavaScript
中,可以很容易地實現(xiàn)方法的鏈式調(diào)用蠕嫁,即讓方法調(diào)用結(jié)束后返回對象自身锨天。
使用鏈式調(diào)用的方式可以省下一些字符和中間變量,但調(diào)試時非常不方便剃毒。如果我們知道一條鏈中有錯誤出現(xiàn)病袄,必須得先把這條鏈拆開才能加上一些調(diào)試log
或者增加斷點。
如果該鏈條的結(jié)構(gòu)相對穩(wěn)定迟赃,后期不易發(fā)生修改陪拘,那么使用鏈式調(diào)用無可厚非。但如果該鏈 條很容易發(fā)生變化纤壁,導致調(diào)試和維護困難左刽,那么還是建議使用普通調(diào)用的形式。 - 分解大型類
面向?qū)ο笤O(shè)計鼓勵將行為分布在合理數(shù)量的更小對象之中酌媒。 - 用
return
退出多重循環(huán)
假設(shè)在函數(shù)體內(nèi)有一個兩重循環(huán)語句欠痴,我們需要在內(nèi)層循環(huán)中判斷,當達到某個臨界條件時退出外層的循環(huán)秒咨。我們大多數(shù)時候會引入一個控制標記變量或設(shè)置循環(huán)標記喇辽。
這兩種做法無疑都讓人頭暈?zāi)垦#唵蔚淖龇ㄊ窃谛枰兄寡h(huán)的時候直接退出整個方法雨席。
如果在循環(huán)之后還有一些將被執(zhí)行的代碼菩咨,我們可以把循環(huán)后面的代碼放到 return 后面,如果代碼比較多,就應(yīng)該把它們提煉成一個單獨的函數(shù)抽米。
4. 綜合案例
4.1 模擬購物車
- 案例分析
(1) 使用jQuery
做一個模擬購物車示例特占。
(2) 顯示購物列表、加入購物車云茸、從購物車刪除 - 涉及到的設(shè)計模式
(1) 創(chuàng)建型
工廠模式是目、單例模式
(2) 結(jié)構(gòu)型
裝飾器模式、代理模式
(3) 行為型
觀察者模式标捺、狀態(tài)模式懊纳、模板方法模式 -
UML
類圖
image.png - 代碼演示
imooc-design-mode
5. 項目應(yīng)用場景總結(jié)
- 單例模式
①mainboard
為單例,是否可以不聲明為類亡容,直接字面量對象嗤疯。
② 利用裝飾器模式,給類添加管理單例的方法闺兢,與創(chuàng)建對象的方法分離身弊。
③ 現(xiàn)有通過this._groupsMap
緩存的方式使用單例模式替換。 - 裝飾器模式
①app
監(jiān)控功能
②titleWidgetModel
裝飾類 - 代理模式
②BaseLayout/utils.js
中的createZoomSelectComponent
方法使用緩存代理列敲。 - 職責鏈模式
①BaseLayout/utils.js
中的createZoomSelectComponent
方法使用職責鏈模式和緩存代理阱佛。 - 組合模式
① 布局與組件的關(guān)系可以使用組合模式。先創(chuàng)建整個表單的樹結(jié)構(gòu)戴而,再逐層遍歷凑术,而不是生成單層布局-控件結(jié)構(gòu)。 - 策略模式
① 參數(shù)面板兩種動畫所意。
② 目錄跳轉(zhuǎn)cpt/frm
淮逊。 - 狀態(tài)模式
① 可放大組件的放大狀態(tài)。
② 可選中組件的選中狀態(tài)扶踊。
③ 參數(shù)面板展示隱藏狀態(tài)泄鹏。
參考資料
- Javascript 設(shè)計模式系統(tǒng)講解與應(yīng)用
- 《JavaScript設(shè)計模式與開發(fā)實踐》(曾探)