JavaScript ES6类中的私有属性

564

在ES6类中是否可以创建私有属性?

以下是一个例子。 如何防止访问instance.property

class Something {
  constructor(){
    this.property = "test";
  }
}

var instance = new Something();
console.log(instance.property); //=> "test"

7
实际上,针对这个功能有第三阶段的提案- https://tc39.github.io/proposal-class-fields/ https://github.com/tc39/proposal-class-fields - arty
@arty 我已经提供了带有示例的答案:https://dev59.com/CGEh5IYBdhLWcg3wrVAN#52237988 - Alister
1
所有针对私有属性/方法的解决方案,包括ES5 / ES6+,都无法提供真正的隐私,因为基于Chrome的浏览器始终可以在任何原型的[Scopes]对象中显示整个执行上下文。有些东西必须在浏览器范围之外编码。在我的测试中,没有一种方法可以向Chrome隐藏任何内容。 - thednp
41个回答

347

私有类特性 现在被大多数浏览器支持。

class Something {
  #property;

  constructor(){
    this.#property = "test";
  }

  #privateMethod() {
    return 'hello world';
  }

  getPrivateMessage() {
      return this.#property;
  }
}

const instance = new Something();
console.log(instance.property); //=> undefined
console.log(instance.privateMethod); //=> undefined
console.log(instance.getPrivateMessage()); //=> test
console.log(instance.#property); //=> Syntax error

8
那么eslint呢?我在等号处收到了解析器错误。Babel能用,只是eslint无法解析这种新的js语法。 - martonx
19
哇,这太丑了。哈希标签是一个有效的字符。这个属性并不真正私有,对吗?我在 TypeScript 中检查了一下。私有成员并没有编译为私有或只读(从外部)。它们只是像另一个(公共)属性一样声明。(ES5) - Domske
3
你如何使用这个写私有方法?我可以这样做吗:#beep() {}; 和这样做:async #bzzzt() {} - Константин Ван
4
如果你的意思是JavaScript根本不需要“私有”的私有属性,那么使用“_”将会导致重大变更。请注意,这是一种翻译方式,并且我已经尽力保持原意和易读性,没有添加任何解释或其他内容。 - decorator-factory
1
这已经在完成的提案规范中了。 - Suraj Rao
显示剩余15条评论

324

更新:请查看其他答案,本回答已过时。

简短回答,ES6类没有原生支持私有属性。

但你可以通过将新属性不附加到对象,而是保留它们在类构造函数中,并使用getter和setter来访问隐藏属性来模拟该行为。注意,每个类实例上的getter和setter都会重新定义。

ES6

class Person {
    constructor(name) {
        var _name = name
        this.setName = function(name) { _name = name; }
        this.getName = function() { return _name; }
    }
}

ES5

function Person(name) {
    var _name = name
    this.setName = function(name) { _name = name; }
    this.getName = function() { return _name; }
}

3
我最喜欢这个解决方案。我同意它不适用于扩展,但对于通常只会被实例化一次的类非常完美。 - Blake Regalia
2
每��创建新对象时,您都在重新定义该类的每个组件。 - Quentin Roy
13
这太奇怪了!在 ES6 中,比 ES5 更多地创建了“闭包金字塔”!在构造函数内部定义函数比上面的 ES5 示例看起来更丑。 - Kokodoko
20
这只是引入了间接性。现在如何将getNamesetName属性设为私有? - aij
3
@aij,那你说出一种不这样做的语言。你可以很容易地看到他可以注释掉setter或getter或两者都注释掉,而_name确实是私有的。 - Stijn de Witt
显示剩余9条评论

265
是的,在类定义中加上#前缀并包含在其中,而不仅仅是在构造函数中。 MDN 文档 真正的私有属性终于在 ES2022 中被添加了。截至 2023 年 01 月 01 日,所有主流浏览器都已经支持私有属性(字段和方法)超过一年,但仍有 5-10% 的用户使用旧版浏览器 [Can I Use]。
示例:
class Person {
  #age

  constructor(name) {
    this.name = name; // this is public
    this.#age = 20; // this is private
  }

  greet() {
    // here we can access both name and age
    console.log(`name: ${this.name}, age: ${this.#age}`);
  }
}

let joe = new Person('Joe');
joe.greet();

// here we can access name but not age

以下是在ES2022之前保持属性私有的方法,具有各种权衡取舍。
作用域变量
这种方法是使用构造函数的作用域来存储私有数据。为了使方法能够访问此私有数据,它们必须在构造函数中创建,这意味着您需要针对每个实例重新创建它们。这会带来性能和内存开销,但这可能是可以接受的。对于不需要访问私有数据的方法,可以按照正常方式声明以避免开销。
示例:
class Person {
  constructor(name) {
    let age = 20; // this is private
    this.name = name; // this is public

    this.greet = () => {
      // here we can access both name and age
      console.log(`name: ${this.name}, age: ${age}`);
    };
  }

  anotherMethod() {
    // here we can access name but not age
  }
}

let joe = new Person('Joe');
joe.greet();

// here we can access name but not age

作用域 WeakMap

为了提高性能,可以使用 WeakMap 来改进上述方法,但会增加更多的混乱。WeakMaps 将数据与对象(这里是类实例)关联起来,以便只能使用该 WeakMap 访问它。因此,我们使用作用域变量方法创建一个私有 WeakMap,然后使用该 WeakMap 来检索与this相关联的私有数据。这比作用域变量方法更快,因为所有实例都可以共享一个单独的 WeakMap,因此您不需要重新创建方法来使它们访问自己的 WeakMaps。

示例:

let Person = (function () {
  let privateProps = new WeakMap();

  return class Person {
    constructor(name) {
      this.name = name; // this is public
      privateProps.set(this, {age: 20}); // this is private
    }

    greet() {
      // Here we can access both name and age
      console.log(`name: ${this.name}, age: ${privateProps.get(this).age}`);
    }
  };
})();

let joe = new Person('Joe');
joe.greet();

// here we can access name but not age

这个示例使用具有Object键的WeakMap来使用一个WeakMap用于多个私有属性;你也可以使用多个WeakMaps并像privateAge.set(this, 20)那样使用它们,或者编写一个小包装器并以另一种方式使用它,比如privateProps.set(this, 'age', 0)
这种方法的隐私理论上可能会被破坏,因为全局WeakMap对象可能会被篡改。话虽如此,所有JavaScript都可以被破坏。
(这种方法也可以使用Map完成,但WeakMap更好,因为Map会创建内存泄漏,除非你非常小心,而且对于这个目的,两者没有其他区别。)
半答案:作用域符号
符号是一种原始值类型,可以作为属性名而不是字符串。您可以使用作用域变量方法创建一个私有符号,然后将私有数据存储在this[mySymbol]中。
这种方法的隐私性可以通过使用Object.getOwnPropertySymbols来突破,但有些棘手。
示例:
let Person = (() => {
  let ageKey = Symbol();

  return class Person {
    constructor(name) {
      this.name = name; // this is public
      this[ageKey] = 20; // this is intended to be private
    }

    greet() {
      // Here we can access both name and age
      console.log(`name: ${this.name}, age: ${this[ageKey]}`);
    }
  }
})();

let joe = new Person('Joe');
joe.greet();

// Here we can access joe's name and, with a little effort, age. We can’t
// access ageKey directly, but we can obtain it by listing all Symbol
// properties on `joe` with `Object.getOwnPropertySymbols(joe)`.

注意,使用Object.defineProperty将属性设置为不可枚举并不能阻止它被包含在Object.getOwnPropertySymbols中。
半个答案:下划线
旧的约定是只使用具有下划线前缀的公共属性。这并不能使其私有化,但它确实可以很好地向读者传达应将其视为私有属性,这通常已经足够。作为交换,我们得到了一种更易于阅读、输入和比其他变通方法更快的方法。
示例:
class Person {
  constructor(name) {
    this.name = name; // this is public
    this._age = 20; // this is intended to be private
  }

  greet() {
    // Here we can access both name and age
    console.log(`name: ${this.name}, age: ${this._age}`);
  }
}

let joe = new Person('Joe');
joe.greet();

// Here we can access both joe's name and age. But we know we aren't
// supposed to access his age, which just might stop us.

摘要

  • ES2022:非常好,但并不被所有访问者支持
  • 作用域变量:私有、较慢、笨拙
  • 作用域WeakMaps:可被篡改、笨拙
  • 作用域Symbols:可枚举且可篡改,有点笨拙
  • 下划线:只是一个隐私请求,没有其他缺点

8
第一个示例代码片段(“作用域变量”)是一个完全的反模式 - 每个返回的对象都将具有不同的类。不要这样做。如果您想要特权方法,请在构造函数中创建它们。 - Bergi
1
将一个类包装在函数中似乎违背了使用类的初衷。如果您已经使用函数创建实例,那么您可以将所有私有/公共成员放在该函数中,并忘记整个类关键字。 - Kokodoko
3
@Bergi @Kokodoko 我修改了作用域变量的方法,使其稍微快一些,并且不会破坏 instanceof。我承认我之前认为这种方法只是为了完整性而包含的,应该更多地考虑它实际上能做到什么。 - twhb
2
非常好的解释!我仍然感到惊讶的是,ES6实际上使模拟私有变量更加困难,在ES5中,您只需在函数内使用var和this来模拟私有和公共。 - Kokodoko
2
@Kokodoko 如果你放弃使用类,把所有东西都放在函数里,那么你也必须回到使用原型方法来实现继承。在类上使用extend远比这种方式更加清晰,因此在函数内部使用类是完全可以接受的。 - Johann
显示剩余8条评论

123
更新:有一份 更好的语法建议 正在路上,欢迎贡献。

是的,有一个方法可以在对象中实现作用域访问 - ES6引入了Symbol

Symbol是独一无二的,除非使用反射(类似于Java/C#中的私有成员),否则无法从外部访问它们,但任何可以访问内部符号的人都可以将其用于键访问:

var property = Symbol();
class Something {
    constructor(){
        this[property] = "test";
    }
}

var instance = new Something();

console.log(instance.property); //=> undefined, can only access with access to the Symbol

8
你可以使用Object.getOwnPropertySymbols吗? ;) - Qantas 94 Heavy
42
据@BenjaminGruenbaum所说,符号似乎不能确保真正的隐私:https://dev59.com/tmEi5IYBdhLWcg3wCoQp#22280202 - d13
33
通过三个键吗?不是。通过符号吗?是的。这很像在C#和Java等语言中使用反射来访问私有字段。访问修饰符并不关乎安全性,而是关于意图的清晰度。 - Benjamin Gruenbaum
10
使用符号似乎类似于使用“const myPrivateMethod = Math.random(); Something.prototype[''+myPrivateMethod] = function () { ... } new Something()''+myPrivateMethod;”。这并不是真正的隐私,而是传统JavaScript意义上的模糊性。我认为JavaScript中的“私有”应该是指使用闭包封装变量,因此这些变量无法通过反射访问。 - trusktr
13
我认为使用privateprotected关键词比使用SymbolName更加简洁明了。我更喜欢用点符号表示法而非方括号表示法。我希望继续使用点来表示私有成员,例如this.privateVar - trusktr
显示剩余14条评论

35
答案是否定的。但是你可以这样创建私有访问属性:

(建议使用符号来确保隐私在ES6规范的早期版本中是正确的,但现在不再是这种情况:https://mail.mozilla.org/pipermail/es-discuss/2014-January/035604.htmlhttps://dev59.com/tmEi5IYBdhLWcg3wCoQp#22280202。有关符号和隐私的更长讨论,请参见:https://curiosity-driven.org/private-properties-in-javascript


6
-1,这并没有真正回答你的问题。(你也可以在ES5中使用带立即调用的闭包)。在大多数语言中(如Java、C#等),私有属性都可以通过反射枚举。私有属性的目的是向其他程序员传达意图,而不是强制执行安全性。 - Benjamin Gruenbaum
1
@BenjaminGruenbaum,我知道,我希望我有一个更好的答案,我也不满意。 - d13
我认为符号仍然是在编程环境中实现不可访问成员的有效方式。是的,如果你真的想找到它们,它们仍然可以被找到,但这不是重点,对吧?你不应该在其中存储敏感信息,但无论如何,在客户端代码中都不应该这样做。但是,它适用于从外部类隐藏属性或方法的目的。 - Kokodoko
1
在类中使用模块级别作用域的变量来替代私有属性会导致单例行为或类似于静态属性的行为。变量的实例将被共享。 - Adrian Moisa

30

在JS中获得真正的隐私的唯一方法是通过作用域,因此没有办法拥有一个只能在组件内部访问的this成员属性。使用WeakMap是在ES6中存储真正私有数据的最佳方式。

const privateProp1 = new WeakMap();
const privateProp2 = new WeakMap();

class SomeClass {
  constructor() {
    privateProp1.set(this, "I am Private1");
    privateProp2.set(this, "I am Private2");

    this.publicVar = "I am public";
    this.publicMethod = () => {
      console.log(privateProp1.get(this), privateProp2.get(this))
    };        
  }

  printPrivate() {
    console.log(privateProp1.get(this));
  }
}

显然,这可能会很慢,而且肯定很丑,但它确实提供了隐私。

请记住,甚至这也不完美,因为Javascript非常动态。某人仍然可以做到

var oldSet = WeakMap.prototype.set;
WeakMap.prototype.set = function(key, value){
    // Store 'this', 'key', and 'value'
    return oldSet.call(this, key, value);
};

为了在值存储时捕获它们,因此如果您想要非常小心,您需要捕获对 .set.get 的本地引用,以显式地使用,而不是依赖于可重载的原型。

const {set: WMSet, get: WMGet} = WeakMap.prototype;

const privateProp1 = new WeakMap();
const privateProp2 = new WeakMap();

class SomeClass {
  constructor() {
    WMSet.call(privateProp1, this, "I am Private1");
    WMSet.call(privateProp2, this, "I am Private2");

    this.publicVar = "I am public";
    this.publicMethod = () => {
      console.log(WMGet.call(privateProp1, this), WMGet.call(privateProp2, this))
    };        
  }

  printPrivate() {
    console.log(WMGet.call(privateProp1, this));
  }
}

3
作为建议,您可以通过将对象作为值来避免使用每个属性一个弱映射。这样,您还可以将每个方法中的地图“获取”次数减少到一个(例如 const _ = privates.get(this); console.log(_.privateProp1); )。 - Quentin Roy
是的,这也完全是一个选项。我主要选择这个是因为它更直接地映射到用户在使用真实属性时所编写的内容。 - loganfsmyth
@loganfsmyth const myObj = new SomeClass(); console.log(privateProp1.get(myObj)) // "I am Private1" 这意味着你的属性是私有的还是公共的? - stackoverflow
2
为了使其正常工作,访问该属性的代码需要访问WeakMap对象,该对象通常被限定在模块内部且无法访问。 - loganfsmyth

22

供其他人参考,我现在了解到建议使用WeakMaps来保存私有数据。

以下是一个更加清晰、可行的示例:

function storePrivateProperties(a, b, c, d) {
  let privateData = new WeakMap;
  // unique object as key, weak map can only accept object as key, when key is no longer referened, garbage collector claims the key-value 
  let keyA = {}, keyB = {}, keyC = {}, keyD = {};

  privateData.set(keyA, a);
  privateData.set(keyB, b);
  privateData.set(keyC, c);
  privateData.set(keyD, d);

  return {
    logPrivateKey(key) {
      switch(key) {
      case "a":
        console.log(privateData.get(keyA));
        break;
      case "b":
        console.log(privateData.get(keyB));
        break;
      case "c":
        console.log(privateData.get(keyC));
        break;
      case "d":
        console.log(privateData.set(keyD));
        break;
      default:
        console.log(`There is no value for ${key}`)
      }
    }
  }
}

20
注意这些属性是静态的。 - Michael Theriot
8
我没有给你的帖子点踩,但是你的WeakMap示例完全错误。 - Benjamin Gruenbaum
4
换句话说,你将数据在所有类实例之间共享,而不是每个实例单独使用。我可以至少修复这个问题吗? - Benjamin Gruenbaum
1
确实,WeakMap需要附加到给定的实例上。请参见http://fitzgeraldnick.com/weblog/53/获取示例。 - widged
2
根据MDN的文档,诸如Symbol等原始数据类型不允许作为WeakMap的键。MDN WeakMap文档 - leepowell
显示剩余4条评论

12

3
私有名称极有可能不会出现在ES6中,尽管他们正在考虑ES7的某种私有形式。请注意,该翻译保留原始语句的含义,同时力求通俗易懂。 - Qantas 94 Heavy
据我所知,私有名称和唯一字符串值都已被符号取代。@Qantas94Heavy - Benjamin Gruenbaum
是的,它可能会变成符号。然而,据我所知,规范中当前包含的“符号”仅用于描述内部属性,例如[[prototype]],并且没有办法在用户代码中创建和使用它们。你知道一些文档吗? - Bergi
它们符合最新的规格,你可以在Jason的副本中找到它们。 - Benjamin Gruenbaum
1
@Cody:在ES6中,你的整个模块代码都有自己的作用域,不需要IEFE。是的,符号的目的是为了唯一性(避免冲突),而不是隐私。 - Bergi
显示剩余7条评论

10

使用ES6模块(最初由@d13提出)对我很有效。它不能完美地模仿私有属性,但至少您可以放心,应该是私有的属性不会泄漏到类外部。以下是一个示例:

something.js

let _message = null;
const _greet = name => {
  console.log('Hello ' + name);
};

export default class Something {
  constructor(message) {
    _message = message;
  }

  say() {
    console.log(_message);
    _greet('Bob');
  }
};

然后,消费代码可以看起来像这样:

import Something from './something.js';

const something = new Something('Sunny day!');
something.say();
something._message; // undefined
something._greet(); // exception

更新(重要):

如@DanyalAytekin在评论中所述,这些私有属性是静态的,因此具有全局范围。它们在使用单例模式时会很好地工作,但在处理瞬态对象时需要小心。延续上面的示例:

import Something from './something.js';
import Something2 from './something.js';

const a = new Something('a');
a.say(); // a

const b = new Something('b');
b.say(); // b

const c = new Something2('c');
c.say(); // c

a.say(); // c
b.say(); // c
c.say(); // c

4
适用于“private static”。 - Danyal Aytekin
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - Johnny Oshika
我越了解函数式编程(尤其是Elm和Haskell),就越相信JS程序员会从基于模块的“模块化”方法中受益,而不是基于OOP类的方法。如果我们将ES6模块视为构建应用程序的基础,并完全忘记类,我相信我们最终会得到更好的应用程序。有没有经验丰富的Elm或Haskell用户对这种方法进行评论? - d13
1
在更新中,第二个 a.say(); // a 应该改为 b.say(); // b - grokky
尝试使用 let _message = null 的方式,不太好,当多次调用构造函数时,会出现混乱。 - Littlee
@Littlee 你说得对。请看我在答案中的更新。这种技术只适用于单例。 - Johnny Oshika

9
是的 - 你可以创建封装属性,但至少在ES6中没有使用访问修饰符(public|private)来实现。

下面是一个简单的示例,展示如何使用ES6实现:

1 使用class关键字创建类

2 在构造函数内部使用letconst保留字声明块作用域变量 -> 因为它们是块级作用域,所以无法从外部访问(封装)

3 为了允许一些访问控制(setter|getter)到这些变量,可以在构造函数内部声明实例方法,使用this.methodName=function(){}语法

"use strict";
    class Something{
        constructor(){
            //private property
            let property="test";
            //private final (immutable) property
            const property2="test2";
            //public getter
            this.getProperty2=function(){
                return property2;
            }
            //public getter
            this.getProperty=function(){
                return property;
            }
            //public setter
            this.setProperty=function(prop){
                property=prop;
            }
        }
    }

现在让我们来检查它:

var s=new Something();
    console.log(typeof s.property);//undefined 
    s.setProperty("another");//set to encapsulated `property`
    console.log(s.getProperty());//get encapsulated `property` value
    console.log(s.getProperty2());//get encapsulated immutable `property2` value

1
这是(目前)解决此问题的唯一方法,尽管构造函数中声明的所有方法都会为类的每个实例重新声明。这对于性能和内存使用来说是一个相当糟糕的想法。类方法应在构造函数范围之外声明。 - Freezystem
@Freezystem 首先:这些是实例方法(而非类方法)。其次,OP的问题是:“如何防止访问instance.property?”我的回答是:“一个...的例子”。第三,如果你有更好的想法——让我们听听。 - Nikita Kurtin
1
我并没有说你错了,我是说你的解决方案是实现私有变量的最佳折衷方案,尽管每次调用 new Something(); 都会创建每个实例方法的副本,因为你的方法在构造函数中声明以访问这些私有变量。如果您创建了大量类的实例,则可能会导致大量内存消耗,从而影响性能。应该将方法声明在构造函数范围之外。我的评论更多地是解释你的解决方案缺点而不是批评。 - Freezystem
1
但是在构造函数中定义整个类不是一个好的实践,这样做不就是在“黑客”JavaScript吗?只要看看其他面向对象编程语言,你会发现构造函数并不是用来定义一个类的。 - Kokodoko
@Kokodoko 我猜你所说的“OOP语言”是指基于类的语言(如Java、C#等),在这些语言中,你可以使用访问修饰符来实现属性封装。是的,你可以说这是一种有点hacky的方式,正如你可以在我的答案(以及上面的评论)中看到的那样,我的想法是举一个例子,展示如何使用ES6实现属性封装,因为像Java或C#中的访问修饰符在ES6中是不可用的。 - Nikita Kurtin
1
是的,那就是我想表达的意思,你的解决方案很好!我只是想说,一般来说,我很惊讶ES6添加了一个“class”关键字,但却删除了使用var和this实现封装的优雅解决方案。 - Kokodoko

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接