JavaScript 设计模式核心原理与应用实践

172 阅读14分钟

引言

在 JavaScript 编程的广袤天地中,项目规模持续扩张,复杂度也与日俱增。构建高效、可维护且灵活的代码结构,已然成为开发者们必须直面的关键挑战。设计模式作为历经沉淀与验证的通用解决方案,宛如开启成功编程之门的钥匙,为我们应对挑战提供了强有力的支持。运用设计模式,能够把复杂问题拆解成一个个易于管理的模块,显著提升代码的可读性、可扩展性以及复用性。本文将深入探究 JavaScript 设计模式的核心原理,并通过丰富详实的应用实践案例,助力读者熟练掌握这些强大的编程技巧。

设计模式基础概念

什么是设计模式

设计模式可简单理解为,在软件开发进程里,针对反复涌现的问题所归纳总结出的通用解决策略。它恰似建筑领域中形形色色的建筑设计方案,不同的建筑风格与功能需求,对应着各异的设计方案;而设计模式则是适配不同软件需求场景的 “代码构建蓝图”。这些方案并非具体的代码实现,而是一种抽象的、极具复用价值的设计理念。

设计模式的重要性

在小型项目中,代码量有限,结构相对简单,或许不借助设计模式也能顺利完成开发。然而,一旦项目规模逐步扩大,团队成员不断增多,缺乏设计模式的代码便会变得晦涩难懂,维护难度直线上升。比如,代码中可能充斥着大量重复逻辑,不同模块间耦合度极高,牵一发而动全身,修改一个功能往往会引发一系列意想不到的问题。而设计模式能够有效规避这些状况,它通过规范代码结构,赋予代码更好的组织性与层次性,让开发者之间的沟通与协作更加顺畅。同时,设计模式极大地提高了代码的复用性,减少了重复代码的编写,大幅提升了开发效率。

常见设计模式分类

设计模式通常可划分为创建型、结构型和行为型三大类别。

  1. 创建型模式:主要聚焦于对象的创建流程,帮助我们更为灵活地创建对象,精细把控对象的创建细节。以单例模式为例,它能确保一个类仅有一个实例,并提供全局访问点。在 JavaScript 中,浏览器的全局对象window便是单例模式的典型代表,无论在页面的哪个部分访问window,获取的都是同一个对象实例。
  1. 结构型模式:重点关注如何将类或对象组合成更为庞大、复杂的结构,以实现更丰富的功能。像代理模式,它为其他对象提供一种代理,用于控制对该对象的访问。在 JavaScript 中,当我们需要加载一个较大的图片时,可以先借助代理对象显示一个占位符,待图片真正加载完成后再替换为真实图片,以此优化用户体验,避免页面因长时间等待图片加载而出现卡顿。
  1. 行为型模式:主要用于处理对象之间的交互和职责分配,促使对象之间的协作更加清晰、高效。例如观察者模式,它定义了对象之间的一对多依赖关系,当一个对象状态发生改变时,所有依赖它的对象都会收到通知并自动更新。在 JavaScript 的事件监听机制中,就广泛运用了观察者模式,比如我们给一个按钮添加点击事件监听器,当按钮被点击(状态改变)时,与之关联的事件处理函数(观察者)就会被触发执行。

JavaScript 设计模式核心原理详解

单例模式

  1. 原理:单例模式的核心要义在于确保一个类仅有一个实例存在。在 JavaScript 中,实现单例模式的方式多种多样。一种常见做法是借助立即执行函数(IIFE)和闭包来创建单例对象。如下所示:
const Singleton = (function () { 
 let instance;   
 function createInstance() { 
    // 这里可放置任何初始化代码,比如创建一个复杂对象    
    const object = {       
       data: 'This is data in the singleton'    
    };       
    return object;   
 }   
 
return {      
  getInstance: function () {   
      if (!instance) {  
          instance = createInstance();    
      }          
      return instance;    
    }  
  };
})();

在上述示例中,Singleton是通过立即执行函数返回的对象,它包含一个getInstance方法。首次调用getInstance时,会创建一个实例并存储在instance变量中,后续再调用getInstance时,直接返回已创建的实例,从而切实保证了单例性。

2. 应用场景:单例模式在众多场景中都有广泛应用。例如在 JavaScript 的模块开发中,如果一个模块仅需一个全局唯一的实例来管理共享状态或资源,就可采用单例模式。像浏览器中的localStorage,本质上就是一个单例,无论在页面的哪个脚本中访问localStorage,操作的都是同一个存储对象,方便在不同页面元素或脚本之间共享数据。

工厂模式

  1. 原理:工厂模式是一种创建对象的设计模式,它将对象的创建与使用分离开来。通过一个工厂函数负责对象的创建工作,而非在客户端代码中直接实例化对象。例如,若要创建不同类型的图形对象(圆形、矩形等),可利用工厂模式简化对象创建流程。
function ShapeFactory(type) { 
    if (type === 'circle') {      
        return {            
            draw: function () {           
                console.log('Drawing a circle');    
            }      
        };   
    } else if (type ==='rectangle') {  
        return {          
            draw: function () {   
                console.log('Drawing a rectangle');  
            }     
        };   
    }
}

