Harmony鸿蒙开发0基础入门到精通Day10--JavaScript篇
单例模式
单例模式是设计模式中创建型模式的一种,核心目标是保证一个类(或对象)在整个应用中只有一个实例,并提供一个全局统一的访问点,避免重复创建实例造成资源浪费或数据不一致。
单例模式的实现依赖两个关键逻辑:
- 控制实例创建:通过私有化构造函数、判断实例是否已存在等方式,阻止外部重复创建实例;
- 提供全局访问:暴露一个静态方法或全局对象,让外部只能通过这个 “入口” 获取唯一实例。
利用闭包 “持久化保存实例状态” 的特性,将实例隐藏在闭包中,仅暴露获取实例的方法,防止外部直接修改。
// 闭包实现单例:创建一个唯一的弹窗实例
const ModalSingleton = (function() {let instance = null; // 闭包中保存唯一实例// 私有方法:创建弹窗 DOM(模拟实例初始化逻辑)function createModal() {const modal = document.createElement('div');modal.style.display = 'none';modal.textContent = '全局唯一弹窗';document.body.appendChild(modal);return modal;}// 暴露的全局访问点:获取实例(不存在则创建)return {getInstance: function() {if (!instance) { // 关键:判断实例是否已存在instance = createModal();}return instance;}};
})();// 使用:多次调用 getInstance,获取的是同一个实例
const modal1 = ModalSingleton.getInstance();
const modal2 = ModalSingleton.getInstance();
console.log(modal1 === modal2); // true(实例唯一)// 显示弹窗(操作同一个 DOM 元素)
modal1.style.display = 'block';通过 ES6 类的静态方法(static)控制实例创建,同时在构造函数中判断是否已有实例,防止外部通过 new 关键字重复创建。
class LoggerSingleton {// 静态属性:保存唯一实例(用 # 私有化,防止外部修改)static #instance = null;// 私有方法:初始化日志配置(模拟实例逻辑)#initConfig() {this.logLevel = 'info';this.logDir = './logs';}// 构造函数:私有化(ES6 用 # 标识私有构造函数,禁止外部 new)constructor() {if (LoggerSingleton.#instance) {throw new Error('Logger 已存在实例,请通过 getInstance 获取');}this.#initConfig();}// 静态方法:全局访问点,获取唯一实例static getInstance() {if (!LoggerSingleton.#instance) {LoggerSingleton.#instance = new LoggerSingleton();}return LoggerSingleton.#instance;}// 实例方法:日志打印(业务逻辑)log(message) {console.log(`[${this.logLevel}] ${message}`);}
}// 使用:通过静态方法获取实例
const logger1 = LoggerSingleton.getInstance();
const logger2 = LoggerSingleton.getInstance();
console.log(logger1 === logger2); // true// 禁止外部 new(会报错)
// const logger3 = new LoggerSingleton(); // 报错:Constructor of class 'LoggerSingleton' is privatelogger1.log('应用启动'); // [info] 应用启动
// 其他文件1:a.js
import logger from './logger.js';
logger.log('a.js 日志'); // [info] a.js 日志// 其他文件2:b.js
import logger from './logger.js';
logger.log('b.js 日志'); // [info] b.js 日志// 验证:a.js 和 b.js 导入的是同一个实例
// 在 a.js 中修改 logger.logLevel = 'error',b.js 中打印会变成 error 级别策略模式
策略模式(Strategy Pattern)是行为型设计模式的一种,核心思想是:将一组可替换的算法(或行为)封装成独立的 “策略”,让它们可以在运行时动态切换,而不影响使用策略的客户端。其本质是 “分离算法的定义与使用”,避免用大量 if-else 或 switch 语句判断逻辑,提高代码的灵活性和可维护性。
为什么需要策略模式?(解决的问题)
没有策略模式时,若存在多种可选算法,通常会用 if-else 堆叠判断,导致代码臃肿、难以扩展:
// 反例:不用策略模式,用 if-else 处理多种支付方式
function calculatePrice(price, payType) {if (payType === 'alipay') {return price * 0.9; // 支付宝9折} else if (payType === 'wechat') {return price * 0.85; // 微信8.5折} else if (payType === 'card') {return price * 0.8; // 银行卡8折} else {throw new Error('未知支付方式');}
}// 使用时
console.log(calculatePrice(100, 'alipay')); // 90问题:
- 新增支付方式(如
cash现金)需修改calculatePrice函数,违反 “开闭原则”(对扩展开放,对修改关闭); - 逻辑复杂时,
if-else堆叠导致代码可读性差、维护困难。
JavaScript 中函数是 “一等公民”,策略模式可简化实现(无需严格的类结构),核心是用对象封装策略,用环境函数调度策略。
// 1. 定义策略:封装不同支付方式的折扣算法(统一接口:接收price,返回计算后价格)
const payStrategies = {alipay: (price) => price * 0.9,wechat: (price) => price * 0.85,card: (price) => price * 0.8,cash: (price) => price // 新增现金支付(无折扣),无需修改其他代码
};// 2. 定义环境:负责接收请求,委托给对应的策略
function calculatePrice(price, payType) {// 检查策略是否存在if (!payStrategies[payType]) {throw new Error('未知支付方式');}// 调用对应策略计算价格return payStrategies[payType](price);
}// 使用:直接指定策略类型
console.log(calculatePrice(100, 'alipay')); // 90
console.log(calculatePrice(200, 'cash')); // 200(新增策略直接可用)改进点:
- 新增支付方式只需在
payStrategies中添加键值对,无需修改calculatePrice函数,符合 “开闭原则”; - 策略逻辑与调度逻辑分离,代码更清晰。
观察者模式
观察者模式(Observer Pattern)是行为型设计模式的一种,核心思想是:建立对象间的 “一对多” 依赖关系,当一个对象(称为 “主题 / 被观察者”)的状态发生变化时,所有依赖它的对象(称为 “观察者”)会自动收到通知并更新,实现 “状态变化 -> 自动同步” 的效果。
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head><body></body>
<script>/*** 学生老师* 观察者模式* * 观察者 老师 一个函数* * 被观察者 学生 一个函数* * 一旦被观察者发生动态改变 那么要及时通知观察者 观察者会采取对应的措施* ***/// 自定义函数 观察者class Observer {constructor(name, fn = () => { }) {this.name = namethis.fn = fn}}// 创建两个观察者const p1 = new Observer('班主任', (state) => {console.log(`班主任看你在:${state}`);})const p2 = new Observer('咨询师', (state) => {console.log(`咨询师看你在:${state}`);})// 被观察者class Subject {constructor(state) {// 自己定义自己的状态this.state = state// 是不是可以定义一个变量用来存储谁偷看我呢this.observer = []}// 定义一个方法需要知道刚才谁看我了addObserver(obs) {// 如果之前她已经来过了 就不需要再次添加// 如果是第一次来 那是不是我就应该要找个变量存储一下this.observer = this.observer.filter(item => item != obs)// 上面判断的是 当前observer中是没有这个变量,this.observer.push(obs)}}// 当前需要实例化被观察者const s1 = new Subject('听课')s1.addObserver(p1)s1.addObserver(p2)s1.addObserver(p1)s1.addObserver(p2)console.log(s1);</script></html>发布订阅模式
发布订阅模式(Publish-Subscribe Pattern)是行为型设计模式的一种,核心思想是:通过一个 “事件总线(Event Bus)” 中间层,实现发布者(Publisher)和订阅者(Subscriber)的完全解耦。发布者无需知道订阅者的存在,订阅者也无需知道发布者的信息,两者通过 “事件” 间接通信 —— 发布者发布事件到总线,总线将事件通知给所有订阅了该事件的订阅者。
核心角色与工作流程
发布订阅模式比观察者模式多了一层 “事件总线”,核心角色和流程如下:
1. 核心角色
- 发布者(Publisher):负责发布事件到事件总线,不直接与订阅者交互;
- 订阅者(Subscriber):通过事件总线订阅感兴趣的事件,当事件被发布时接收通知并处理;
- 事件总线(Event Bus):中间层,管理所有事件的订阅关系(存储 “事件类型→订阅者回调” 的映射),并在事件发布时通知对应订阅者。
2. 工作流程
- 订阅:订阅者通过事件总线的
on方法,订阅特定事件(如'message'),并注册回调函数(事件触发时执行的逻辑); - 发布:发布者通过事件总线的
emit方法,发布特定事件,并传递事件数据; - 通知:事件总线收到事件后,遍历该事件的所有订阅者回调,依次执行(可传递事件数据);
- 取消订阅:订阅者可通过
off方法取消对某个事件的订阅,不再接收通知。
与观察者模式的核心区别(关键!)
发布订阅模式常与观察者模式混淆,但两者的解耦程度和结构有本质区别:
| 对比维度 | 观察者模式 | 发布订阅模式 |
|---|---|---|
| 核心结构 | 主题(Subject)直接管理观察者(无中间层) | 发布者、订阅者通过 “事件总线” 间接交互(有中间层) |
| 耦合度 | 主题与观察者轻度耦合(主题知道观察者的 update 方法) | 发布者与订阅者完全解耦(彼此不知道对方存在) |
| 通信方式 | 主题主动调用观察者的方法 | 发布者发布事件到总线,总线转发给订阅者 |
| 适用场景 | 单一主题与多个观察者的直接关联(如 DOM 事件) | 跨模块 / 跨系统的松散耦合通信(如全局事件、消息队列) |
形象比喻:
- 观察者模式:老师(主题)直接点名通知学生(观察者)交作业;
- 发布订阅模式:老师(发布者)在班级群(事件总线)发 “交作业” 通知,所有订阅了群消息的学生(订阅者)收到通知。
JavaScript 中的发布订阅模式实现(事件总线)
在 JavaScript 中,发布订阅模式的核心是实现一个 “事件总线”,提供 on(订阅)、emit(发布)、off(取消订阅)、once(一次性订阅)等方法。
class EventBus {constructor() {// 存储事件映射:{ 事件类型: [回调1, 回调2, ...] }this.events = Object.create(null); }/*** 订阅事件* @param {string} type - 事件类型(如 'message')* @param {Function} callback - 事件触发时的回调函数*/on(type, callback) {// 初始化事件对应的回调数组(首次订阅时)if (!this.events[type]) {this.events[type] = [];}// 添加回调(去重:避免重复订阅同一回调)if (!this.events[type].includes(callback)) {this.events[type].push(callback);}}/*** 发布事件* @param {string} type - 事件类型* @param {...any} args - 传递给订阅者的参数(可多个)*/emit(type, ...args) {// 若事件无订阅者,直接返回if (!this.events[type]) return;// 遍历所有订阅者回调,执行并传递参数this.events[type].forEach(callback => {callback.apply(this, args); // 绑定 this 为 EventBus 实例});}/*** 取消订阅* @param {string} type - 事件类型* @param {Function} callback - 要取消的回调函数(不传则取消该事件所有订阅)*/off(type, callback) {if (!this.events[type]) return;// 若未传 callback,清空该事件所有订阅if (!callback) {this.events[type] = [];return;}// 否则,移除指定回调this.events[type] = this.events[type].filter(cb => cb !== callback);}/*** 一次性订阅(触发一次后自动取消)* @param {string} type - 事件类型* @param {Function} callback - 回调函数*/once(type, callback) {// 包装回调:执行后自动取消订阅const wrapper = (...args) => {callback.apply(this, args); // 执行原回调this.off(type, wrapper); // 取消自身订阅};this.on(type, wrapper);}
}深浅拷贝
深浅拷贝是 JavaScript 中处理引用类型数据(如对象、数组)复制的核心概念,核心区别在于是否递归复制引用类型的内部层级:浅拷贝仅复制表层属性,引用类型仍共享原数据;深拷贝则完全复制所有层级,生成独立的新对象,两者的选择直接影响数据独立性和代码安全性。
核心概念:先明确基本类型与引用类型
深浅拷贝的差异仅针对引用类型,需先区分两种数据类型的存储机制:
- 基本类型(number、string、boolean、null、undefined、Symbol、BigInt):值直接存储在栈内存,拷贝时直接复制值,不存在深浅之分;
- 引用类型(object、array、function、Map、Set 等):值存储在堆内存,变量仅保存堆内存的 “地址引用”,拷贝时若只复制地址则为浅拷贝,复制地址指向的所有数据则为深拷贝。
浅拷贝
只复制对象的表层属性:
- 基本类型属性:直接复制值,新对象与原对象的基本类型属性独立;
- 引用类型属性:仅复制 “地址引用”,新对象与原对象的引用类型属性共享同一堆内存数据,修改一方会影响另一方。
常见实现方法(附示例)
| 实现方式 | 适用场景 | 示例代码 |
|---|---|---|
| Object.assign() | 对象表层拷贝 | const obj2 = Object.assign({}, obj1); |
| 扩展运算符(...) | 对象 / 数组表层拷贝 | const obj2 = { ...obj1 };const arr2 = [ ...arr1 ]; |
| 数组 slice () | 数组表层拷贝(无参数) | const arr2 = arr1.slice();(slice (0) 效果相同) |
| 数组 concat () | 数组表层拷贝(空参数) | const arr2 = arr1.concat(); |
| Array.from() | 数组表层拷贝 | const arr2 = Array.from(arr1); |
浅拷贝的特点与问题
- 优点:性能高(仅复制表层),实现简单;
- 问题:引用类型属性共享,修改会相互影响(典型坑点)。
深拷贝(Deep Copy):完全复制,独立无关联
原理
递归复制对象的所有层级属性:
- 基本类型属性:复制值;
- 引用类型属性:递归遍历其内部所有层级,生成新的独立对象(新的堆内存地址),新对象与原对象完全独立,修改一方不影响另一方。
const obj1 = { name: "Alice", address: { city: "Beijing" } };
const obj2 = JSON.parse(JSON.stringify(obj1));// 修改引用类型属性:互不影响
obj2.address.city = "Shanghai";
console.log(obj1.address.city); // "Beijing"(原对象不变)局限性(必知!):
- 无法拷贝 函数、undefined、Symbol(会被忽略);
- 无法拷贝 循环引用(如
obj1.self = obj1,会报错); - 无法拷贝 特殊对象(如 Map、Set、Date、RegExp,会被转为普通对象或丢失信息)。
自定义递归函数(灵活可控)
原理:通过递归遍历对象 / 数组的每一层,判断属性类型,基本类型直接复制,引用类型则创建新对象 / 数组后继续递归。
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head><body></body>
<script>let obj = {name: '张三',age: 18,infor: {id: 10,gender: '男'}}// //浅拷贝// let obj2 = {...obj}// obj.infor.id = 100// console.log(obj2);//infor:{id: 100, gender: '男'}//这里属于浅拷贝,对于引用数据来说,是一个地址//这样的话,修改地址里的值,拷贝过来的地址所对应的值也会发生改变。//这是浅拷贝的弊端。//target是新对象 obj是旧对象function cloneDFS(target, obj) {for (let k in obj) {// if (Object.prototype.toString.call(obj[k]) == '[object Object]') {// target[k] = {}// cloneDFS(target[k], obj[k])// } else if (Object.prototype.toString.call(obj[k]) == '[object Array]') {// target[k] = []// cloneDFS(target[k], obj[k])// }else{// target[k] = obj[k]// }if (obj[k] instanceof Object) {target[k] = {}cloneDFS(target[k], obj[k])} else if (obj[k] instanceof Array) {target[k] = []cloneDFS(target[k], obj[k])}else{target[k] = obj[k]}}}let newC = {};cloneDFS(newC,obj)obj.infor.id = 100console.log(obj);console.log(newC);</script></html>