如何在es6类中声明私有变量和私有方法

16

在ES5中,我们使用构造函数

function Person(name,gender){

    var initial ="";    // we use var key word to make variable private

    function getNameWithInitial(){ // this is the private method to get name with initial
        console.log(this);
        initial = this.gender ==="male"?"Mr. ":"Mrs. ";
        return initial + this.name;
    }


    this.name = name;
    this.gender = gender;
    this.getName = function(){
        return getNameWithInitial.call(this);
    }


}


var manas = new Person("Manas","male");

console.log(manas.getName());

我的问题是如何在ES6类中声明私有变量和私有方法


JavaScript 中没有私有方法,尽管你可以有局部变量/函数。 - dandavis
你展示的代码演示了一个protected方法,而不是一个private方法。无论如何,在ES6中旧的方式仍然有效。 - dandavis
"ES6 仍然支持旧的方式。你能给我展示在 ES6 中如何创建所谓的受保护方法的例子吗?" - manas
4个回答

21
使用另一个名为ES2015模块的特性是实现这一目标的方法之一。
你可能已经熟悉了AMD模块或者commonJS模块(Nodejs中使用的模块)。好吧,ES6/ES2015为JS带来了一个标准 - 我们将其称为ES6模块,但它们现在是JS语言的一部分。一旦你拥有了模块,就可以对私有函数和对象变量进行信息隐藏。请记住,只有"导出"的内容对客户端调用代码可见。
让我们通过你的示例代码来完成这个过程。这是第一步: person.js
 const getNameWithInitial = function () {
      let initial = this._gender === 'male' ?
      'Mr. ' :
      'Mrs. ';
      return initial + this._name;
    }

  export class Person {

    constructor(name, gender) {
        this._name = name;
        this._gender = gender;
      }

      get name() {
          return getNameWithInitial.call(this);
        }
    }
  }

客户端.js

import {Person} from './person';
const manas = new Person('Manas', 'male');
console.log(manas.name);  // this calls what was your getName function

现在,getNameWithInitial函数已经有效地成为私有函数,因为它没有被导出,所以client.js无法看到它。

然而,对于Person类还存在一个问题,因为它是被导出的。目前,你可以直接访问manas对象并执行以下操作:

manas._name = 'Joe'

有了像 _name 这样的属性,我们可以结合模块和符号。这是 ES6+/ES2015 中可用的一种强大且轻量级的信息隐藏技术。

Symbol 是一种新的内置类型。每个新的 Symbol 值都是唯一的。因此可以用作对象的键。

如果客户端调用代码不知道用于访问该键的符号,它们无法获取该键,因为该符号未导出。

让我们看看我们修改后的代码,以利用符号和模块来隐藏类属性。

person.js

const s_name = Symbol();
const s_gender = Symbol();

const getNameWithInitial = function () {
  let initial = this[s_gender] === 'male' ?
    'Mr. ' :
    'Mrs. ';
  return initial + this[s_name];
}

export class Person {

  constructor(name, gender) {
    this[s_name] = name;
    this[s_gender] = gender;
  }

  get name() {
    return getNameWithInitial.call(this);
  }
}

现在,客户端不能仅执行以下操作:

 manas._name = 'Joe' 

因为_name并没有被用作名称值的键。

然而,符号通过反射特性(如Object.getOwnPropertySymbols)被暴露出来,因此请注意使用此技术时它们并不是“完全”私有的。

import {Person} from './person';
const manas = new Person('Manas', 'male');
const vals = Object.getOwnPropertySymbols(manas);
manas[vals[0]] = 'Joanne';
manas[vals[1]] = 'female';

总结 - 模块通常是隐藏某些内容的好方式,因为如果没有导出,则在模块外部不可用,并且与私有存储的符号一起使用作为键,则类属性也可以变为隐藏状态(但不是严格私有)。使用模块的方法在今天已经可以通过构建工具例如webpack / browserify和babel。


1
但是,Object.getOwnPropertySymbols( person ).map( symbol => person[symbol] ); // [ name, gender ] 这个方法怎么办呢?当然,键名不再可读,但它们与 Object.keys(person).map( key => person[key] ); 一样并不比较安全。如果您要存储打算在闭包中保持隐藏的数据,并且它确实需要保持隐藏,请不要使用此方法。 - Norguard
更新的答案 - 你是对的,这只是一种有效的信息隐藏策略,不能保证隐私。Babel文档清楚地说明了这一点 - https://babeljs.io/docs/learn-es2015/ - arcseldon
2
为什么不能在Person类外部声明var namevar gender,然后从Person的函数中访问它们呢?这些变量不会被导出,所以它们不是真正的私有吗? - Jez
2
@Jez 因为使用 new 操作符实例化的每个 Person 实例都会创建一个闭包,它们共享相同的 namegender 引用,导致不同的实例操作同一数据,从而产生意想不到的结果。 - leo
@leo 你可以将这些私有变量放在类所属的闭包中。这样,任何创建的实例都会拥有自己的私有变量。更好的做法是使用“let”而不是“var”来创建这些变量。 - Johann
@AndroidDev 那么你需要每个实例都有自己的方法(闭包),因为你不能在原型上创建一个能够访问构造函数中声明变量的闭包。 - Armen Michaeli