在此例中,ShapeFactory就是工厂函数,它依据传入的参数type创建不同类型的图形对象。客户端代码只需调用ShapeFactory并传入相应类型,即可获取对应的图形对象,无需了解对象创建的具体细节。

2. 应用场景:在开发大型游戏项目时,往往需要创建各种不同类型的游戏角色,如战士、法师、刺客等。每个角色都有独特的属性和行为方法。运用工厂模式,可将角色的创建逻辑封装在一个工厂函数中,游戏开发者只需调用工厂函数并传入角色类型,就能轻松创建出对应的游戏角色,避免在游戏的各个部分重复编写复杂的角色创建代码,有效提高了代码的可维护性和可扩展性。

观察者模式

  1. 原理:观察者模式定义了一种一对多的依赖关系,多个观察者对象可同时监听某一个主题对象。当主题对象的状态发生变化时,会通知所有依赖它的观察者对象,使其能够自动更新自身状态。在 JavaScript 中,可通过对象的属性和方法来实现观察者模式。
function Subject() {  
    this.observers = [];    
    this.subscribe = function (observer) {   
        this.observers.push(observer);  
    };   
    this.unsubscribe = function (observer) {    
        this.observers = this.observers.filter((obs) => obs!== observer); 
    };   
    this.notify = function () {     
        this.observers.forEach((observer) => observer.update());};
    }
}

function Observer() {   
    this.update = function () {  
        console.log('Observer has been updated');   
    };
}

在上述代码中,Subject类代表主题对象,它维护着一个观察者数组observers,并提供了subscribe方法用于添加观察者,unsubscribe方法用于移除观察者,notify方法用于通知所有观察者更新。Observer类代表观察者对象,它拥有一个update方法,当接收到主题对象的通知时会执行相应的更新操作。

2. 应用场景:在实时聊天应用中,每个聊天窗口可看作一个观察者,而聊天服务器则是主题对象。当有新消息抵达服务器时,服务器作为主题对象会通知所有正在监听的聊天窗口(观察者),聊天窗口接收到通知后会自动更新显示新消息,实现了实时通信的效果。这种方式降低了聊天窗口和服务器之间的耦合度,便于对系统进行扩展和维护。

策略模式

  1. 原理:策略模式定义了一系列算法,将每个算法封装起来,使它们可相互替换,算法的变化独立于使用算法的客户。在 JavaScript 中,可通过对象字面量和函数来实现策略模式。例如,在计算订单总价的场景中,不同会员等级有不同的折扣策略。
const discountStrategies = {   
    normal: function (price) {    
        return price;   
    },    
    silver: function (price) {    
        return price * 0.95;  
    },   
    gold: function (price) {     
        return price * 0.9;   
    }
};

function calculateTotal(price, strategy) {  
    return discountStrategies[strategy](price);
}

在此例中,discountStrategies对象包含了不同的折扣策略函数,calculateTotal函数依据传入的策略名称调用相应的折扣函数来计算总价。

2. 应用场景:在电商系统中,订单的支付方式丰富多样,如支付宝、微信支付、银行卡支付等。每种支付方式的处理逻辑各不相同,运用策略模式可将每种支付方式的逻辑封装成独立的函数。在需要进行支付时,根据用户选择的支付方式调用相应的支付策略函数,这样能方便地添加或修改支付方式,而不会对其他业务逻辑造成影响。

装饰者模式

  1. 原理:装饰者模式能够动态地给一个对象添加额外的职责。相较于生成子类,在增加功能方面,装饰者模式更为灵活。在 JavaScript 中,可通过函数来模拟装饰者模式。例如,给一个已有的函数添加日志记录功能。
function originalFunction() {   
    console.log('Original function is called');
}

function logDecorator(func) { 
    return function () {   
        console.log('Before function call');  
        func.apply(this, arguments);    
        console.log('After function call');  
    };
}

const decoratedFunction = logDecorator(originalFunction);

decoratedFunction();

在这个例子中,logDecorator函数接收一个原始函数func作为参数,返回一个新函数。新函数在执行原始函数前后添加了日志记录功能,实现了对原始函数的装饰。

2. 应用场景:在 Web 应用中,若有一个显示图片的函数。倘若想要在显示图片前添加图片加载动画,在图片显示后添加点击事件处理功能,便可运用装饰者模式。通过创建不同的装饰函数,分别为原始的显示图片函数添加加载动画和点击事件处理功能,无需修改原始函数的代码,遵循了单一职责原则。

代理模式

  1. 原理:代理模式为其他对象提供一种代理,用于控制对该对象的访问。代理对象可在访问目标对象之前进行一些预处理,或者在访问之后进行一些后处理。在 JavaScript 中,代理模式可通过多种方式实现,比如利用对象的访问器属性(getter 和 setter)。例如,有一个需要从远程服务器获取数据的对象,为提升性能,可使用代理对象来缓存数据。
