JavaScript 常见10种设计模式

发布于:2025-07-13 ⋅ 阅读:(23) ⋅ 点赞:(0)

一. 设计模式介绍

  • 设计模式是我们在 解决问题的时候针对特定问题给出的简洁而优化的处理方案
  • 在 JS 设计模式中,最核心的思想:封装变化。
  • 将变与不变分离,确保变化的部分灵活、不变的部分稳定。
  • 本文介绍以下10种常见js设计模式
    • 构造器模式
    • 原型模式
    • 单例模式
    • 工厂模式
    • 建造者模式
    • 适配器模式
    • 观察者模式
    • 装饰者模式
    • 策略模式
    • 发布订阅模式

设计模式就是套路
没有一种设计模式可以解决所有问题。设计模式是针对特定问题出现的简洁优化的解决方案

二. 构造器模式

js特有。

 var employee1 = {
     name:"kerwin",
     age:100
 }
 var employee2 = {
     name:"tiechui",
     age:18
 }

以上写法,如果数据量变多,代码重复并且臃肿。
es6之前,通过构造器函数创建对象。

Employee里的this指向的是最终生成的实例employee1, employee2.

function Employee(name,age){
    this.name = name;
    this.age =age;
    this.say = function(){
        console.log(this.name+"-",this.age)
    }
}
var employee1 = new Employee("kerwin",100)
var employee2 = new Employee("tiechui",18)

弊端:每次new实例,say方法都会开辟内存创建此方法(构造器模式每次创建实例都会重复创建方法)。——但是不同对象的say方法是一样的。

三. 原型模式

3.1 原型模式

js 特有。
基于构造器模式改造, 使得代码复用性增加。
js特有,将方法放到函数的原型中。
函数的原型是唯一的,在内存中只有一份。

function Employee(name,age){
    this.name = name;
    this.age =age;

}
Employee.prototype.say = function(){
    console.log(this.name+"-",this.age)
}
new Employee("kerwin",100)
new Employee("tiechui",18)

3.2 补充:类语法兼顾构造器&原型模式

类语法是es6出现的。es5用构造函数创建对象。
构造器函数: constructor()
类是实例的抽象,实例是类的实现。
类中的方法是在构造器里还是挂载在原型上?后者 —— es6类语法兼顾构造器和原型模式。

class Employee {
	constructor(name, age) {
		this.name = name;
		this.age = age;
	}
	say() {
		console.log(this.name);
	}
}
var employee1 = new Employee("kerwin",100);
var employee2 = new Employee("tiechui",18);

3.3 案例

<!--
 * @作者: kerwin
-->

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        * {
            margin: 0;
            padding: 0;
        }

        ul {
            list-style: none;
        }

        .header {
            display: flex;
            width: 500px;
        }

        .header li {
            flex: 1;
            height: 50px;
            line-height: 50px;
            text-align: center;
            border: 1px solid black;
        }

        .box {
            position: relative;
            height: 200px;
        }

        .box li {
            position: absolute;
            left: 0;
            top: 0;
            width: 500px;
            height: 200px;
            background-color: yellow;
            display: none;
        }

        .header .active {
            background-color: red;
        }

        .box .active {
            display: block;
        }
    </style>
</head>

<body>
    <div class="container1">
        <ul class="header">
            <li class="active">1</li>
            <li>2</li>
            <li>3</li>
            <li>4</li>
            <li>5</li>
            <li>6</li>
        </ul>
        <ul class="box">
            <li class="active">111</li>
            <li>222</li>
            <li>333</li>
            <li>444</li>
            <li>555</li>
            <li>666</li>
        </ul>
    </div>

    <div class="container2">
        <ul class="header">
            <li class="active">1</li>
            <li>2</li>
            <li>3</li>
            <li>4</li>
            <li>5</li>
            <li>6</li>
        </ul>
        <ul class="box">
            <li class="active">111</li>
            <li>222</li>
            <li>333</li>
            <li>444</li>
            <li>555</li>
            <li>666</li>
        </ul>
    </div>

    <script>
        function Tabs(selector, type) {
            this.selector = document.querySelector(`${selector}`)
            this.type = type

            this.headers = this.selector.querySelectorAll(".header li ")
            this.boxs = this.selector.querySelectorAll(".box li ")
            // new完实例,调用自己的change方法,绑定事件
            this.change()
        }

        Tabs.prototype.change = function () {
            for (let i = 0; i < this.headers.length; i++) {
                this.headers[i].addEventListener(this.type, () => {

                    for (let m = 0; m < this.headers.length; m++) {
                        this.headers[m].classList.remove("active")
                        this.boxs[m].classList.remove("active")
                    }
                    this.headers[i].classList.add("active")
                    this.boxs[i].classList.add("active")

                }, false)
            }
        }

        new Tabs('.container1', "click")
        new Tabs('.container2', "mouseover")
    </script>
