从 ES5 到 ES6 解锁 JavaScript 私有变量

在 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?

  1. JSON.stringify() 只能序列化对象的可枚举属性,而 #secret 不会成为对象的可枚举属性,因此它不会出现在 JSON 结果里。
  2. 即使尝试 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"] 读取。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值