JavaScript实现私有属性的几种方式小结
作者:刘同学有点忙
什么是私有属性
我们不抠定义,用大白话来说,如果类中的某个属性只能在类内部使用,在类外部(比如通过类的实例)访问不到,这个属性就是私有属性。
我们用下面的代码举例说明,当然了,代码中的属性并不是私有属性,只是为了说明私有属性是怎么一回事:
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私有属性的资料请关注脚本之家其它相关文章!