5
如果你想要一个 ES5 解决方案的模拟,它非常简单;构造函数只是成为了保持闭包的东西,你可以在其中添加任何需要记住私有状态的方法/对象。
原型方法没有访问初始构造函数的闭包的权限,除非使用一些特权 getter。
class Person {

  constructor ({ name, age, deepDarkSecret }) {
    const bribe = () => console.log(`Give me money, or everybody will know about ${ redactRandom(deepDarkSecret) }`);

    Object.assign(this, { name, age }); // assigning publicly accessible values
    Object.assign(this, { bribe }); // assign "privileged" methods (functions with closure-reference to initial values)
  }

  recallSecret () {
    console.log("I'm just a prototyped method, so I know nothing about any secret, unless I use a privileged function...");
    this.bribe();
  }
}

如果您查看这个示例,您会注意到它与您的示例并没有太大区别(只是使用了“更干净”的小工具,特别是添加原型/静态方法)。 它仍然是在原型下面。
如果您有使用任何类型的模块/导出(ES6是理想的),那么通过另一种数据类型,您可以拥有真正的隐私而不必自己清理。
这看起来有点hackey。希望它将来会变得更加简洁,并成为更好东西的基础,但如果您想要基于实例的私有访问,即使是对于原型化的方法,请在从中导出类的模块中制作一个WeakMap。
使用this作为您的键。现在,所有原型方法都可以访问WeakMap的闭包而不是创建特权函数...
...缺点是每次想要私有变量时,都必须在WeakMap中查找它们,而不是从this中获取。
const personalBaggage = new WeakMap();

class Person {
  constructor ({ name, age, darkestMoment }) {
    const privates = { name, age, darkestMoment };
    personalBaggage.add(this, privates);
  }

  recallDarkestHour () {
    const { darkestMoment } = personalBaggage.get(this);
    console.log(darkestMoment);
  }
}

export default Person;

只要您不在this内部失去引用,这个方法就可以正常工作。
与使用Symbol不同,无论如何您都无法获得对私有对象的引用。
如果您知道对象上的符号,可以使用这些符号查找属性。
而获取任何对象上符号列表只需调用一个函数。
Symbol不是用于保密,而是用于防止命名冲突,并定义可以在任何对象/函数上使用但不会在循环中被捕捉、被意外调用或覆盖等常见符号。

将私有数据包存储在以this为键的弱映射中,可让您访问完全隐藏的数据集,该数据集在该模块之外是完全无法访问的。
更好的是,弱映射中的键/值不会阻止GC清理它们。
通常,如果它们留在一个数组中,那么它们将保留在那里。 如果一个对象的最后一个使用地点是作为弱映射中的键,则它会被收集并且弱映射值会自动擦除(在本机ES6中;内存奖励无法进行填充,但对象可以)。


非常类似于此处给出的答案:https://dev59.com/0JLea4cB1Zd3GeqP01NR#34418487 - arcseldon
@arcseldon 这个方法非常接近;话虽如此,在被接受的答案中,你需要为每个你想隐藏的成员/方法创建一个WeakMap实例,而在我的示例中,你只需要创建一个map并从中解构出你所需的内容。当然,两种方法都有各自的优缺点。对我来说,这种方式感觉稍微不那么笨重。 - Norguard

0

这篇文章对该主题有很好的介绍。在你提问之前,我并不太了解如何在ES6中完成这个操作,但是经过阅读之后,我觉得这个功能非常酷炫。所以你可以创建一个与函数索引相关联的符号。

这段代码直接来自他们的网站。

var Person = (function() {
    var nameSymbol = Symbol('name');

    function Person(name) {
        this[nameSymbol] = name;
    }

    Person.prototype.getName = function() {
        return this[nameSymbol];
    };

    return Person;
}());

var p = new Person('John');

JavaScript中的私有属性


2
它们并不是从外部无法访问的;它们是通过文本名称尝试访问它们的人无法访问,并且它们不包括在迭代中,但您仍然可以从对象中获取所有键,检查 .toString 中的键,并使用键列表查找值。符号非常有用,只是不要认为它可以在依赖闭包保持关闭以防止篡改的项目中为您提供安全性。 - Norguard

0
你可以使用 Symbol 来完成它。

const _name = Symbol();

class Person {
  constructor(name) {
    this[_name] = name;
  } 
}

const person = new Person('John');

alert(person.name);
// => undefined


2
Object.getOwnPropertySymbols( person ).map( symbol => person[symbol] ); // ["John"] 这不是说它在其他方面没有极大的用处,但是如果您想要的是极高的隐私性,那么它并不算是极为私密的。 - Norguard

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