JavaScript -- 私有属性如何实现

在 JavaScript 中,实现类的私有属性有多种解决方案

1. 闭包与函数作用域(基于 ES5 及之前的方式)

  • 基本原理
    利用函数内部形成的闭包来隐藏变量,使其不能从外部直接访问,从而模拟私有属性的效果。在构造函数中定义变量,并通过特权方法(在构造函数中定义,且可以访问内部变量的方法)来操作这些变量,外部只能通过调用这些特权方法来间接与内部变量交互。
  • 示例代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Person(name) {
let age; // 模拟私有属性,外部无法直接访问
this.getName = function () {
return name;
};
this.setAge = function (newAge) {
age = newAge;
};
this.getAge = function () {
return age;
};
}

const person = new Person('John');
person.setAge(30);
console.log(person.getName()); // 输出 'John'
console.log(person.getAge()); // 输出 30
console.log(person.age); // 输出 undefined,无法直接访问 age 属性

在上述代码中,age 变量在 Person 构造函数内部定义,外部无法直接获取或修改它,只能通过 setAgegetAge 等特权方法来操作,这样就实现了类似私有属性的功能,但这种方式的缺点是每个实例都会创建一套自己的特权方法,占用较多内存,不够高效。

2. Symbol(ES6 引入)

  • 基本原理
    Symbol 是一种基本数据类型,它的值是唯一的,可以用作对象的属性名,并且不会与其他常规属性名冲突。通过将私有属性的键定义为 Symbol,可以在一定程度上隐藏属性,使其不太容易被外部随意访问,但并非真正严格意义上的私有,因为知晓 Symbol 值的代码依然可以访问该属性。
  • 示例代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const _age = Symbol('age'); // 创建一个Symbol作为私有属性的键

class Person {
constructor(name) {
this.name = name;
this[_age] = 0; // 使用Symbol作为属性名来设置私有属性
}
getAge() {
return this[_age];
}
setAge(newAge) {
this[_age] = newAge;
}
}

const person = new Person('Alice');
person.setAge(25);
console.log(person.getAge()); // 输出 25
console.log(person[_age]); // 会报错,因为无法直接通过Symbol访问,除非获取到具体的Symbol值

使用 Symbol 可以一定程度上隐藏属性,但它主要是利用了 Symbol 的唯一性和不易被外部知晓的特点,在安全性要求不是极高的场景下可以作为一种实现私有属性感觉的方式。

3. WeakMap(ES6 引入)

  • 基本原理
    基于 WeakMap 对键的弱引用特性以及不可枚举性来实现类的私有属性。以类的实例作为 WeakMap 的键,私有属性的值作为对应的值存储在 WeakMap 中,这样外部无法直接通过实例对象查看到私有属性,并且当实例对象不再被引用时,对应的私有属性相关数据也能被垃圾回收机制合理回收。
  • 示例代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const privateData = new WeakMap(); // 创建WeakMap来存储私有数据

class Person {
constructor(name) {
this.name = name;
privateData.set(this, { age: 0 }); // 将实例和对应的私有属性数据存入WeakMap
}
getAge() {
return privateData.get(this).age;
}
setAge(newAge) {
privateData.get(this).age = newAge;
}
}

const person = new Person('Bob');
person.setAge(35);
console.log(person.getAge()); // 输出 35
// 无法直接从实例对象上查看到私有属性相关信息

这种方式利用了 WeakMap 的特性,相对来说更符合私有属性的语义,且在内存管理方面有优势,不过代码逻辑上会稍微复杂一点,需要额外维护 WeakMap 这个数据结构。

4. 类字段声明的私有属性(ES2022 引入)

  • 基本原理
    在类的定义中,使用 # 前缀来声明私有属性,这是 JavaScript 语言标准中正式支持的私有属性语法,通过这种方式声明的私有属性,外部无法直接访问,并且在类的内部可以正常使用,语法上更加直观、简洁,符合面向对象编程中私有属性的规范定义。
  • 示例代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Person {
#age; // 使用 # 前缀声明私有属性
constructor(name) {
this.name = name;
this.#age = 0;
}
getAge() {
return this.#age;
}
setAge(newAge) {
this.#age = newAge;
}
}

const person = new Person('Eve');
person.setAge(28);
console.log(person.getAge()); // 输出 28
console.log(person.#age); // 会报错,无法直接访问私有属性

这种方式是目前最新且最符合私有属性语义的实现方式,代码可读性好,在现代 JavaScript 开发中,如果支持相应的 ES2022 语法环境,建议优先采用这种方式来定义类的私有属性。

总之,不同的解决方案各有优劣,开发者可以根据项目所使用的 JavaScript 版本、对私有属性安全性的要求以及代码可读性等方面的考量,选择合适的方式来实现类的私有属性。

参考

MDN 私有属性

注意

  1. 兼容性问题:闭包与函数作用域的方式在 ES5 及之前就可以使用,兼容性较好,但代码风格相对较旧且效率不高;SymbolWeakMap 方式依赖于 ES6 及之后的特性,在一些老旧浏览器或者不支持相应特性的 JavaScript 运行环境中可能无法正常使用,需要进行兼容性测试和处理;而类字段声明的私有属性方式更是依赖于 ES2022 语法,目前部分较旧的环境可能还未支持,使用时要充分考虑项目的部署环境对语法的支持情况。
  2. 代码维护与可读性:闭包方式的代码相对较复杂,每个实例都有独立的特权方法,随着代码量增加,维护成本较高且可读性欠佳;Symbol 方式如果在项目中大量使用,需要注意 Symbol 值的管理,避免出现混乱;WeakMap 方式涉及额外的数据结构维护,逻辑上稍复杂一些;而类字段声明的私有属性方式虽然简洁直观,但对于不熟悉新语法的开发者来说可能需要一定的学习成本,在团队协作中要确保成员对相应语法都能理解和运用,以保障代码的可维护性。
  3. 安全性并非绝对:尽管使用各种方式来实现私有属性,但在 JavaScript 中,由于其动态语言的特性,通过一些特殊手段(比如修改对象的原型、利用反射机制等,虽然这些在常规开发中不常见但理论上可行),依然有可能访问到所谓的 “私有属性”,所以不能将其视为绝对安全的,在涉及高安全性需求的场景下,可能需要结合其他安全机制(如代码混淆、加密等)来进一步保障数据的安全性。

JavaScript -- 私有属性如何实现
http://example.com/2023/09/12/JavaScript-私有属性如何实现-copy/
作者
lyric
发布于
2023年9月12日
许可协议