js设计模式
设计模式(Design pattern)代表了最佳的实践,通常被有经验的面向对象的软件开发人员所采用。设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。
而虽然js前端使用设计模式不像后端那么频繁,但是依然是一名程序员必备的本领,本文通过使用实例方便大家理解记忆设计模式,可用于面试前速记。
一. 设计模式介绍
在js设计模式中,核心:封装变化。
将变与不变分离,确保变化灵活,不变稳定。
- 构造器原型模式
使用设计模式前:
javascript
var employee1 = {
name:"zs",
age:18
}
var employee2 = {
name:"ls",
age:19
}
在构造对象时,如果数据量变多,对象的代码会变得冗余臃肿
使用设计模式后:
javascript
function Employee(name,age){
this.name = name;
this.age =age;
this.say = function(){
console.log(this.name+"-",this.age)
}
}
new Employee("zs",18)
new Employee("ls",19)
- 工厂模式
由一个工厂对象决定创建某一种产品的对象类的实例。主要用来创建同一类对象。
只需要传入参数,就能返回具体的实例。
javascript
function UserFactory(role){
function User(role,pages){
this.role = role;
this.pages = pages;
}
switch(role){
case "superadmin":
return new User("superadmin",["home","user-manage","right-manage","news-manage"])
break;
case "admin":
return new User("admin",["home","user-manage","news-manage"])
break;
case "editor":
return new User("editor",["home","news-manage"])
break;
default:
throw new Error('参数错误')
}
}
- 抽象工厂模式
相比于工厂模式,抽象工厂模式会抽离出一个不变的公共父类。其子类就是各种的不同类。再通过函数判断返回这写不同的类。它并不会直接生成可用实例。而是返回不同的类,让开发者得到想要的类后自行实例化。
javascript
class User {
constructor(name) {
this.name = name
}
welcome() {
console.log("欢迎回来", this.name)
}
dataShow() {
throw new Error("抽象方法不允许直接调用")
}
}
class Editor extends User {
constructor(name) {
super(name)
this.role = "editor"
this.pages = ["home", "news-manage"]
}
dataShow() {
console.log("editor的可视化逻辑")
}
}
class Admin extends User {
constructor(name) {
super(name)
this.role = "admin"
this.pages = ["home", "user-manage", "news-manage"]
}
dataShow() {
console.log("admin的可视化逻辑")
}
AddUser() {
console.log("adduser方法")
}
}
class SuperAdmin extends User {
constructor(name) {
super(name)
this.role = "superadmin"
this.pages = ["home", "user-manage", "right-manage", "news-manage"]
}
dataShow() {
console.log("superadmin的可视化逻辑")
}
AddUser() {
console.log("adduser方法")
}
AddRight() {
console.log("addright方法")
}
}
function getAbstractUserFactory(role) {
switch (role) {
case 'superadmin':
return SuperAdmin;
break;
case 'admin':
return Admin;
break;
case 'editor':
return Editor;
break;
default:
throw new Error('参数错误')
}
}
- 建造者模式:
假如你是一个工厂的设计师,同过前面的工厂或抽象工厂模式,已经获得了一个个独立的零件。接下来要拼接在一起成一个新的大零件。你现在已经不需要关心小零件构造,只需要关心大零件的组成结构与顺序。这就是建造者模式!
javascript
class Navbar {//没一个类的细节都不同
init() {
console.log("navbar-init")
}
getData() {
return new Promise((resolve) => {
setTimeout(() => {
resolve()
console.log("navbar-getData")
}, 1000)
})
}
render() {
console.log("navbar-render")
}
}
class List {
init() {
console.log("List-init")
}
getData() {
return new Promise((resolve) => {
setTimeout(() => {
resolve()
console.log("List-getData")
}, 1000)
})
}
render() {
console.log("List-render")
}
}
class Operator {
async startBuild(builder) {//设计师只关心这个函数中代码的运行逻辑
await builder.init()
await builder.getData()
await builder.render()
}
}
const op = new Operator();
const navbar = new Navbar();
const list = new List();
op.startBuild(navbar);
op.startBuild(list);
- 单例模式
作为设计师的你,有一个工具包,工具包里有且只有一个趁手的工具。你每次都只会使用这个工具。然而一开始你并没有工具包,你可以买一个并一直使用这一个
为防止全局污染可以使用闭包来封闭一个变量,这就是单例模式的原理。
保证一个类仅有一个实例,并提供一个访问它的全局访问点。
主要解决一个全局使用的类频繁地创建和销毁,占用内存。
javascript
const Modal = (function () {
let modal = null
return function () {//返回一个类,但是由于modal变量访问了外层作用域所以缓存不会立即释放
if (!modal) {
modal = document.createElement('div')
modal.innerHTML = '登录对话框'
modal.className = 'kerwin-modal'
modal.style.display = 'none'
document.body.appendChild(modal)
}
return modal
}
})()
document.querySelector('#open').addEventListener('click', function () {
const modal = new Modal()
modal.style.display = 'block'
})
document.querySelector('#close').addEventListener('click', function () {
const modal = new Modal()
modal.style.display = 'none'
})
- 装饰器模式
假设工厂中生产的是一台电视,在设计电视时,总会有开机广告,这是固定好的但又跟电视本身的内容无关联,那么开机广告就是有前置装饰器设置的。
对已有功能进行拓展,这样不会更改原有代码,对其他的业务产生影响,这方便我们在较少的改动下对软件进行拓展。
javascript
Function.prototype.before = function (beforeFn) {
var _this = this;
return function () {
beforeFn.apply(this, arguments);
return _this.apply(this, arguments);
};
};
Function.prototype.after = function (afterFn) {
var _this = this;//函数体本省
return function () {//返回一个新的函数
var ret = _this.apply(this, arguments);
afterFn.apply(this, arguments);
return ret;
};
};
function test() {
console.log("11111")
}
var test1 = test.before(() => {
console.log("00000")
}).after(()=>{
console.log("22222")
})
test1()
- 适配模式
工厂中的水管并不是大小一样的,这时候使用一个首尾大小不一样的(适配器)工具可以解决此问题
可以理解为类中的函数柯里化
将一个类的接口转换成客户希望的另一个接口。适配器模式让那些接口不兼容的类可以一起工作,比如在原本的函数外再嵌一个函数,这样调用函数名变了但我们使用适配器改变了原有的函数名变为了调用的函数名(这在以下项目中更改接口api很有用!)
javascript
//按照官网代码复制
class TencentMap {
show() {
console.log('开始渲染腾讯地图');
}
}
//按照官网代码复制
class BaiduMap {
display() {
console.log('开始渲染百度地图');
}
}
class BaiduMapAdapter extends BaiduMap {
constructor() {
super();
}
render() {
this.display();
}
}
class TencentMapAdapter extends TencentMap {
constructor() {
super();
}
render() {
this.show();
}
}
// 外部调用者
function renderMap(map) {
map.render(); // 统一接口调用
}
renderMap(new TencentMapAdapter());
renderMap(new BaiduMapAdapter());
- 策略模式
假如你作为设计师处理一件很复杂的事情,你可以选择一个人扛,也可以选择采用身边助理的意见。策略模式就是如此。将if else分开放到对象中去。这样想要加逻辑时就不必关注if else的先后顺序。
javascript
var list = [{
title: "男人看了沉默",
type: 1
},
{
title: "震惊",
type: 2
},
{
title: "kerwin来了",
type: 3
},
{
title: "tiechui离开了",
type: 2
}
]
let obj = {
1: {
content: "审核中",
className: "yellowitem"
},
2: {
content: "已通过",
className: "greenitem"
},
3: {
content: "被驳回",
className: "reditem"
}
}
mylist.innerHTML = list.map(item =>
`
<li>
<div>${item.title}</div>
<div class="${obj[item.type].className}">${obj[item.type].content}</div>
</li>
`).join("")
- 代理模式
代理模式(Proxy),为其他对象提供一种代理以控制对这个对象的访问。
代理模式使得代理对象控制具体对象的引用。代理几乎可以是任何对象:文件,资源,内存中的对象,或者是一些难以复制的东西。
准确来说就好似Proxy进行代理,通过其代理功能可以过滤掉一些禁用。缺点是必须访问代理后的对象,不会影响原有对象。vue3响应式底层原理
javascript
let obj = {}
let proxy = new Proxy(obj,{
get(target,key){
console.log("get",target[key])
return target[key]
},
set(target,key,value){
console.log("set",target,key,value)
if(key==="data"){
box.innerHTML = value
}
target[key] = value
}
})
- 观察者模式
作为总设计师,会有许多手下,他们都是看你办事,你要是发出号令,他们就得相应做出变化
一个被观察对象有许多观察者对象,被观察者变化,观察者发生相应变化。
javascript
class Sub {
constructor() {
this.observers = []
}
add(observer) {
this.observers.push(observer)
}
remove(observer) {
this.observers = this.observers.filter(item => item !== observer)
}
notify() {
this.observers.forEach(item => item.update())
}
}
class Observer {
constructor(name) {
this.name = name
}
update() {
console.log("通知了", this.name)
}
}
const observer1 = new Observer("kerwin")
const observer2 = new Observer("tiechui")
const sub = new Sub()
sub.add(observer1)
sub.add(observer2)
setTimeout(() => {
sub.notify()
}, 2000)
- 发布订阅模式
虽然作为总设计师有很大权利,但总是有上级的,上级有时会发布一些任务下来,这些任务有的不是立即完成,你可以记录下来。到了时候再完成即可。
观察者模式:观察者和目标要相互知道
发布订阅模式:发布者和订阅者不用互相知道,通过第三方实现调度,属于经过解耦合的观察者模式
javascript
class SubPub {
constructor() {
this.message = {}//记录本
}
subscribe(type, fn) {
if (!this.message[type]) {
this.message[type] = [fn]
} else {
this.message[type].push(fn)
}
}
publish(type, ...arg) {
if (!this.message[type]) return
const event = {
type: type,
arg: arg || {}
}
// 循环执行为当前事件类型订阅的所有事件处理函数
this.message[type].forEach(item => {
item.call(this, event.arg)
})
}
}
与观者不同的是,订阅的是事件而非对象,而且有不同的记录本,记录不同的事
- 模块化模式
模块化模式最初被定义为在传统软件工程中为类提供私有和公共封装的一种方法。
能够使一个单独的对象拥有公共/私有的方法和变量,从而屏蔽来自全局作用域的特殊部分。这可以减少我们的函数名与在页面中其他脚本区域内定义的函数名冲突的可能性。
早前模块化是由闭包完成
javascript
var testModule = (function () {
var count = 0;
return {
increment () {
return ++count;
},
reset: function () {
count = 0;
},
decrement(){
return --count;
}
};
})();
模块化
javascript
export default {
name:"moduleA",
test(){
return "test"
}
}
<script type="module">
import moduleA from './1.js'
console.log(moduleA)
</script>
- 迭代器模式
为遍历不同数据结构的 "集合" 提供统一的接口;
能遍历访问 "集合" 数据中的项,不关心项的数据结构
javascript
// 统一遍历接口实现
var kerwinEach = function (arr, callBack) {
for (let i = 0; i < arr.length; i++) {
callBack(i, arr[i])
}
}
// 外部调用
kerwinEach([11, 22, 33, 44, 55], function (index, value) {
console.log([index, value]);
var oli =document.createElement("li")
oli.innerHTML = value
list.appendChild(oli)
})
- 指责链模式
Promise的then链式调用采用的就是这种模式
使多个对象都有机会处理请求,从而避免了请求的发送者与多个接收者直接的耦合关系,将这些接收者连接成一条链,顺着这条链传递该请求,直到找到能处理该请求的对象。
javascript
btn.addEventListener("click", function (event) {
checks.check()
});
function checkEmpty() {
if (input.value.length == 0) {
console.log("这里不能为空");
return
}
return "next"
}
function checkNumber() {
if (Number.isNaN(+input.value)) {
console.log("这里必须是数字");
return
}
return "next"
}
function checkLength() {
if (input.value.length < 6) {
console.log("这里要大于6个数字");
return
}
return "next"
}
class Chain {
constructor(fn) {
this.checkRule = fn || (() => "next");
this.nextRule = null;
}
addRule(nextRule) {
this.nextRule = new Chain(nextRule);
return this.nextRule;//不断返回实例,实现链式调用
}
end() {
this.nextRule = {
check: () => "end"
};
}
check() {
this.checkRule() == "next" ? this.nextRule.check() : null;
}
}
const checks = new Chain();
checks.addRule(checkEmpty).addRule(checkNumber).addRule(checkLength).end();
最后还有一种不算设计模式但是非常有用的模式,名为函数柯里化
试想一下你要打印一个日志,它的内容是时间,类型,以及内容
很明显时间在一天中日期是不会变的,类型也就那么几种,唯独内容每次都不一样
但是前面两个参数,只是不会频繁变化,但却不能完全写死。
这时需要一个容器,记住前面两个参数。大家可能会想到使用Object对象,但是不要忘了函数也是对象,合理利用函数闭包,完全可以优雅的解决这个棘手的问题。
javascript
function logs(x){
return function(y){
return function(z){
console.log(`时间:${x},类型:${y},内容:${z}`)
}
}
}
let logDate=logs(new Date().toLocaleDateString())//这个容器保存当天时间
let logDateError=logDate('error')//定义不同类型
let logDateSuccess=logDate('success')
let message1=logDateError('错误信息1')//填写完全不同的内容
let message1=logDateError('错误信息2')
let message2=logDateSuccess('成功信息1')
这样写logs函数大家可能担心陷入回调地狱,凡是关于回调地狱都可以使用递归解决!
自动封装函数:
javascript
function currying(fn) {
let curryFn = function (...args) {
if (args.length >= fn.length) {
return fn(...args)
} else {
return function (...newArgs) {
return curryFn(...args, ...newArgs)
}
}
}
return curryFn
}
function test(a, b, c) {
console.log(a, b, c)
}
var testCurrying = currying(test)//放入该函数即可自动生成
testCurrying(1)(2)(3)