前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
首页程序员面试题库WeakMap 模拟私有状态 对比 # 私有字段
JavaScript对象与原型

用 WeakMap 怎么模拟 class 的私有属性?和 # 私有字段比各有什么优劣?

在模块里建一个 WeakMap,以实例 this 为键存私有数据,只在类的方法里读写。外部拿不到这个 WeakMap,也就拿不到数据;实例被回收后条目自动消失。新代码直接用 # 私有字段,语法层面保证私有,访问错了还会报错。

前端进阶之旅 · 一题精讲更新于 2026.09.29
JavaScript#对象与原型
先看核心答案读代码示例
理解线索

私有数据藏在哪里

  1. 模块作用域WeakMap 不导出,外部代码根本引用不到
  2. WeakMap 弱引用键被回收时条目一起消失,不会因为存私有数据而泄漏
  3. # 私有字段数据挂在实例内部,只有类体里的代码能按名字访问

WeakMap 方案的「私有」只取决于作用域,同一个模块里的任何函数都能读写;# 字段则只有类体内部能访问,边界更严格。

核心回答

先记住这个答案

做法是在类所在的模块里建一个不导出的 WeakMap,构造函数里执行 privates.set(this, { ... }),方法里用 privates.get(this) 取数据。外部拿不到 privates 这个变量,所以访问不到数据。用 WeakMap 而不是 Map,是因为它对键是弱引用,实例没人用了会被正常回收,私有数据跟着消失。它的缺点是写法啰嗦、每次访问多一次查表、调试时在实例上看不到这些数据;# 私有字段在语言层面解决了这些问题,是现在的首选。

  • WeakMap 以实例为键,私有性来自模块作用域
  • 键是弱引用,实例回收后私有数据跟着回收
  • # 字段是语言级私有,类外访问直接语法错误
  • 私有字段可用 #x in obj 检查品牌

一个计数器的两种写法

同一个需求:计数器的 count 不允许外部直接改,只能通过 increment() 加。

WeakMap 版和 # 版对照JavaScript
const privates = new WeakMap() // 模块内变量,不导出

class CounterA {
  constructor() { privates.set(this, { count: 0 }) }
  increment() { return ++privates.get(this).count }
}

class CounterB {
  #count = 0
  increment() { return ++this.#count }
}

const a = new CounterA()
const b = new CounterB()
a.increment()
b.increment()
console.log(a.count, Object.keys(a).length)
console.log(b.count, Object.keys(b).length)
console.log(a.increment(), b.increment())
查看输出与解释
undefined 0
undefined 0
2 2

两个实例对外都看不到 count,Object.keys 也是空的。区别在数据放哪:CounterA 的数据在实例外面的一张表里,实例只是查表用的钥匙;CounterB 的数据就挂在实例身上,只是名字带 #,类外面的代码没法按这个名字去拿。

为什么非得是 WeakMap

换成普通 Map 也能实现私有,但会出内存问题。Map 对键是强引用:只要 Map 还在,里面的实例就永远不会被回收,哪怕业务代码早就不用它了。一个频繁创建销毁的组件用这种写法,内存会一直涨。

WeakMap 的键是弱引用,它不会阻止键被垃圾回收。实例在别处没有引用了,回收器可以直接回收它,对应的条目也随之失效。代价是 WeakMap 不能遍历、没有 size,因为条目随时可能消失。对私有数据来说这正好合适,本来也不需要遍历。

另一个限制是键只能是对象(以及未注册的 Symbol),所以这个方案只适合以实例为单位存数据。

两种方案的实际差别

WeakMap 方案出现得早,ES2015 就能用,不需要编译也能在老环境运行,Babel 早期编译 # 字段时生成的代码也正是 WeakMap。它的私有边界是模块:同一个文件里的其他函数、其他类都能读写 privates,有时这反而方便,比如两个类需要共享内部状态。

# 私有字段的边界是类体。类外面写 obj.#count 直接是语法错误,代码都跑不起来;在类里对一个没有这个字段的对象访问 #count,会抛 TypeError,而 WeakMap.get 只会安静地返回 undefined。访问速度上,# 字段是引擎直接支持的属性访问,比查一次 WeakMap 快。在 Chrome DevTools 里还能直接展开看到 #count 的值,调试体验好很多。

回答前,多想一步

容易答错的地方

以为 WeakMap 方案做到了绝对私有
它的私有只靠作用域。如果有人不小心把 privates 导出了,或者同模块的代码写错了,数据照样能被改。# 字段才是语言保证的私有。
以为 WeakMap 的值也是弱引用
弱的只有键。值是被强引用的,只是当键被回收后,这个条目整体失效,值才跟着可以被回收。如果值里反过来引用了键,也不会造成泄漏,这是 WeakMap 专门处理过的情况。
以为 # 字段能被 Proxy 透明代理
# 字段的访问检查的是对象本身,Proxy 包一层之后,方法里的 this 变成代理对象,访问 this.#count 会抛 TypeError。WeakMap 方案同样会查不到,因为代理对象不是原来那个键。
试着用自己的话回答

面试官还会怎么问?

子类能访问父类的 # 私有字段吗?

不能。# 字段只在声明它的那个类体内可见,子类里写 this.#count 是语法错误。子类想用,父类要提供方法或者改用约定的 _count 形式。WeakMap 方案如果父子类在同一个模块,倒是可以共享同一张表。

怎么判断一个对象是不是某个类真正创建的?

# 字段支持 #count in obj 这种写法,返回布尔值,不会报错,比 instanceof 可靠,因为 instanceof 只看原型链,改个原型就能骗过。WeakMap 方案对应的写法是 privates.has(obj)。

为什么不用 Symbol 做私有属性名?

Symbol 属性不会出现在 Object.keys 和 for...in 里,但 Object.getOwnPropertySymbols 能把它们全部列出来,拿到后就能随意读写。所以 Symbol 只能算「不容易撞名」,谈不上私有。

从一道题,走向一组知识

把知识连起来

值与类型

为什么 String([]) 是空字符串,而 String([1,2]) 是 '1,2'?

两题都在讲「数据藏在哪、外部看得到什么」,数组转字符串是另一个值层面的隐式行为,关联较弱。

值与类型

typeof [] 为什么是 'object'?怎么准确判断一个值是数组?

WeakMap 的键必须是对象,判断数组时同样要面对 typeof 分不清对象种类的问题。

参考资料

  • JavaScript:对象与原型参考资料

示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。

本题目录
  1. 先记住这个答案
  2. 一个计数器的两种写法
  3. 为什么非得是 WeakMap
  4. 两种方案的实际差别
  5. 容易答错的地方
  6. 面试官还会怎么问
  7. 把知识连起来
读懂,再试着讲出来

先看核心答案,再读代码。最后展开追问,检查自己有没有遗漏边界。

试着回答追问
浏览全部面试题理解原理,也关注真实的使用场景。回到顶部 ↑