本文总结JavaScript中实现私有属性的多种方案。从早期的下划线命名约定、闭包技巧,到结合ES模块的变量隐藏,再到基于Symbol和TypeScript private修饰符的尝试。重点解析ES2022正式引入的#私有字段语法,以及为兼容旧环境使用WeakMap的实现方式,对比各方案的优缺点与适用场景。
私有属性的定义
通俗而言,若某属性仅限类内部调用,外部(如实例)无法访问,即视为私有属性。
以下代码仅作示例,其中属性并非真正的私有属性,旨在阐释私有属性的概念:
class Person {
name = 'name' // 我们假设name是一个私有属性,当然,它现在不是
getName() {
return this.name // 类内部可以访问私有属性
}
}
const person = new Person()
person.getName() // 'name'
person.name // 尝试直接在类外部访问私有属性会报错
实现路径
早期JavaScript缺乏原生私有属性支持,需借助变通手段实现。
基于命名规范的弱约束
一种策略是通过命名规范进行约束,约定以下划线开头的属性标识为私有。
class Person {
_name = 'name' // 约定下划线开头的属性是私有属性
getName() {
return this._name // 类内部可以访问私有属性
}
}
const person = new Person()
person._name // 开发者可以不遵守命名规范,运行时在类外部访问完全没问题
Vue源码中大量使用下划线前缀来命名属性和变量。

然而这仅是弱约束,运行时仍可在类外部直接访问这些属性,无任何限制。
基于闭包机制
function Person(){
const name = 'name'
this.getName = function(){
return name
}
}
const person = new Person()
person.getName() // 'name'
person.name // undefined
上述代码中getName函数引用了Person函数的词法环境,借助闭包特性达成私有属性效果。私有属性name在Person外部不可见,仅能通过特权方法getName访问。
此方法的局限性同样显著:
私有属性与特权方法均须在构造函数内定义,且方法未挂载至原型,每实例化一个对象即生成一次方法副本。
结合ES模块将私有属性移至类外部
const name = 'name'
export class Person {
getName() {
return name
}
}
前述代码中,ES模块仅导出类,未导出类外变量name,使得类可访问变量name,而外部无法触及。
const person = new Person()
person.getName() // 'name'
person.name // undefined
基于Symbol类型
const name = Symbol('name')
export class Person {
[name] = 'name'
getName() {
return this[name]
}
}
上述代码利用变量存储Symbol值,在类内部通过动态属性方式添加私有属性。同样依赖ES模块仅导出类而不导出Symbol,致使外部无法获取Symbol值以访问私有属性。
const person = new Person()
person.getName() // 'name'
然而,仍存在获取该Symbol值的途径。
const symbols = Object.getOwnPropertySymbols(person)
person[symbols[0])
因此,此方式并未实现绝对的私有化。
TypeScript中的private关键字
TypeScript提供了private修饰符,尝试使用此方式是否可行?
class Person {
private name = 'name'
getName() {
return name
}
}
const person = new Person()
person.getName() // 'name'
person.name // 编译时错误 Property 'name' is private and only accessible within class 'Person'.
在TypeScript中,于类外部访问private属性会在编译阶段报错,看似理想。但需铭记TypeScript最终需编译为JavaScript,观察编译结果:

编译为JavaScript后,private修饰符消失。若通过动态属性绕过编译时的类型检查,编译后的JavaScript代码在运行时不会报错:
person['name'] // 'name'

ES2022标准
ES2022正式引入私有属性,在属性名前添加#前缀以表示私有。
class Person {
#name = 'name'
getName() {
return this.#name
}
}
const person = new Person()
person.getName() // 'name'
person.#name // Uncaught SyntaxError: Private field '#name' must be declared in an enclosing class
若将上述代码置于Chrome控制台执行,或许会发现person.#name仍可访问值。自Chrome111版本起,开发者工具允许读写私有属性且不报错,原因是Chrome团队认为此举便于调试。
查阅MDN以获取更多关于私有属性的信息。
利用WeakMap解决兼容性问题
若需兼顾ES2022之前的环境兼容性,可采用WeakMap方案。
const privateFields = new WeakMap()
export class Person {
constructor() {
privateFields.set(this, {
name: 'name'
})
}
getName() {
return privateFields.get(this).name
}
}
前述代码在类外部维护一个weakMap,并在constructor中向weakMap绑定实例this与{name: 'name'}的映射关系。访问时同样通过this从weakMap中检索name。
同样得益于ES模块特性,模块外部无法访问weakMap,自然也就无法触及私有属性。
const person = new Person()
person.getName() // 'name'
然而此写法存在缺点,即代码繁琐且不够直观。实际上,ES2022私有属性方案经babel编译后的代码大致与此类似。

可见babel编译后的代码量显著增加,因需确保程序健壮性,必须考虑诸多边缘场景。仅观察红框标注代码亦可看出,编译后确实采用了WeakMap方案。
总结
本文归纳了JavaScript中实现私有属性的多种途径。ES2022引入的正式写法无疑是正规军,且代码简洁。若需考虑兼容性,WeakMap方案确实保障了私有性,尽管写法稍显繁琐。其他方案在健壮性上各有不足,了解即可。
以上为JavaScript实现私有属性的几种方式小结的详细内容,更多关于JavaScript私有属性的资料请关注本站其它相关文章!