2025年09月19日 Tags: Js
最近在读 ESLint 的源码,看到作者在实例化 ESLint 类的时候会将一些变量存储到 WeakMap 中,觉得这个用法应该好好记录一下。这里研究一下为什么使用 WeakMap 来进行存储,以及哪些变量/数据需要使用 WeakMap 进行存储,学习一下 WeakMap 的使用场景。
背 1000 道面试题,不如认真研究一个技术应用。🤷♀️
首先,上一下 ESLint 的源代码:
const privateMembers = new WeakMap();
class ESLint {
constructor(options = {}) {
const processedOptions = processOptions(options);
const warningService = new WarningService();
const linter = createLinter(processedOptions, warningService);
const cacheFilePath = getCacheFile(processedOptions.cacheLocation, processedOptions.cwd);
const lintResultCache = createLintResultCache(processedOptions, cacheFilePath);
const defaultConfigs = createDefaultConfigs(options.plugins);
this.#configLoader = createConfigLoader(processedOptions, defaultConfigs, linter, warningService);
privateMembers.set(this, {
options: processedOptions,
linter,
cacheFilePath,
lintResultCache,
defaultConfigs,
configs: null,
configLoader: this.#configLoader,
warningService,
});
}
// more code ...
}
这里,可以看到 ESLint 作者在封装 ESLint 类时,将一些变量存储在 WeakMap 中,以实现私有化。对外暴露的 node.js API —— ESLint 类,在外部一经实例化,是不能访问这些私有化成员的。
(1)WeakMap 是一个键值对的集合,键必须是对象或非全局注册的symbol,也就是键必须是可被垃圾回收的。当 WeakMap 的键被垃圾回收时,其相应的值也会被垃圾回收。
(2)WeakMap 是不能够被遍历和序列化的(JSON.stringify()
),这是因为 WeakMap 的键值状态依赖于垃圾回收的状态,是不确定的,因此不允许观察其键值的生命周期。因此如果你想访问键值列表,则应该使用 Map,其有一系列的遍历方法:forEach、entries、keys、values。而 WeakMap 是没有这些方法的。
const a = Symbol(); // 唯一
const b = Symbol(); // 唯一
const c = {};
const m = new WeakMap();
m.set(a, {a: 1});
m.set(b, {b: 2});
m.set(c, [1, 2, 3]);
console.log(JSON.stringify(m)); // 返回一个 {} ,没有内容
console.log(m.get(a)); // {a: 1}
console.log(m.get(b)); // {b: 2}
m.has(c); // true
m.delete(c);
m.has(c); // false
官方:使用
Symbol()
函数的语法,不会在你的整个代码库中创建一个可用的全局的 symbol 类型。要创建跨文件可用的 symbol,甚至跨域(每个都有它自己的全局作用域),使用Symbol.for()
方法和Symbol.keyFor()
方法从全局的 symbol 注册表设置和取得 symbol。
这句话的意思是说,如果不传递任何描述(description)、直接使用 symbol()
方法创建的 symbol 是不会将该 symbol 注册到全局的 symbol 注册表中的。也就是说无法通过 Symbol.for()
或 Symbol.keyFor()
注册、查找 symbol。
通常,Symbol 可以分为以下三种:
Symbol()
Symbol(description)
,Symbol.for(key)
如果在全局注册中未查找到对应的 symbol 也会全局注册。这篇文章中列举了几个私有化的实现方式:Hiding Implementation Details with ECMAScript 6 WeakMaps
_
来命名function Public() {
this._private = "foo";
}
Public.prototype.method = function () {
// Do stuff with `this._private`...
};
这种方式需要开发人员自觉遵守私有变量的使用规则,如果用户在使用私有变量或方法时,完全可以对这些变量或方法进行重写,这种私有化方式只是形式上的,并不是真正实现私有化。
function Public() {
const closedOverPrivate = "foo";
this.method = function () {
// Do stuff with `closedOverPrivate`...
};
}
// Or
function makePublic() {
const closedOverPrivate = "foo";
return {
method: function () {
// Do stuff with `closedOverPrivate`...
}
};
}
这种通过闭包的方式的确将变量封装在函数或类的内部,在调用函数或实例化类后,通常外部是不能访问到内部的变量的。但是,这种方式的缺点也很明显,就是如果这种变量多了会占用内存,影响性能。前面也提到了垃圾回收,在使用闭包时,如果一直保持对函数内部变量的引用,就会影响变量进行垃圾回收,内存无法释放而导致内存泄漏。
const privateFoo = Symbol("foo");
function Public() {
this[privateFoo] = "bar";
}
Public.prototype.method = function () {
// Do stuff with `this[privateFoo]`...
};
module.exports = Public;
通过 symbol 作为实例属性名,用户对这些属性“只可远观而不可亵玩焉”,看上去确实实现了私有化。但是!!!,通过 Object.getOwnPropertySymbols()
、Reflect.ownKeys()
依然可以得到对象自有属性 symbols 组成的数组,通过这个方式依旧可以实现对实例私有属性的访问甚至修改。
const p = Symbol('private');
class Test {
constructor() {
this[p] = 123;
}
}
const t = new Test(); // Test {Symbol(private): 123}
const symbols = Object.getOwnPropertySymbols(t); // [Symbol(private)]0: Symbol(private)length: 1[[Prototype]]: Array(0)
console.log(t[symbols[0]]); // 123 (访问)
t[symbols[0]] = 345; // 修改
console.log(t); // Test {Symbol(private): 345} (可以看到私有属性被修改了)
其他方式:
ES2022 正式为 class 添加了私有属性,方法是在属性名之前使用 #
表示。在类的外部使用私有属性是会报错的。
class IncreasingCounter {
#count = 0;
get value() {
console.log('Getting the current value!');
return this.#count;
}
increment() {
this.#count++;
}
}
const counter = new IncreasingCounter();
counter.#myCount // 报错!!(但是在控制台中使用是不会报错的,这样是为了方便调试)
由于是 ES2022 版本才有的,使用这种方式时要考虑兼容性问题。
最终,就要说到 WeakMap 了 😁
const privateMembers = new WeakMap();
class Test() {
constructor() {
privateMembers.set(this, {
// private properties ...
})
},
method1() {
const {
// use private properties
} = privateMembers.get(this);
}
}
通过 WeakMap 来使用私有属性,可以细数出以下优点:
基于上述优点,一些常见框架比如 Vue 在实现响应式时,也使用到了 WeakMap,对响应式变量的订阅进行收集。一旦响应式变量被回收,那么相应的一系列副作用也会被回收。一是方便进行统一管理(后续对副作用的执行),二是方便进行垃圾回收。 ( 深入响应式系统)
插件实现:私有属性,不希望被外部访问、修改,实现封装。
一些变量/方法需要被回收。以及兼容性考虑。
Hiding Implementation Details with ECMAScript 6 WeakMaps