在 JavaScript 中,从 ES5 之前到 ES6 之后,私有变量的变化经历了多个阶段,每种方式都有其独特的实现逻辑和局限性。
在 ES5 及更早的版本中,私有变量主要依赖于闭包或命名约定来模拟封装,但这些方法本质上并未提供真正的私有性,仍然可能被外部访问或修改。
ES6 引入了私有字段(# 语法),为 JavaScript 提供了真正的私有属性,确保数据只能在类内部访问,外部无法直接读取、修改或删除。这种方式克服了传统方法的局限,使私有变量更加安全和规范。
1. ES6 及以上的私有变量
ES6 之后,JavaScript 逐步引入了更优雅的方式来实现私有变量,以下是常见的方法:
1.1 # 私有字段(ES2020 引入)
ES2020(ES11)引入了 # 号作为私有字段标识符,使类内部的变量真正实现私有化。
class Animal {
#secret = 'I am private'; // 私有字段
constructor(name) {
this.name = name;
}
getSecret() {
return this.#secret;
}
setSecret(newSecret) {
this.#secret = newSecret;
}
}
const dog = new Animal('Dog');
console.log(dog.getSecret()); // 'I am private',可以通过 getter 方法访问
console.log(dog.#secret); // SyntaxError: Private field '#secret' must be declared in an enclosing class
特点:
- 私有字段不是普通对象属性,而是类的私有内部属性,只能在类内部访问,外部无法访问。
- 不能被子类继承。
- 无法通过 Object.keys() 或 Object.getOwnPropertyNames() 获取,保证私有性。
注意事项:
1、私有字段不会作为普通键值对存储,不能用 this['#secret'] 访问
2、JSON.stringify() 不能序列化 # 私有字段
console.log(dog['#secret']); // undefined,无法直接通过 this['#secret'] 访问
console.log(JSON.stringify(dog)); // {"name":"Dog"},#secret 不会出现在序列化结果中
为什么 JSON.stringify() 不能序列化 #secret?
- JSON.stringify() 只能序列化对象的可枚举属性,而 #secret 不会成为对象的可枚举属性,因此它不会出现在 JSON 结果里。
- 即使尝试 Object.assign({}, dog),#secret 依然不会被拷贝,因为它不属于实例的可遍历属性。
console.log(Object.keys(dog)); // ['name'],没有 #secret
console.log(Object.getOwnPropertyNames(dog)); // ['name'],没有 #secret
console.log(Object.assign({}, dog)); // { name: 'Dog' },没有 #secret
如果希望实现序列化,可以手动实现 toJSON 方法:
class Animal {
#secret = 'I am private';
constructor(name) {
this.name = name;
}
toJSON() {
return {
name: this.name,
secret: this.#secret, // 手动添加私有字段
};
}
}
const cat = new Animal('Cat');
console.log(JSON.stringify(cat)); // {"name":"Cat","secret":"I am private"}
3、不能使用 delete 删除私有字段
普通属性可以用 delete 删除,但私有字段不行:
class Animal {
#secret = 'Hidden';
deleteSecret() {
delete this.#secret; // ❌ SyntaxError: Private fields can not be deleted
}
}
如果想“清空”私有字段,可以提供 setter 方法来修改其值,而不是尝试删除它。
4、in 关键字可以检查私有字段是否存在
虽然 Object.hasOwnProperty() 不能检测 #secret,但 in 关键字可以:
class Animal {
#secret = 'Hidden';
name = 'dog';
}
const dog = new Animal();
console.log('#secret' in dog); // false,普通方式无法检测
console.log(Object.hasOwn(dog, '#secret')); // false
console.log(Object.hasOwn(dog, 'name')); // true
// 但是 in 可以在类内部使用:
class Check {
#hidden = 42;
hasSecret() {
return #hidden in this; // ✅ true
}
}
console.log(new Check().hasSecret()); // true
1.2 使用 WeakMap 存储私有变量
在 # 私有字段之前,WeakMap 也是一种实现私有属性的技巧。
const privateData = new WeakMap();
class Animal {
constructor(name) {
this.name = name;
privateData.set(this, { secret: 'I am private' });
}
getSecret() {
return privateData.get(this).secret;
}
setSecret(newSecret) {
privateData.get(this).secret = newSecret;
}
}
const dog = new Animal('Dog');
console.log(dog.getSecret()); // ✅ "I am private"
console.log(dog.secret); // ❌ undefined(无法访问)
特点:
- 变量存储在 WeakMap 中,外部无法直接访问。
- 不影响对象原型链,避免 # 私有字段无法继承的问题。
- 自动垃圾回收,不需要手动删除。
缺点:写法较麻烦,每个实例访问时需要 WeakMap.get()。
1.3 使用 Symbol 伪私有属性
可以用 Symbol 创建独一无二的属性名,外部不易访问,但不能真正做到私有化。
const _secret = Symbol('secret');
class Animal {
constructor(name) {
this.name = name;
this[_secret] = 'I am private';
}
getSecret() {
return this[_secret];
}
}
const dog = new Animal('Dog');
console.log(dog.getSecret()); // ✅ "I am private"
console.log(dog[_secret]); // ✅ 仍然可以访问(不是真正的私有)
console.log(Object.keys(dog)); // ✅ ["name"](Symbol 不会被枚举)
console.log(Object.getOwnPropertyNames(dog)); // ✅ ["name"](Symbol 属性不会被返回)
特点:
- 外部可以通过 Object.getOwnPropertySymbols() 找到 Symbol,所以只是伪私有。
- 不会出现在 Object.keys() 和 JSON.stringify() 结果中,比普通属性更难被发现。
- 仍然可以通过 dog[Object.getOwnPropertySymbols(dog)[0]] 访问。
console.log(Object.getOwnPropertySymbols(dog)); // [ Symbol(secret) ]
console.log(dog[Object.getOwnPropertySymbols(dog)[0]]); // I am private
2. ES5 及更早版本的私有变量
在 ES6 之前,没有 # 私有字段,通常使用以下方法模拟私有变量。
2.1 作用域与闭包
闭包是 JavaScript 在 ES3 时代就已经存在的一个强大机制,用来封装变量并防止外部访问。
方法 1:立即执行函数(IIFE)+ 闭包
IIFE(Immediately Invoked Function Expression)立即执行函数创建了一个独立作用域,使外部无法访问内部变量。
var Animal = (function () {
var privateData = {}; // 私有存储对象
function Animal(name) {
this.name = name;
privateData[this.name] = { secret: 'I am private' };
}
Animal.prototype.getSecret = function () {
return privateData[this.name].secret;
};
return Animal;
})();
const dog = new Animal('Dog');
console.log(dog.getSecret()); // ✅ "I am private"
console.log(dog.secret); // ❌ undefined(外部无法访问)
特点:
- privateData 变量存储在 IIFE 作用域内,外部无法访问。
- 但 每个实例都使用 privateData 存储,会导致数据泄露风险。
或者结合 WeakMap
var Animal = (function () {
var secret = new WeakMap();
function Animal(name) {
this.name = name;
secret.set(this, 'I am private');
}
Animal.prototype.getSecret = function () {
return secret.get(this);
};
return Animal;
})();
const dog = new Animal('Dog');
console.log(dog.getSecret()); // ✅ "I am private"
console.log(dog.secret); // ❌ undefined
特点:
- 结合 IIFE 和 WeakMap,避免私有数据污染全局作用域。
- 但 WeakMap 仍然需要额外的存储开销。
方法 2:构造函数中的局部变量
可以在构造函数内部声明局部变量,使其无法被外部访问。
function Animal(name) {
let secret = 'I am private'; // 局部变量,外部无法访问
this.name = name;
this.getSecret = function () {
return secret;
};
this.setSecret = function (newSecret) {
secret = newSecret;
};
}
const dog = new Animal('Dog');
console.log(dog.getSecret()); // ✅ 'I am private'
console.log(dog.secret); // ❌ undefined
特点:
- secret 变量作用域只在构造函数内部,不会污染全局。
- 但每个实例都会创建一份新的 secret,不共享,导致内存占用增加。
2.2 借助 JavaScript 特性
在 ES3/ES5 时代,可以利用 this、prototype、Object.defineProperty() 进行私有变量的模拟。
方法 3:this 作用域技巧
由于 JavaScript 的 this 关键字在不同作用域下行为不同,可以利用它来创建受保护的私有变量。
function Animal(name) {
this.name = name;
this.getSecret = (function () {
var secret = 'I am private'; // 局部作用域
return function () {
return secret;
};
})();
}
const dog = new Animal('Dog');
console.log(dog.getSecret()); // ✅ "I am private"
console.log(dog.secret); // ❌ undefined
特点:
- 避免了多个实例共享同一变量的问题。
- 但无法更改 secret,因为 secret 变量的作用域被封闭。
方法 4:prototype 属性屏蔽
利用 prototype 让实例无法访问私有变量。
function Animal(name) {
var secret = "I am private"; // 作用域限制
this.name = name;
this.getSecret = function () {
return secret;
};
}
Animal.prototype.secret = "public"; // 在原型链上定义一个伪公有变量
const dog = new Animal("Dog");
console.log(dog.getSecret()); // ✅ "I am private"
console.log(dog.secret); // ✅ "public"(实例不会访问私有变量)
特点:原型链上的 secret 无法影响 getSecret 的作用域。
2.3 借助对象与命名约定
方法 5:_ 伪私有变量
有些开发者使用 _ 作为变量前缀,表明该变量不应被外部直接访问。
function Animal(name) {
this.name = name;
this._secret = 'I am private'; // 仅作标记
}
const dog = new Animal('Dog');
console.log(dog._secret); // ❌ 仍然可以访问(只是约定)
特点:仅仅是一种命名约定,并不是真正的私有变量。
3. 注意点
3.1 private vs #
private 和 #(私有字段)都用于定义类的私有属性,但它们有本质区别:
1. private(TypeScript 专属)
- 仅在 TypeScript 编译时 进行访问限制,运行时仍然是公有属性,可通过 obj["privateProp"] 访问。
- 仅用于 TypeScript 语法检查,不会影响 JavaScript 代码的实际执行。
- 不能被子类继承,也不能访问,它只在定义它的类内部可用。
class Parent {
#secret = "I am private";
showSecret() {
console.log(this['#secret']); // ✅ 父类内部可以访问
}
}
class Child extends Parent {
showParentSecret() {
console.log(this.secret); // ❌ 属性“secret”为私有属性,只能在类“Parent”中访问。
}
}
const p = new Parent();
console.log(p.showSecret); // [Function: showSecret]
const c = new Child();
console.log(c.secret); // ❌ 报错:Property 'secret' is private
2. #(ES6+ 私有字段)
真正的 JavaScript 级别的私有字段,无法从外部访问,也不能使用 this["#xxx"] 读取。