</body>

</html>

四. 工厂模式

由一个工厂对象决定创建某一种产品对象类的实例。主要用来创建同一类对象。

注意switch不是方法,是分支语句,方法是UserFactory,里面有一个User构造函数。

// es5写法
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('参数错误')
    }
}
var user1 = UserFactory('editor');

简单工厂的优点在于,你只需要一个正确的参数,就可以获取到你所需要的对象,而无需知道其创建的具体细节。
但是在函数内包含了所有对象的创建逻辑和判断逻辑的代码,每增加新的构造函数还需要修改判断逻辑代码
当我们的对象不是上面的3个而是10个或更多时,这个函数会成为一个庞大的超级函数,便得难以维护。
所以,简单工厂只能作用于创建的对象数量较少,对象的创建逻辑不复杂时使用

以上是es5的写法,下面是es6的写法

// es6类写法
class User {
    constructor(role, pages){
        this.role = role;
        this.pages = pages;
    }
    
    static UserFactory(role) {
	    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('参数错误')
	    }
	}
}

static方法又称为是类方法,不需要实例化、即可调用的方法。通过类名.方法名()即可调用,如User.UserFactory().

如果不加static,又想访问UserFactory(),则需要创建一个对象实例来调用。

因此此时调用UserFactory()可以通过var user1 = User.UserFactory('editor')实现。

五. 建造者模式

建造者模式(builder pattern)属于创建型模式的一种,提供一种创建复杂对象的方式。它将一个复杂的对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。

建造者模式是一步一步创建一个复杂的对象,它允许用户只通过指定复杂的对象的类型和内容就可以构建它们,用户不需要指定内部的具体构造细节。

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

建造者模式将一个复杂对象的构建层与其表示层相互分离,同样的构建过程可采用不同的表示。
工厂模式主要是为了创建对象实例或者类簇(抽象工厂),关心的是最终产出(创建)的是什么,而不关心创建的过程。
而建造者模式关心的是创建这个对象的整个过程,甚至于创建对象的每一个细节。

六. 单例模式

1、保证一个类仅有一个实例,并提供一个访问它的全局访问点
2、主要解决一个全局使用的类频繁地创建和销毁,占用内存

如果是全局变量实现,容易造成命名空间污染和变量覆盖问题。

6.1 es5 闭包实现单例模式

闭包:在函数内部,return 函数,被外界变量Singleton引用,导致函数里的变量无法被释放,如此构建出了闭包。

   var Singleton= (function () {
       return function () {
       }
   })()

es5闭包实现单例模式。

    var Singleton = (function (name, age) {
        let instance = null;

		function User(name, age) {
			this.name = name;
			this.age = age;
		}
		
        return function (name, age) {
            if (!instance) {
                instance = new User(name, age);
            }
            return instance;
        }
    })()

	Singleton('kerwin', 100);

注意:第一次调用Singleton方法,创建一个instance,再次调用Singleton方法,由于这是闭包,此时instance没有被回收,此时直接return 之前创建的instance.

Singleton(‘kerwin’, 100) === Singleton(‘kerwin’, 100); // true

6.2 es6写法

	class Singleton {
		constructor(name, age){
			if(Singleton.instance) {
				this.name = name;
				this.age = age;			
				Singleton.instance = this;	
			}
			return Singleton.instance;
		}
	}

	new Singleton('kerwin', 100);

new Singleton(‘kerwin’, 100) === new Singleton(‘kerwin’, 100); // true

每次new 类()获得的都是第一次实例化的对象。

6.3 示例-单一对话框

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
</head>
<style>
    .kerwin-modal{
        height: 200px;
        width: 200px;
        line-height: 200px;
        position: fixed;
        left: 50%;
        top: 50%;
        transform: translate(-50%, -50%);
        background-color: yellow;
        text-align: center;
    }
</style>

<body>
    <button id='open'>打开弹框</button>
    <button id='close'>关闭弹框</button>
