一、函数类与 class 类的基础认知 📚
在 JavaScript 里,类就像制作饼干的模具,能造出一堆长得一样的饼干(对象)。我们有两种 “模具”:函数类和 ES6 新出的 class 类。
1. 函数类
用构造函数就能定义函数类,既能收参数,又能挂公共方法,还能搞个 “静态方法” 玩玩
比如这样:
function Point(x, y) {
this.x = x; // 给实例加个x属性
this.y = y; // 再给实例加个y属性
}
// 原型上挂个实例方法,大家都能用
Point.prototype.toString = function () {
return `(${this.x}, ${this.y})`;
};
// 直接在函数上定义的,就是静态方法,实例拿不到
Point.toSum = function (a, b) {
return a + b;
};
// 造个实例试试
const point1 = new Point(1, 2);
console.log(point1.toString()); // 输出:(1, 2) ✨
console.log(Point.toSum(3, 4)); // 输出:7 ✨
console.log(point1.toSum); // 输出:undefined(实例访问不到静态方法)
这里的Point就是函数类,new一下就能造实例。this指向新实例,prototype上的是实例方法,函数上直接定义的是静态方法,只能用类名调用
2. class 类
ES6 的class类就是个 “语法糖”,把构造函数和原型包装得更好看啦,写起来超直观
看例子:
class Point {
// 构造器,造实例时自动调用,用来初始化
constructor(x, y) {
this.x = x;
this.y = y;
}
// 实例方法,其实也是挂在原型上的
toString() {
return `(${this.x}, ${this.y})`;
}
// 静态方法,加个static关键字,只能类名调用
static toSum(a, b) {
return a + b;
}
}
// 造个实例玩玩
const point2 = new Point(3, 4);
console.log(point2.toString()); // 输出:(3, 4) ✨
console.log(Point.toSum(5, 6)); // 输出:11 ✨
console.log(point2.toSum); // 输出:undefined(实例还是拿不到静态方法)
class里的constructor就是构造器,toString是实例方法,static修饰的toSum是静态方法,和函数类功能差不多,但看起来更清爽
二、从生活场景理解单例模式 🏠
你家的总电闸,是不是只有一个?多装几个就乱套了!😅
编程里也一样,有些对象就该独一份!比如全局配置、弹窗管理器、购物车,要是整出好几个实例,不仅费资源,数据还可能打架。这种 一个类只能造一个实例 的思路,就是单例模式
三、单例模式的核心特点 🔑
就仨特点,记牢咯:
- 唯一实例:不管造多少次,都返回同一个对象
- 自行实例化:自己管自己的创建,不用外人插手
- 全局访问:有个统一的入口能拿到这个唯一实例
代码效果大概这样:
// 假设有个单例类Singleton
const instance1 = new Singleton();
const instance2 = new Singleton();
console.log(instance1 === instance2); // 输出:true(俩变量指向同一个实例)
四、用 JS 实现单例模式 💻
实现方式有好几种,从简单到复杂来看看
1. 基础实现:用闭包存实例
先整个函数类的:
function Singleton() {
// 看看有没有实例了
if (Singleton.instance) {
return Singleton.instance;
}
// 没有就造一个
this.name = "单例实例";
this.version = "1.0.0";
// 存起来
Singleton.instance = this;
}
// 试试
const a = new Singleton();
const b = new Singleton();
console.log(a === b); // true ✨
console.log(a.name); // "单例实例"
console.log(b.version); // "1.0.0"
2. 基于 ES6 class 的实现
用 class 写更清楚:
class Singleton {
// 实例方法
addData(item) {
this.data.push(item);
}
// 静态方法,提供全局访问入口
static getInstance() {
if (!Singleton.instance) {
Singleton.instance = new Singleton();
}
return Singleton.instance;
}
}
// 获取实例
const c = Singleton.getInstance();
const d = Singleton.getInstance();
console.log(c === d); // true ✨
c.addData("test");
console.log(d.data); // ["test"](数据共享)
3. 更优雅的实现:立即执行函数
用闭包把实例藏起来,更安全:
const Singleton = (function() {
let instance; // 闭包里的变量存实例
class InnerSingleton {
constructor() {
this.config = {
theme: "light",
language: "zh-CN"
};
}
updateConfig(key, value) {
this.config[key] = value;
}
}
// 返回个函数用来拿实例
return function() {
if (!instance) {
instance = new InnerSingleton();
}
return instance;
};
})();
// 用用看
const e = new Singleton();
const f = new Singleton();
e.updateConfig("theme", "dark");
console.log(f.config.theme); // "dark"(数据同步更新啦)
console.log(e === f); // true ✨
五、单例模式的优缺点分析 ⚖️
优点:
- 省资源:就一个实例,不用频繁创建销毁,省性能
- 数据一致:都操作同一个实例,不会出现数据乱套的情况
- 全局能访问:在哪都能拿到,用着方便
缺点:
- 隐藏依赖:全局都能改,说不定哪就被改了,不好查
- 测试麻烦:实例状态会影响测试,前一个测试改了数据,下一个可能就不准了
- 不专一:又管创建又管业务,违背单一职责原则
- 难扩展:就一个实例,想扩展或替换都费劲
六、单例模式的适用场景 📍
虽然有缺点,但这些场景用它超合适:
- 全局配置对象:整个应用就一份配置,比如接口地址、主题设置
// 全局配置单例
class Config {
constructor() {
if (Config.instance) return Config.instance;
this.apiBaseUrl = "https://api.example.com";
this.timeout = 5000;
Config.instance = this;
}
}
- 弹窗管理器:整个页面就一个管理器管弹窗显示隐藏
class DialogManager {
constructor() {
if (DialogManager.instance) return DialogManager.instance;
this.dialogs = [];
DialogManager.instance = this;
}
showDialog(content) {
// 显示弹窗的逻辑
}
}
- 购物车实例:电商网站,购物车必须全局唯一
class ShoppingCart {
constructor() {
if (ShoppingCart.instance) return ShoppingCart.instance;
this.items = [];
ShoppingCart.instance = this;
}
addItem(product) {
this.items.push(product);
}
}
- 日志工具:一般就一个日志实例统一处理日志输出
七、使用单例模式的注意事项 ⚠️
- 别乱用:不是啥都要搞成单例,确实需要唯一实例再用
- 管好状态:单例状态会一直存在,没用的数据及时清掉
- 注意并发:多线程环境(JS 用 Worker 时)要加锁保证唯一
- 测试隔离:写单元测试时,每个测试前后最好重置下单例状态
八、总结 📝
单例模式就是保证一个类只有一个实例,还能全局访问。省资源、数据一致是它的好处,但也有隐藏依赖、测试难等问题。
需要全局唯一对象协调系统时,再用单例模式!比如全局配置、弹窗管理,用它能让代码更清爽高效
希望这篇文章能让你搞懂单例模式,下次遇到类似需求,就能轻松搞定!🎉