const realData = { 
    getData: function () {   
        // 模拟从远程服务器获取数据     
        console.log('Fetching data from server'); 
        return 'Some data from server';   
    }
};

const dataProxy = {  
    cache: null,  
    getData: function () {    
        if (!this.cache) { 
            this.cache = realData.getData();   
    }    
    return this.cache;  
    }
};

在此例中,dataProxy是realData的代理对象。当调用dataProxy.getData()时,如果缓存中没有数据,就会调用realData.getData()从服务器获取数据并缓存,下次再调用时直接返回缓存中的数据,减少了对服务器的请求次数。

2. 应用场景:除了上述缓存数据的场景,在访问一些敏感资源时,也可运用代理模式进行权限控制。例如,在企业内部系统中,某些功能模块仅特定角色的用户才能访问。可创建一个代理对象,在访问这些功能模块之前,检查当前用户的角色权限,若权限不足则阻止访问,从而保护敏感资源。

适配器模式

  1. 原理:适配器模式将一个类的接口转换成客户期望的另一个接口,使原本因接口不兼容而无法协同工作的类能够携手合作。在 JavaScript 中,可通过对象组合的方式实现适配器模式。例如,有一个旧的函数oldFunction,它接受两个参数并返回它们的和,现在需要一个新的函数newFunction,它接受一个数组参数并返回数组元素的和,可利用适配器模式来适配oldFunction。
function oldFunction(a, b) {  
    return a + b;
}

function adapterFunction(arr) { 
    return oldFunction.apply(null, arr);
}

在此例中,adapterFunction充当适配器,将接受数组参数的需求适配到oldFunction接受两个参数的接口上。

2. 应用场景:在集成第三方库时,常常会遭遇接口不兼容的问题。例如,一个第三方地图库提供的获取位置信息的函数返回的是一种特定格式的对象,而我们的应用程序需要的是另一种格式的位置信息。此时,可运用适配器模式,创建一个适配器函数,将第三方库返回的位置信息格式转换为我们应用程序所需的格式,实现与第三方库的无缝集成。

迭代器模式

  1. 原理:迭代器模式提供一种按顺序访问聚合对象中各个元素的方法,且无需暴露该对象的内部表示。在 JavaScript 中,数组和字符串等内置对象都有默认的迭代器,我们也能自定义迭代器。例如,创建一个迭代器来遍历一个对象的属性。
const myObject = {  
    a: 1,   
    b: 2,  
    c: 3
};

function* objectIterator(obj) {  
    for (let key in obj) {   
        if (obj.hasOwnProperty(key)) {   
            yield { key, value: obj[key] };   
        }   
    }
}

const iterator = objectIterator(myObject);


for (let item of iterator) {  
    console.log(item.key, item.value);
    }

在此例中,objectIterator是一个生成器函数,它返回一个迭代器对象。通过for...of循环可按顺序遍历myObject的属性。

2. 应用场景:在处理复杂的数据结构时,迭代器模式尤为实用。比如在一个树状结构的数据中,需要遍历树的所有节点。使用迭代器模式可将遍历逻辑封装在迭代器中,使外部代码能够便捷地按顺序访问树的节点,而无需了解树的内部结构实现细节。

命令模式

  1. 原理:命令模式将一个请求封装为一个对象,如此一来,我们可用不同的请求对客户进行参数化,还能对请求排队、记录请求日志,以及支持可撤销的操作。在 JavaScript 中,可通过对象和函数来实现命令模式。例如,在一个简单的图形绘制系统中,有绘制圆形和矩形的操作,可将这些操作封装成命令对象。
const canvas = document.createElement('canvas');
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');

function CircleCommand(x, y, radius) {   
    this.x = x;  
    this.y = y;  
    this.radius = radius;  
    this.execute = function () {  
        ctx.beginPath();   
        ctx.arc(this.x, this.y, this.radius, 0, 2 * Math.PI);    
        ctx.fillStyle = 'blue';  
        ctx.fill();   
    };
}

function RectangleCommand(x, y, width, height) {  
    this.x = x;   
    this.y = y;  
    this.width = width; 
    this.height = height;  
    this.execute = function () {    
        ctx.fillStyle ='red';   
        ctx.fillRect(this.x, this.y, this.width, this.height);
    };
}
const commands = [];commands.push(new CircleCommand(50, 50, 20));

commands.push(new RectangleCommand(100, 100, 50, 30));

commands.forEach((command) => command.execute());

在此例中,CircleCommand和RectangleCommand均为命令对象,它们都拥有一个execute方法来执行相应的绘制操作。commands数组存储了一系列命令对象,通过遍历数组并调用每个命令对象的execute方法来执行绘制操作。

2. 应用场景:在文本编辑器中,用户的撤销、重做、复制、粘贴等操作均可运用命令模式来实现。将每个操作封装成一个命令对象,这些命令对象可存储在一个历史记录栈中。当用户执行撤销操作时,从栈中取出上一个命令对象并调用其撤销方法;当用户执行重做操作时,从栈中取出下一个命令对象并调用其执行方法,通过这种方式实现了操作的可撤销和可重做功能。