ES6类中的受保护属性(使用符号?)

4

问题:

如何在ES6类中优雅地实现受保护的属性?(只能从子类内部访问

我不是在寻找像“ES没有受保护/包属性”这样的响应。已经知道了。我想要一个漂亮而干净的解决方法来模拟受保护的属性。

我不想增加安全性。只需对所有API的最终用户公开一个更清晰的接口


示例:

我有以下API:(node

my-class.js:

let Symbols = {
    _secret: Symbol("_secret")
};
class MyClass {
    constructor() {
        this.public = "This is public";
        this[Symbols._secret] = "This is private";
    }
}
// Set the Symbols to a static propietry so any class can access it and extend it
MyClass[Symbol.for("_Symbols")] = Symbols;
module.exports = MyClass

my-child-class.js:

let MyClass = require("./my-class.js");

// extends protected properties with own properties
Symbols = Object.assign({}, MyClass[Symbol.for("_Symbols")] , {
    _childSecret = Symbol("_childSecret")
});

class MyChildClass extends MyClass {
    constructor() {
        super();
        this[Symbols._childSecret] = "This is also private";
        console.log(this[Symbols._secret]); //logs "this is private"
        console.log(this[Symbols._childSecret]); //logs "this is also private"
    }
}
// Set the Symbols to a static propietry so any class can access it and extend it
MyClass[Symbol.for("_Symbols")] = Symbols;
module.exports = MyChildClass;

使用该类:

let MyChildClass = require("./my-child-class.js");
var c = new MyChildClass();

优点:

  • 暴露的 API 更加清晰。而且终端用户可以查看所暴露的方法。

问题:

  • 基类中的代码很“好看”,但是子类中的代码不太好看。有没有办法改善顺序?

  • 任何能够访问 Symbol.for("_Symbols") 的人都可以访问 API 的所有受保护/私有属性。(编辑: 我不介意这个。这对我来说不是问题,因为如果有人想要打破 API 访问内部符号,那就是他们的问题。


2
你如何通过命名约定在ES6类中优雅地实现受保护的属性?无论你想出什么方法,都可以被规避,因此最终只会增加自己代码的复杂性。 - Thomas
2
我不介意这个。对我来说这不是问题,因为如果有人想要打破API访问内部符号,那就是他们的错。所以,你不在乎并理解你过于复杂的代码只会带来麻烦吗?那么,为什么你要把它变得比必要的更加复杂呢?将所有属性都变成普通属性,问题就解决了。 - zerkms
1
公共API == 方法。将您的属性“规范化”,并尽可能公开多个方法/获取器/设置器。 - zerkms
2
如果您担心使用多个“private”属性会污染类,也许您可以创建一个名为_private的对象,并将所有私有属性保存在一个地方(作为_private的属性)。 - Hugo Silva
1
@Ciberman,在JS中没有protected,你暴露的一切都是公开的。无论你想将某些内容暴露给开发人员还是子类,都没有区别;暴露就是暴露。你可以使用一些约定来注释某些属性为private/protected/whatever,但这样做会导致你的应用程序最终崩溃。最好的方法是使这些属性不可枚举,这样开发人员就不会意外地遇到它们,但其他所有东西基本上都是多余的。 - Thomas
显示剩余3条评论
4个回答

3
声明:使用模块和符号是 ES2015+ 中的信息隐藏技术(但使用符号的类属性将被隐藏,不是严格私有 - 根据 OP 的问题和假设)。
通过 ES2015 模块(仅导出已声明为导出的内容)和 ES2015 符号的组合可以实现轻量级信息隐藏。符号是一种新的内置类型。每个新符号值都是唯一的。因此可以用作对象上的键。
如果客户端调用代码不知道用于访问该键的符号,则无法获得该符号,因为该符号未导出。例如:

vehicle.js

const s_make = Symbol();
const s_year = Symbol();

export class Vehicle {

  constructor(make, year) {
    this[s_make] = make;
    this[s_year] = year;
  }

  get make() {
    return this[s_make];
  }

  get year() {
    return this[s_year];
  }
}

并且使用模块 vehicle.js

client.js

import {Vehicle} from './vehicle';
const vehicle1 = new Vehicle('Ford', 2015);
console.log(vehicle1.make); //Ford
console.log(vehicle1.year); // 2015

然而,虽然符号是独一无二的,但它们实际上并不私有,因为它们通过反射功能(如Object.getOwnPropertySymbols)被公开暴露...

const vals = Object.getOwnPropertySymbols(vehicle1);
vehicle1[vals[0]] = 'Volkswagon';
vehicle1[vals[1]] = 2013;
console.log(vehicle1.make); // Volkswagon
console.log(vehicle1.year); // 2013

请记住,尽管在混淆足够的情况下,这种方法可能被考虑。

1

使用WeakMap方法的一种变体,ES6中可以实现保护属性。

基本技术如下:

  1. 为每个类存储一个私有弱引用以保存实例受保护的数据。
  2. 在超类构造函数中创建受保护的数据。
  3. 将受保护的数据从超类构造函数传递到子类构造函数。

简单演示(旨在清晰易懂,但功能不够完善,请参见下文进行改进)。这在父类中设置了受保护的数据,并在子类中访问它。如果没有方法公开它,则类外部无法访问它:

// Define parent class with protected data
const Parent = (()=>{

  const protData = new WeakMap();
  
  class Parent {
    constructor () {
      // Create and store protected data for instance
      protData.set(this,{
        prop: 'myProtectedProperty',
        meth () { return 'myProtectedMethod'; }
      });
      
      // If called as super pass down instance + protected data
      if(new.target!==Parent){
        this.prot = protData.get(this);
      }
    }
    
    setText (text) {
      const prot = protData.get(this);
      prot.text = text;
    }
    
    getText () {
      const prot = protData.get(this);
      return prot.text;
    }
  }
  
  return Parent; // Expose class definition

})();

// Define child class with protected data
const Child = (()=>{

  const protData = new WeakMap();
  
  class Child extends Parent {
    constructor (...args) {
      super(...args);
      protData.set(this,this.prot); // Store protected data for instance
      this.prot = undefined; // Remove protected data from public properties of instance
    }
    
    getTextChild () {
      const prot = protData.get(this);
      return prot.text;
    }
  }
  
  return Child; // Expose class definition

})();

// Access protected data
const child = new Child();
child.setText('mytext');
console.log(child.getText()); // 'mytext'
console.log(child.getTextChild()); // 'mytext'

这里有几个细节可以改进:

  1. 这不适用于进一步的子类。我们在第一个子类中清除了受保护的数据,因此进一步的构造函数将不会收到它。
  2. 新实例的键中包含“prot”。我们在子类构造函数中清除了该属性,但仍会枚举。使用delete很诱人,但delete非常缓慢

解决任意数量的子类很容易。如果我们作为super调用,则只需保留受保护的数据即可:

if(new.target!==Child)this.prot=undefined;

对于属性残留问题,我喜欢的解决方案是在基类中创建一个全新的实例,并使用绑定的this来分别传递实例和受保护的数据。然后你就有了一个完全干净的实例,没有删除性能影响。你必须在构造函数中使用一些惯用语才能使其正常工作,但完全可行。
以下是解决这些问题的最终方案:

// Protected members in ES6

// Define parent class with protected data
const Parent = (()=>{

  const protData = new WeakMap();
  
  let instanceNum = 0;
  
  class Parent {
  
    constructor (...args) {
      // New instance since we will be polluting _this_
      //   Created as instance of whichever class was constructed with _new_
      const inst = Object.create(this.constructor.prototype);
      // .. do normal construction here *on inst*
      
      // If called as super pass down instance + protected data
      if(new.target!==Parent){
        protData.set(inst,{  // Create and store protected data for instance
          instanceNum: ++instanceNum
        });
        this.inst=inst; // Pass instance
        this.prot=protData.get(inst); // Pass protected data
      }
      
      // If called directly return inst as construction result
      //   (or you could raise an error for an abstract class)
      else return inst;
    }
    
    sayInstanceNum () {
      const prot = protData.get(this);
      console.log('My instance number is: '+prot.instanceNum);
    }
  
    setInstanceNumParent (num) {
      const prot = protData.get(this);
      prot.instanceNum = num;
    }
  
  }
  
  return Parent; // Expose class definition

})();

// Define child class with protected data
const Child = (()=>{

  const protData = new WeakMap();
  
  class Child extends Parent {
  
    constructor (...args) {
      super(...args);
      protData.set(this.inst,this.prot); // Store protected data for instance
      
      // If called directly return inst as construction result,
      //   otherwise leave inst and prot for next subclass constructor
      if(new.target===Child)return this.inst;
    }
    
    celebrateInstanceNum () {
      const prot = protData.get(this);
      console.log('HONKYTONK! My instance number is '+prot.instanceNum+'! YEEHAWW!');
    }
    
    setInstanceNumChild (num) {
      const prot = protData.get(this);
      prot.instanceNum = num;
    }
  
  }
  
  return Child; // Expose class definition

})();

// Define grandchild class with protected data
const Grandchild = (()=>{

  const protData = new WeakMap();
  
  class Grandchild extends Child {
  
    constructor (...args) {
      super(...args);
      protData.set(this.inst,this.prot); // Store protected data for instance
      
      // If called directly return inst as construction result,
      //   otherwise leave inst and prot for next subclass constructor
      if(new.target===Grandchild)return this.inst;
    }
    
    adoreInstanceNum () {
      const prot = protData.get(this);
      console.log('Amazing. My instance number is '+prot.instanceNum+' .. so beautiful.');
    }
    
    setInstanceNumGrandchild (num) {
      const prot = protData.get(this);
      prot.instanceNum = num;
    }
  
  }
  
  return Grandchild; // Expose class definition

})();

// Create some instances to increment instance num
const child1 = new Child();
const child2 = new Child();
const child3 = new Child();
const grandchild = new Grandchild();

// Output our instance num from all classes
grandchild.sayInstanceNum();
grandchild.celebrateInstanceNum();
grandchild.adoreInstanceNum();

// Set instance num from parent and output again
grandchild.setInstanceNumParent(12);
grandchild.sayInstanceNum();
grandchild.celebrateInstanceNum();
grandchild.adoreInstanceNum();

// Set instance num from child and output again
grandchild.setInstanceNumChild(37);
grandchild.sayInstanceNum();
grandchild.celebrateInstanceNum();
grandchild.adoreInstanceNum();

// Set instance num from grandchild and output again
grandchild.setInstanceNumGrandchild(112);
grandchild.sayInstanceNum();
grandchild.celebrateInstanceNum();
grandchild.adoreInstanceNum();


受保护的方法可以在公共方法的实例上下文中调用,例如:prot.myMethod.call(this,arg1,arg2,arg3) - user6798019

0

使用#表示私有属性(例如#someProperty), 使用_表示受保护的属性(例如_someProperty), 不使用前缀表示公共属性。


-1

你的方法是毫无意义的。

符号并不提供任何安全性,因为它们是公开的。你可以很容易地使用 Object.getOwnPropertySymbols 获取它们。

所以如果你不关心安全,只想要简单性,就使用一个普通的 _secret 属性。

class MyClass {
  constructor() {
    this.public = "This is public";
    this._secret = "This is private";
  }
}
module.exports = MyClass;

let MyClass = require("./my-class.js");
class MyChildClass extends MyClass {
  constructor() {
    super();
    this._childSecret = "This is also private";
    console.log(this._secret); // "this is private"
    console.log(this._childSecret); // "this is also private"
  }
}
module.exports = MyChildClass;

1
嗯...问题在于最终用户将查看 _secret 属性,例如,在 Chrome 调试器/控制台中当他们 console.log(new MyClass()) 时。你知道有什么解决方法吗? - Ciberman
2
@Ciberman,使用ES7装饰器,你可以将Object.defineProperty(this, "_secret", { value: "This is private", writable:true /*, enumerable: false (default if undefined) */ })封装为一个非常漂亮的语法。 - Thomas
@Ciberman 他们也可以通过 Object.getOwnPropertySymbols(new MyClass()) 查看您的符号。在公共实例中安全存储私有数据的方法是不存在的。您应该将其存储在其他地方。 - Oriol
4
为什么他们不能在控制台中查看符号属性?毕竟,调试器的作用就是检查实现细节。除了“不使用调试器”之外,没有其他解决方法。 - Bergi
我应该提到像WebStorm这样的IDE能够理解“_protected模式”,并且在代码完成时不会突出显示前缀属性。 - igorsantos07
我们可以在一个类的外部以及一个模块的.mjs文件中放置一个键符号。 - Константин Ван

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