JavaScript 的 7 种设计模式
myzbx 2024-12-08 16:39 33 浏览
原文地址:Understanding Design Patterns in JavaScript
原文作者:Sukhjinder Arora
译者:HelloGitHub-Robert
当启动一个新的项目时候,我们不应该马上开始编程。而是首先应该定义项目的目的和范围,然后列出其功能或规格。如果你已经开始编程或者正在从事一个复杂的项目,则应该选择一个最适合你项目的设计模式。
什么是设计模式?
在软件工程中,设计模式是针对软件设计中常见问题的可重用解决方案。设计模式也是经验丰富的开发人员针对特定问题的最佳实践。它可以被当作编程的模板。
为什么要使用设计模式?
许多工程师要么认为设计模式浪费时间,要么不知道如何恰当的使用设计模式。但如果能正确使用设计模式,则可以帮助你写出更好的可读性更高的代码,并且代码更容易被维护和理解。
最重要的是,设计模式为软件开发人员提供了通用的词汇表。它们能让学习你代码的人很快了解代码的意图。例如,如果你的项目中使用了装饰器模式,那么新的开发可以很快就知道这段代码的作用,从而他们可以将更多精力放在解决业务问题上,而不是试图理解代码在做什么。
我们已经知道了什么是设计模式和它的重要性,下面我们深入研究一下 JavaScript 中的 7 种设计模式。
一、模块模式
模块是一段独立的代码,因此我们可以更新模块而不会影响代码的其它部分。模块还允许我们通过为变量创建单独的作用域来避免命名空间污染。当它们与其它代码解耦时,我们还可以在其它项目中重用模块。
模块是任何现代 JavaScript 应用程序不可或缺的一部分,有助于保持代码干净,独立和有条理。在 JavaScript 中有许多方法可以创建模块,其中一种是模块模式。
与其它编程语言不同,JavaScript 没有访问修饰符,也就是说,你不能将变量声明为私有的或公共的。因此,模块模式也可用来模拟封装的概念。
模块模式使用 IIFE(立即调用的函数表达式),闭包和函数作用域来模拟封装的概念。例如:
const myModule = (function() {
const privateVariable = 'Hello World';
function privateMethod() {
console.log(privateVariable);
}
return {
publicMethod: function() {
privateMethod();
}
}
})();
myModule.publicMethod();
由于是 IIFE 因此代码会被立即执行,并将返回对象赋值给了 myModule 变量。由于闭包,即使在 IIFE 完成后,返回的对象仍可以访问 IIFE 内部定义的函数和变量。
因此,IIFE 内部定义的变量和函数对外部是看不见的,从而使其成为 myModule 模块的私有成员。
执行代码后,myModule 变量看起来像下面所示:
const myModule = {
publicMethod: function() {
privateMethod();
}};
因此当我们调用 publicMethod() 时候,它将调用 privateMethod() 例如:
// Prints 'Hello World'
module.publicMethod();
二、揭示模块模式
揭示模块模式是 Christian Heilmann 对模块模式的略微改进。模块模式的问题在于,我们必须创建新的公共函数才能调用私有函数和变量。
在这种模式下,我们将返回的对象的属性映射到要公开暴露的私有函数上。这就是为什么将其称为揭示模块模式。例如:
const myRevealingModule = (function() {
let privateVar = 'Peter';
const publicVar = 'Hello World';
function privateFunction() {
console.log('Name: '+ privateVar);
}
function publicSetName(name) {
privateVar = name;
}
function publicGetName() {
privateFunction();
}
/** reveal methods and variables by assigning them to object properties */
return {
setName: publicSetName,
greeting: publicVar,
getName: publicGetName
};
})();
myRevealingModule.setName('Mark');
// prints Name: Mark
myRevealingModule.getName();
这种模式让我们更容易知道哪些函数和变量是公共的,无形中提高了代码的可读性。执行代码后 myRevealingModule 看起来像下所示:
const myRevealingModule = {
setName: publicSetName,
greeting: publicVar,
getName: publicGetName
};
当我们调用 myRevealingModule.setName('Mark') 时,实际调用了内部的 publicSetName。当调用 myRevealingModule.getName() 时,实际调用了内部的 publicGetName 例如:
myRevealingModule.setName('Mark');
// prints Name: Mark
myRevealingModule.getName();
与模块模式相比,揭示模块模式的优势有:
- 通过修改 return 语句中的一行,我们可以将成员从公共变为为私人,反之亦然。
- 返回的对象不包含任何函数定义,所有右侧表达式都在 IIFE 中定义,从而使代码清晰易读。
三、ES6 模块
在 ES6 之前,JavaScript 没有内置模块,因此开发人员必须依靠第三方库或模块模式来实现模块。但是自从 ES6,JavaScript 内置了模块。
ES6 的模块是以文件形式存储的。每个文件只能有一个模块。默认情况下,模块内的所有内容都是私有的。通过使用 export 关键字来暴露函数、变量和类。模块内的代码始终在严格模式下运行。
3.1 导出模块
有两种方法可以导出函数和变量声明:
- 在函数和变量声明的前面添加 export 关键字。例如:
// utils.js
export const greeting = 'Hello World';
export function sum(num1, num2) {
console.log('Sum:', num1, num2);
return num1 + num2;
}
export function subtract(num1, num2) {
console.log('Subtract:', num1, num2);
return num1 - num2;
}
// This is a private function
function privateLog() {
console.log('Private Function');
}
- 在代码的最后添加 export 关键字来暴露函数和变量。例如:
// utils.js
function multiply(num1, num2) {
console.log('Multiply:', num1, num2);
return num1 * num2;
}
function divide(num1, num2) {
console.log('Divide:', num1, num2);
return num1 / num2;
}
// This is a private function
function privateLog() {
console.log('Private Function');
}
export {multiply, divide};
3.2 导入模块
与导出模块相似,有两种使用 import 关键字导入模块的方法。例如:
- 一次导入多个项目
// main.js
// importing multiple items
import { sum, multiply } from './utils.js';
console.log(sum(3, 7));
console.log(multiply(3, 7));
- 导入所有模块
// main.js
// importing all of module
import * as utils from './utils.js';
console.log(utils.sum(3, 7));
console.log(utils.multiply(3, 7));
3.3 导入导出中使用别名
- 重命名导出
// utils.js
function sum(num1, num2) {
console.log('Sum:', num1, num2);
return num1 + num2;
}
function multiply(num1, num2) {
console.log('Multiply:', num1, num2);
return num1 * num2;
}
export {sum as add, multiply};
- 重命名导入
// main.js
import { add, multiply as mult } from './utils.js';
console.log(add(3, 7));
console.log(mult(3, 7));
四、单例模式
一个单例对象是只能实例化一次的对象。如果不存在,则单例模式将创建类的新实例。如果存在实例,则仅返回对该对象的引用。重复调用构造函数将始终获取同一对象。
JavaScript 是一直内置单例的语言。我们只是不称它们为单例,我们称它们为对象字面量。例如:
const user = {
name: 'Peter',
age: 25,
job: 'Teacher',
greet: function() {
console.log('Hello!');
}
};
因为 JavaScript 中的每个对象都占用一个唯一的内存位置,并且当我们调用该 user 对象时,实际上是在返回该对象的引用。
如果我们尝试将 user 变量复制到另一个变量并修改该变量。例如:
const user1 = user;
user1.name = 'Mark';
我们将看到两个对象都被修改,因为 JavaScript 中的对象是通过引用而不是通过值传递的。因此,内存中只有一个对象。例如:
// prints 'Mark'
console.log(user.name);
// prints 'Mark'
console.log(user1.name);
// prints true
console.log(user === user1);
可以使用构造函数来实现单例模式。例如:
let instance = null;
function User() {
if(instance) {
return instance;
}
instance = this;
this.name = 'Peter';
this.age = 25;
return instance;
}
const user1 = new User();
const user2 = new User();
// prints true
console.log(user1 === user2);
调用此构造函数时,它将检查 instance 对象是否存在。如果对象不存在,则将 this 变量分配给 instance 变量。如果该对象存在,则只返回该对象。
单例也可以使用模块模式来实现。例如:
const singleton = (function() {
let instance;
function init() {
return {
name: 'Peter',
age: 24,
};
}
return {
getInstance: function() {
if(!instance) {
instance = init();
}
return instance;
}
}
})();
const instanceA = singleton.getInstance();
const instanceB = singleton.getInstance();
// prints true
console.log(instanceA === instanceB);
在上面的代码中,我们通过调用 singleton.getInstance 方法来创建一个新实例。如果实例已经存在,则此方法仅返回该实例。如果该实例不存在,则通过调用该 init() 函数创建一个新实例。
五、工厂模式
工厂模式使用工厂方法创建对象而不需要指定具体的类或构造函数的模式。
工厂模式用于创建对象而不需要暴露实例化的逻辑。当我们需要根据特定条件生成不同的对象时,可以使用此模式。例如:
class Car{
constructor(options) {
this.doors = options.doors || 4;
this.state = options.state || 'brand new';
this.color = options.color || 'white';
}
}
class Truck {
constructor(options) {
this.doors = options.doors || 4;
this.state = options.state || 'used';
this.color = options.color || 'black';
}
}
class VehicleFactory {
createVehicle(options) {
if(options.vehicleType === 'car') {
return new Car(options);
} else if(options.vehicleType === 'truck') {
return new Truck(options);
}
}
}
这里,创建了一个 Car 和一个 Truck 类(具有一些默认值),该类用于创建新的 car 和 truck对象。而且定义了一个VehicleFactory 类,用来根据 options 对象中的 vehicleType 属性来创建和返回新的对象。
const factory = new VehicleFactory();
const car = factory.createVehicle({
vehicleType: 'car',
doors: 4,
color: 'silver',
state: 'Brand New'
});
const truck= factory.createVehicle({
vehicleType: 'truck',
doors: 2,
color: 'white',
state: 'used'
});
// Prints Car {doors: 4, state: "Brand New", color: "silver"}
console.log(car);
// Prints Truck {doors: 2, state: "used", color: "white"}
console.log(truck);
我为类 VehicleFactory 创建了一个新的 factory 对象。然后,我们通过调用 factory.createVehicle 方法并且传递 options 对象,其 vehicleType 属性可能为 car 或者 truck 来创建新 Car 或 Truck 对象。
六、装饰器模式
装饰器模式用于扩展对象的功能,而无需修改现有的类或构造函数。此模式可用于将特征添加到对象中,而无需修改底层的代码。
此模式的一个简单示例为:
function Car(name) {
this.name = name;
// Default values
this.color = 'White';
}
// Creating a new Object to decorate
const tesla= new Car('Tesla Model 3');
// Decorating the object with new functionality
tesla.setColor = function(color) {
this.color = color;
}
tesla.setPrice = function(price) {
this.price = price;
}
tesla.setColor('black');
tesla.setPrice(49000);
// prints black
console.log(tesla.color);
这种模式的一个更实际的例子是:
假设汽车的成本取决于其功能的数量。如果没有装饰器模式,我们将不得不为不同的功能组合创建不同的类,每个类都有一个 cost 方法来计算成本。例如:
class Car() {
}
class CarWithAC() {
}
class CarWithAutoTransmission {
}
class CarWithPowerLocks {
}
class CarWithACandPowerLocks {
}
但是,通过装饰器模式,我们可以创建一个基类 car 并且通过装饰器函数给不同的对象添加对应的成本逻辑。
class Car {
constructor() {
// Default Cost
this.cost = function() {
return 20000;
}
}
}
// Decorator function
function carWithAC(car) {
car.hasAC = true;
const prevCost = car.cost();
car.cost = function() {
return prevCost + 500;
}
}
// Decorator function
function carWithAutoTransmission(car) {
car.hasAutoTransmission = true;
const prevCost = car.cost();
car.cost = function() {
return prevCost + 2000;
}
}
// Decorator function
function carWithPowerLocks(car) {
car.hasPowerLocks = true;
const prevCost = car.cost();
car.cost = function() {
return prevCost + 500;
}
}
首先,我们创建了小轿车的基类 Car。然后针对要添加的特性创建了装饰器并且此装饰器以 Car 对象为参数。然后通过返回更新后的小汽车成本来覆盖对象的成本函数,且添加了一个用来标识某个特性是否已经被添加的属性。
要添加新的功能,我们只需要像下面一样就可以:
const car = new Car();
console.log(car.cost());
carWithAC(car);
carWithAutoTransmission(car);
carWithPowerLocks(car);
最后,我们可以像这样计算汽车的成本:
// Calculating total cost of the car
console.log(car.cost());
结论
我们已经了解了 JavaScript 中使用的各种设计模式,但是这里没有涉及到可以用 JavaScript 实现的设计模式。
尽管了解各种设计模式很重要,但不要过度使用它们也同样重要。在使用设计模式之前,你应该仔细考虑你的问题是否适合该设计模式。要知道某个模式是否适合你的问题,应该好好研究该设计模式以及它的应用。
最后,欢迎优秀的你加入 HelloGitHub 的「译文亦舞」系列,让你的才华舞动起来!把优秀的文章分享给更多的人。要求:
- 平时浏览 GitHub、开源、编程、程序员等英文资讯和文章
- 想把自己阅读到优秀的英文文章分享给更多的人
- 翻译准确但不是直翻或机翻
- 保证每月至少翻译或校正 1 篇高质量文章
- 了解 Markdown 和排版规则
- 联系我
关注 HelloGitHub 头条号第一时间收到推送
- 上一篇:一文带你解读?JavaScript的基本用法
- 下一篇:JavaScript 版本
相关推荐
- 资深架构师亲授,从堆栈到GC,一篇文章打通任督二脉!
-
“又双叒OOM了?”“服务半夜崩了,日志全是`java.lang.OutOfMemoryError`...”“GC停顿太长,用户投诉卡顿!”如果你也常被这些问题折磨,根本症结往往在于:你对Java...
- Java JAR 启动内存参数配置指南:从基础设置到性能优化
-
在启动Java可执行JAR文件时,合理配置JVM内存参数是保障应用稳定性和性能的关键。本文将系统讲解如何通过命令行参数、环境变量等方式指定内存配置,并结合实际场景提供优化建议。一、核心内存...
- 浏览器存储"四大家族":谁才是你的数据管家?
-
当你关闭浏览器再重新打开,登录状态为何还在?购物车商品为何不会消失?这些"记忆"背后,藏着浏览器存储的"四大家族"——Cookie、localStorage、sessi...
- SOP与SIP深度解析(sop与soic)
-
SOP(标准作业程序)与SIP(标准检验程序)是确保产品质量和生产效率的两大支柱,分别聚焦于生产执行和质量验证。一、核心区别:目标与作用域维度SOP(标准作业程序)SIP(标准检验程序)定位指导“如何...
- Java 技术岗面试全景备战!从基础到架构的系统性通关攻略分享
-
Java技术岗的面试往往是一项多维度的能力检验。本文将会从核心知识点、项目经验到面试策略,为你梳理一份系统性的备战攻略!需要的同学可以私信小编【学习】一、技术基础:面试的“硬性指标”1.最重要的还是...
- C++11 新特性(c++11新特性 pdf)
-
一、核心语言革新移动语义与右值引用通过&&标识临时对象(右值),实现资源转移而非复制。例如移动构造函数将原对象资源指针转移后置空,避免深拷贝,极大优化容器操作性能。12类型推导auto:自动推导变量类...
- 2026年前每个开发者都应该学习的技能
-
优秀开发者和伟大开发者之间的差距正在快速扩大。随着AI工具的爆炸式增长、自动化工作流程和日益复杂的技术栈,开发者不能再仅仅"知道如何编码"了。在2026年及以后,您的优势不仅仅是编写代...
- 看一看,Python这四种作用域你都知道吗?
-
点赞、收藏、加关注,下次找我不迷路一、啥是作用域?先打个比方比如说,你在自己的卧室(相当于一个小空间)里放了一本书,这本书在卧室里随便你怎么看,这就是这本书在卧室这个"作用域"内...
- 抛弃立即执行函数 (IIFE),拥抱现代 JavaScript 块级作用域
-
IIFE(ImmediatelyInvokedFunctionExpression)曾是JavaScript开发中的重要工具,但随着ES6+的块级作用域特性,我们现在有了更优雅的替代...
- 2025 年是时候重新认识 Symbol 的八大特性了?
-
大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发!1.什么是Symbol原始类型在J...
- 函数、表达式与控制流:Rust 的核心语法构建块
-
在上一篇中我们了解了变量与类型,本篇将深入函数、表达式与控制流的使用,让你的代码更具逻辑性。一、函数定义与调用函数是组织和复用代码的基本单元。在Rust中,使用fn关键字定义函数:///计算...
- 所有权、借用与生命周期:理解 Rust 的核心机制
-
上一篇我们学习了函数、表达式和控制流,这一篇将正式进入Rust最核心、最独特的语言机制:所有权系统。一、为什么需要所有权机制?在C/C++中,内存管理依赖开发者手动操作,容易出现野指针、重复...
- Rust 语言的借用规则:构筑安全内存管理体系的核心保障机制
-
前言在系统级编程范畴内,内存安全始终是一项极具挑战性的关键议题。Rust语言凭借其独树一帜的「借用规则」(BorrowingRules),于编译阶段便有效规避了诸如空指针、野指针以及数据竞争等一系...
- 函数编写指南:参数、返回值与作用域实战详解
-
你是否在编写函数时遇到过参数传递混乱、返回值逻辑不清晰,或者变量作用域导致的奇怪bug?别担心,这篇文章将用最通俗的语言和实战案例,带你彻底搞懂函数的核心三要素:参数、返回值与作用域。一、参数:灵活...
- 服务器频繁报错?5 步教你快速排查修复!运维必看!
-
服务器突然报错、网站打不开、数据库连不上……这些问题是不是让你头大?别慌!今天教你一套「望闻问切」的排查法,90%的服务器故障都能轻松解决!一、定位错误类型:先看日志再动手1.日志是关键系统日志...
- 一周热门
- 最近发表
- 标签列表
-
- HTML 简介 (30)
- HTML 响应式设计 (31)
- HTML URL 编码 (32)
- HTML Web 服务器 (31)
- HTML 表单属性 (32)
- HTML 音频 (31)
- HTML5 支持 (33)
- HTML API (36)
- HTML 总结 (32)
- HTML 全局属性 (32)
- HTML 事件 (31)
- HTML 画布 (32)
- HTTP 方法 (30)
- 键盘快捷键 (30)
- CSS 语法 (35)
- CSS 轮廓宽度 (31)
- CSS 谷歌字体 (33)
- CSS 链接 (31)
- CSS 定位 (31)
- CSS 图片库 (32)
- CSS 图像精灵 (31)
- SVG 文本 (32)
- 时钟启动 (33)
- HTML 游戏 (34)
- JS Loop For (32)