</body>
<script>
    const Modal = (function () {
        let modal = null
        return function () {
            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'
    })
</script>

</html>

七. 装饰器模式

装饰器模式能够很好地对已有功能进行拓展,这样不会更改原有的代码,对其他的业务产生影响,这方便我们在较少的改动下对软件功能进行拓展。

将不核心的功能抽离出来。

Function是js的原生的function构造函数。

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

在test执行前,注入before前置和after后置函数。

然后有一个卖座的数据,点击埋码,不写了。p10

八. 适配器模式

将一个类的接口转换成客户希望的另一个接口。适配器模式让那些接口不兼容的类可以一起工作。

 //按照官网代码复制
class TencentMap {
    show() {
        console.log('开始渲染腾讯地图');
    }
}
//按照官网代码复制
class BaiduMap {
    display() {
        console.log('开始渲染百度地图');
    }
}
// 适配器1
class BaiduMapAdapter extends BaiduMap {
    constructor() {
        super();
    }
    render() {
        this.display();
    }
}
// 适配器2
class TencentMapAdapter extends TencentMap {
    constructor() {
        super();
    }
    render() {
        this.show();
    }
}
// 外部调用者
function renderMap(map) {
    map.render(); // 统一接口调用
}
renderMap(new TencentMapAdapter());
renderMap(new BaiduMapAdapter());

适配器不会去改变实现层,那不属于它的职责范围,它干涉了抽象的过程。外部接口的适配能够让同一个方法适用于多种系统

九. 策略模式

策略模式定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用算法的客户。策略模式属于对象行为模式,它通过对算法进行封装,把使用算法的责任和算法的实现分割开来,并委派给不同的对象对这些算法进行管理。
该模式主要解决在有多种算法相似的情况下,使用 if...else 所带来的复杂和难以维护。它的优点是算法可以自由切换,同时可以避免多重if...else判断,且具有良好的扩展性。

let strategy = {
	"A": (salary )=>{
		return salary * 4;
	},
	"B": (salary )=>{
		return salary * 3;
	},
	"C": (salary )=>{
		return salary * 2;
	},
}

function calBonus(level, salary) {
	return strategy[level](salary);
}

calBonus("A", 10000);
calBonus("B", 5000);

应用场景

  • 适用于开发node路由匹配策略
  • 适用于后台返回数组,前端进行映射显示到页面上,示例如下。
<!--
 * @作者: kerwin
-->
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>

    <style>
        li {
            display: flex;
            justify-content: space-between;
        }

        .reditem {
            background-color: red;
        }

        .yellowitem {
            background-color: yellow;
        }

        .greenitem {
            background-color: green;
        }
    </style>
</head>

<body>
    <ul id="mylist">

    </ul>
    <script>
        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("")
    </script>
</body>

</html>
  1. 可以有效地避免多重条件选择语句
  2. 代码复用性高,避免了很多粘贴复制的操作。
  3. 策略模式提供了对开放封闭原则的支持,将算法独立封装在strategies中,使得它们易于切换,易于扩展。

十. 观察者模式

观察者模式包含观察目标Subject和观察者Observer两类对象,
一个目标可以有任意数目的与之相依赖的观察者
一旦观察目标的状态发生改变,所有的观察者都将得到通知。

一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新,解决了主体对象与观察者之间功能的耦合,即一个对象状态改变给其他对象通知的问题。

// 基本写法
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)

优势:目标者与观察者,功能耦合度降低,专注自身功能逻辑;观察者被动接收更新,时间上解耦,实时接收目标者更新状态。

缺点:观察者模式虽然实现了对象间依赖关系的低耦合,但却不能对事件通知进行细分管控,如 “筛选通知”,“指定主题事件通知” 。

应用场景:后台系统的通信功能。示例p14,不看了。

十一. 发布订阅模式

1.观察者Observer和目标Subject要相互知道
2.发布者和订阅者不用互相知道,通过第三方实现调度,属于经过解耦合的观察者模式

const PubSub = {
	list: [],
	publish() {
	this.list.forEach(item => item());
	},
	subscribe(cb) {
		this.list.push(cb);
	}
}
// 订阅者1
function testA() {
	console.log("testA");
}
// 订阅者2
function testB() {
	console.log("testB");
}
// 订阅
PubSub.subscribe(testA);
PubSub.subscribe(testB);
// 发布
PubSub.publish();  // 俩回调函数被执行

希望订阅的事件可以细分。
案例改造

const SubPub = {
     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)
         })
     },
     unsubscribe(type,fn){
         if (!this.message[type]) return

         if(!fn){
             this.message[type] && (this.message[type].length = 0)
         }else{
             this.message[type] = this.message[type].filter(item=>item!==fn)
         }
     }
 }
 // 订阅者1
function testA() {
	console.log("testA");
}
// 订阅者2
function testB() {
	console.log("testB");
}
SubPub.subscribe("A", testA);
SubPub.subscribe("B", testB);

网站公告

今日签到

点亮在社区的每一天
去签到