0%

javascript singleton mode

A simple singleton with global variable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const Singleton = function (name) {
this.name = name;
};

let instance = null;

Singleton.getInstance = function (name) {
if (!instance) {
instance = new Singleton(name);
}

return instance;
};

const a = Singleton.getInstance('zdd');
const b = Singleton.getInstance('ddz');
console.log(a === b); // true

这是一个最简单的单例模式,但是这种方式有一个问题,就是instance是一个全局变量,会污染全局空间。稍微改进一下,将instance挂载到Singleton上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const Singleton = function (name) {
this.name = name;
};

Singleton.getInstance = function (name) {
if (!this.instance) {
this.instance = new Singleton(name);
}

return this.instance;
};

const a = Singleton.getInstance('zdd');
const b = Singleton.getInstance('ddz');
console.log(a === b);

这样就不会污染全局空间了,但是这种方式还有一个问题,无法阻止使用者通过new Singleton()来创建实例。这无形中增加了使用者的心智负担,使用者必须提前知道Singleton是一个单例,也必须调用getInstance方法来获取实例。假设使用者用new Singleton()来创建实例,那么就会创建出多个实例。

1
2
3
const c = new Singleton('zdd');
const d = new Singleton('ddz');
console.log(c === d); // false, not a singleton !!!

为了避免用户通过new来创建实例,可以使用闭包来实现,将Singleton变成一个IIFE(Immediately Invoked Function Expression)。

Use IIFE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const Singleton = (() => {
let instance = null;

// 这里的init函数返回的是一个对象,而不是一个类的实例。
// 这里init是箭头函数,最好不要使用this, 因为this指向global对象。
const init = (name) => ({
name,
});

return {
getInstance: (name) => {
if (!instance) {
instance = init(name);
}

return instance;
},
};
})();

const a = Singleton.getInstance('zdd');
const b = Singleton.getInstance('ddz');
console.log(a === b); // true

此时,使用者就无法通过new来创建实例了,只能通过getInstance来获取实例。

1
2
3
const c = new Singleton(); // error, Singleton is not a constructor
const d = new Singleton();
console.log(c === d);

这里使用IIFE的好处是:

  1. 不会污染全局空间,所有变量及方法都是私有的。
  2. 用户无法使用new来创建实例,只能通过getInstance来获取实例。

为了区分私有方法和变量,以及公有方法和变量,可以使用下面的代码,更好的区分私有和公有。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
const Singleton = (() => {
let instance = null;

const init = () => {
// Private methods and variables
const privateVariable = 'I am private variable';
const privateMethod = () => {
console.log('Private method');
};

// This object is the return value of init will be assigned to instance.
return {
// Public methods and variables
publicVariable: 'I am public variable',
publicMethod: () => {
console.log('Public method');
},
};
};

// Public method to get the singleton instance
const getInstance = () => {
if (!instance) {
instance = init();
}
return instance;
};

// Expose the public method
return {
getInstance,
};
})();

// Usage
const singletonInstance1 = Singleton.getInstance();
const singletonInstance2 = Singleton.getInstance();

console.log(singletonInstance1 === singletonInstance2); // true

前面说过,使用.getInstance()来获取实例,增加了用户的心智负担,如果想要用户通过new来创建实例,可以使用下面的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const Singleton = (() => {
let instance = null;

function createInstance(name) {
if (!instance) {
// Init code goes here, If you want to exact init to a function, you must use createInstance.prototype.init = function(name){this.name = name}. This will make init public to every instance, it's bad idea!
this.name = name;
instance = this;
}

return instance;
}

return createInstance;
})();

const a = new Singleton('zdd');
const b = new Singleton('ddz');
console.log(a === b); // true

下面是使用ES6的class来实现单例模式。代码更加简洁优雅。

Use class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Singleton {
constructor() {
if (!Singleton.instance) {
Singleton.instance = this;
// Your initialization code here
}
return Singleton.instance;
}

// Additional properties and methods can be added here
}

const instance1 = new Singleton();
const instance2 = new Singleton();
console.log(instance1 === instance2); // true

虽然这种方式代码简洁,但是这种方式有一个问题,就是用户可以通过new Singleton()来创建实例,虽然创建的实例都是同一个,但是还是很别扭,我们可以换一个思路,指导出一个实例,而不是导出整个类。

Use Proxy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Singleton {
constructor() {
if (!Singleton.instance) {
Singleton.instance = this;
// Your initialization code here
}
return Singleton.instance;
}

// Additional properties and methods can be added here
}

const singletonInstance = new Singleton();
export default Object.freeze(singletonInstance);

这样用户只需使用我们导出的实例就行了,不用自己去创建实例。但是这种方式还是有一个问题,就是用户可以通过singletonInstance.instance来获取到实例,这样就破坏了单例模式的封装性。我们可以使用ES2022中新增的private field来解决这个问题。

Use private field

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Singleton {
static #instance = null;

constructor() {
if (!Singleton.#instance) {
Singleton.#instance = this;
// Your initialization code here
}
return Singleton.#instance;
}

// Additional properties and methods can be added here
logInstance() {
console.log(Singleton.#instance);
}
}

const singleton = new Singleton();
export default Object.freeze(singleton);

TypeScript

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Singleton {
private static instance: Singleton;

private constructor() {}

public static getInstance(): Singleton {
if (!Singleton.instance) {
Singleton.instance = new Singleton();
}
return Singleton.instance;
}
}

const s1 = Singleton.getInstance();
const s2 = Singleton.getInstance();
console.log(s1 === s2); // true

Use Proxy

待补充。