JavaScript中的设计模式
- 原型模式
- 代理模式
- 迭代器模式
javascript 中提供的api可以帮助实现这几种模式
原型模式
原型链中的原型就是原型模式
- 定义:复制已有对象来创建新的对象
- 应用场景:JS 中对象创建的基本模式
在某些特定的场景中,庞大对象的初始化,复制一个已有对象来创建一个新对象会有更好的性能,内存占用也会更少
在JavaScript中是常见的模式,但在其他语言中几乎不会用到
用原型模式创建上线订阅中的用户
javascript 提供Object.create(obj)基于obj创建一个对象
const baseUser: User = {
name: "",
status: "offline",
followers: []
subscribe(user: User, notify: Notify){
user.followers.push({user, notify})
},
online(){
this.status = "online"
this.followers.forEach(({notify} => {
notify(this)
})
}
}
// 复制已有对象创建新对象
export const createUser = (name: string) => {
// 以baseUser为原型创建一个新的对象user
const user: User = Object.create(baseUser)
user.name = name
user.followers = []
return user
}
代理模式
- 定义:可以自定义控制对象的访问方式,并且允许在更新前后做一些额外的处理
- 应用场景:监控,代理工具,前端框架实现等
使用代理模式实现用户状态订阅
单一原则,一个函数只做一件事情
这里的online既改变了status的状态,又通知所有notify函数的执行,实际上做了两件事情,不利于后续增加日志以及代码的添加和重构
type Notify = (user:User) => void // 订阅者(订阅者也可以是一个函数)
export class User {
name:string
status: "offline" | "online"
followers: {user: User; notify: Notify}[] // 存储不同的user对应的回调函数(订阅)
constructor(name: string){
this.name = name
this.status = "offline"
this.followers = []
}
// 订阅
subscribe(user: User, notify: Notify){
user.followers.push({user, notify})
}
// 发布
online(){
this.status = "online"
this.followers.forEach(({notify} => {
notify(this)
})
}
}
用代理模式对online函数进行优化
- 让
online只做一件事情
class User {
//....
online(){
this.status = "online"
}
}
- 代理
使用代理方式可以根据需要对代理对象进行配置和处理,使得函数更加可扩展和可复用。
采用createProxyUser将创建对象和代理行为包起来可以起到分支和隔离的作用
- 封装:
createProxyUser函数充当了一个工厂函数的角色,负责创建带有代理功能的用户对象。 - 隔离性:可以限制代理对象的作用范围,仅在
createProxyUser函数内部可见,可以避免代理对象的误用或直接修改,提高代码的安全性和可维护性。
export const createProxyUser = (name: string) => {
const user = new User(name)
const proxyUser = new Proxy(user, {
set: (target, prop: keyof User, value) => {
target[prop] = value
// 如果是status的值发生变化则执行notifyStatusHandlers函数
if(prop === 'status') {
notifyStatusHandlers(target, value)
// 如果要监听或status的值改变需要触发什么新的行为,都可以在继续加
// ...
}
}
})
const notifyStatusHandlers = (user: User, status: 'online' | 'offline') => {
if(status === 'online') {
user.followers.forEach({notify} => {
notify(user)
})
}
}
return prosyUser
// 可以添加其他的新行为
// ...
}
迭代器模式
- 定义:在不暴露数据类型的情况下访问集合中的数据
- 应用场景:数据结构中有多种数据类型,列表,树等,提供通用操作接口
const number = [1, 2, 3]
const map = new Map()
map.set('k1', 'v1')
map.set('k2', 'v2')
const set = new Set(['1', '2', '3'])
for(const number of numbers) {}
for(const [key, value] of map){}
for(const key of set) {}
for...of...
for...of要求目标对象实现了迭代器接口,即对象必须具有Symbol.iterator属性,并且该属性是一个函数,返回一个迭代器对象。特性:
- 遍历可迭代对象:
for...of可以用于遍历实现了迭代器(Iterator)接口的数据结构,包括数组、字符串、Set、Map 等。它提供了一种简洁的方式来访问可迭代对象中的每个元素。- 遍历值而非索引:
for...of可以直接获取可迭代对象中的值,而不是索引- 不支持普通对象:
for...of不适用于普通对象的遍历。如果需要遍历普通对象的属性,可以使用for...in循环。- 对象不支持顺序遍历:
for...of在遍历集合时是按顺序进行的,但在遍历对象属性时并不保证顺序,因为对象属性之间没有固定的顺序。性能:
- 性能较好:相比传统的
for循环或forEach方法,for...of通常具有更好的性能表现。这是因为它基于迭代器的机制,在每次迭代时都会调用迭代器的next()方法,无需在每次迭代中重新计算数组长度或手动管理索引- 不支持修改:由于
for...of是基于迭代器的机制,它并不支持修改可迭代对象的值。如果在循环过程中修改了可迭代对象,可能会导致意外的行为或错误结果。如果需要修改可迭代对象,建议使用传统的for循环或其他遍历方式。
用 for...of 迭代所有组件
//
class MyDomElement {
tag: string
children: MyDomElement[]
constructor(tag: string) {
this.tag = tag
this.children = []
}
addChildren(component: MyDomElement) {
this.children.push(component)
}
// 迭代方法,使该类实例化的对象可以用for...of进行迭代
[Symbol.iterator]() {
const list = [...this.children]
let node
return {
next: () => {
while((node = list.shift()){ // list 中还有元素
node.children.length > 0 && list.push(...node.children)
return { value: node, done: false }
}
return { value: null, done: true }
}
}
}
}
// 测试用例
test("can iterate root element", () => {
const body = new MyDomElement("body")
const header = new MyDomElement("header")
const main = new MyDomElement("main")
const banner = new MyDomElement("banner")
const content = new MyDomElement("content")
const footer = new MyDomElement("footer")
body.addChildren(header)
body.addChildren(main)
body.addChildren(footer)
main.addChildren(banner)
main.addChildren(content)
const expectTags: string[] = []
for (const element of body) {
if (element) {
expectTags.push(element.tag)
}
}
expect(expectTags.length).toBe(5);
})
前端框架中的设计模式
代理模式
前端框架中对DOM操作的代理
在前端框架中操作的DOM 都是代理后的DOM,代理的DOM会和真正的DOM进行Diff操作,最后才进行视图的更新
这种代理在vue3中是用proxy实现的,在react中是用另外的方式,但原理都是当更改DOM时,真正改的都是代理后的虚拟DOM
组合模式
- 定义:可以多个对象组合使用,也可以单个对象独立使用
- 应用场景:DOM,前端组件,文件目录,部门
总结
- 总结出抽象的模式相对简单,但是想要将抽象的模式套用到场景中却比较困难
- 现代编程语言的多编程范式带来的更多可能性(函数式编程等)