JavaScript-使用WeakMap创建对象的私有属性

我们都知道JavaScript本身并没有共有、私有属性的概念,不过我们可以通过一些方式实现私有属性。
WeakMap也是ES6里就有了,不过我曾一直不太了解它的应用场景,看到有文章说用它的应用场景之一是实现私有属性。怎么实现?为何要用它实现?
关于用WeakMap实现私有属性的方式,本文会从它最原始的面貌看起,理解使用WeakMap的意义

闭包-可行吗?

提到“私有”的实现方式,我的第一反应就是闭包,似乎可行:

1
2
3
4
5
6
7
8
9
10
11
12
13
const Person = (function() {
let name;

function Person(n) {
name = n;
}

Person.prototype.getName = function() {
return name;
};

return Person;
}());

这样我们只要

1
let person1 = new Person('小明');

这样person1就有它的私有name属性了,外部无法直接访问或修改改属性。等等,似乎有什么问题,如果我们再实例化一次Person呢?

1
2
3
let person1 = new Person('小明');
let person2 = new Person('大明');
console.log(person1.getName()); // 大明

小明变成了大明!person1name属性被覆盖了,所以这种方式实现的私有属性,实际上是被所有实例共享的,如果需要每个实例单独拥有自己的私有属性,这种方法就不行了。

改进

为了让每个实例拥有自己的私有属性,相互之间不影响,我们可以引入一个id作为每个实例的唯一标识,这样就实现了真正的私有属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const Person = (function() {
const private = {};
let privateId = 0;

function Person(name) {
this._privateId = privateId++;
private[this._privateId] = {};
private[this._privateId].name = name;
}

Person.prototype.getName = function() {
return private[this._privateId].name;
};

return Person;
}());

let person1 = new Person('小明');
let person2 = new Person('大明');
console.log(person1.getName()); // 小明

不过这样仍然有隐患,实例化后我们还是能更改其_privateId属性的值,一旦它被更改,私有属性就获取不到了。

1
2
person1._privateId = 111;
console.log(person1.getName()); // 报错

所以我们改为用Object.defineProperty方法申明_private属性,防止它被更改。

1
2
3
4
5
6
7
Object.defineProperty(this, '_privateId', {
value: privateId++,
writable: false, // 设为不可写,_privateId就不会被更改了(其实默认就是false)
});
this._privateId = privateId++;
private[this._privateId] = {};
private[this._privateId].name = name;

至此,我们仅用es5的特性就实现了私有属性,而然该方法还有以下几个弊端:

  • 即便Person的某个实例对象被垃圾回收了,private对象里存储的它的全部私有属性依旧不会被回收,这会导致内存泄漏问题
  • 每个实例对象多出了一个_privateId属性,而且该方法不够直观优雅

WeakMap了解一下?

既然我们不想多造一个单独的_privateId属性去实现私有属性的存储,那还有什么值可以作为该实例对象的唯一标识呢?它的内存地址?可是js似乎不允许直接获取一个对象的地址。直接拿对象本身作为key可以吗?

1
2
private[this] = {};
private[this].name = name;

这么写在语法上当然没问题,但实际上一个对象的key只能是字符串或Symbol,因而所有对象都会被转为同一段符串“[object Object]”(private[this] = {}实际上就是private[‘[object Object]’] = {}),所以这种方法当然不行。
而ES6的Map和WeakMap是可以将对象作为key的(WeakMap的key只能是对象)。那我们用WeakMap试一下?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const Person = (function() {
const private = new WeakMap();

function Person(name) {
private.set(this, {});
private.get(this).name = name;
}

Person.prototype.getName = function() {
return private.get(this).name;
};

return Person;
}());

let person1 = new Person('小明');
let person2 = new Person('大明');
console.log(person1.getName()); // 小明

现在代码看上去简单优雅了很多,我们不再需要额外的id去标识每个实例,那内存泄漏问题呢?这就是为何要用WeakMap而不是Map了,WeakMap对key对象仅有“弱引用”,当没有其它引用指向该key对象时,该对象即可被垃圾回收,WeakMap不会阻止回收。

其实用WeakMap实现私有属性本身是比较简单的,但了解其背后的原因和原理更加重要。