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

Posted

技术标签:

【中文标题】ES6 类中的受保护属性(使用符号?)【英文标题】:Protected properties in ES6 classes (using Symbols?) 【发布时间】:2016-12-11 05:49:04 【问题描述】:

问题:

您将如何以优雅的方式在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 更干净。 API 的最终用户可以查看公开的方法。

问题:

代码在基类中很“漂亮”,但在子类中却没有那么漂亮。有什么办法可以改善顺序吗?

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

【问题讨论】:

“你将如何以优雅的方式在 ES6 类中实现受保护的属性?”通过命名约定。您想出的所有东西都可以被规避,因此最终您只会增加自己代码的复杂性。 “我不介意这个。这对我来说不是问题,因为如果有人想破坏 API 访问内部符号,那是他们的错” --- 这是正确的,那您不在乎并理解您过于复杂的代码只会带来复杂性吗?那么,你为什么要让它变得比必要的复杂呢?使所有属性都只是普通属性,问题就解决了。 公共 api == 方法。使您的属性“正常”并根据需要公开尽可能多的方法/getter/setter。 如果您担心使用多个“私有”属性污染类,也许您可​​以创建一个名为 _private 的对象,并将所有私有道具保存在一个地方(作为 _private 的属性) @Ciberman,JS 你暴露的一切都是公开的期间没有保护。你想暴露某事没有区别。给开发者或某个子类;暴露是暴露的。您可以使用一些约定将某些属性注释为私有/受保护/其他。弄乱这个/依赖这个,你的应用程序最终会崩溃。你能做的最好的就是使这些属性不可枚举,这样开发人员就不会意外地发现它们,但其他一切都只是膨胀。 【参考方案1】:

声明:使用模块和符号是 ES2015+ 中的一种信息隐藏技术(但使用符号的类属性将被隐藏,而不是严格私有 - 根据 OP 问题和假设)。

轻量级信息隐藏可以通过 ES2015 模块(只会导出您声明为导出的内容)和 ES2015 symbols 的组合来实现。 Symbol 是一种新的内置类型。每个新的符号值都是唯一的。因此可以用作对象的键。

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

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

请记住这一点,尽管在模糊处理就足够的情况下,可以考虑使用这种方法。

【讨论】:

【参考方案2】:

在 ES6 中可以使用 WeakMap method for private properties 的变体来保护属性。

基本技巧是:

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

简单的演示(目的是为了清晰但不是理想的功能,请参阅下面的改进)。这会在父类中设置受保护的数据并在子类中访问它。如果没有方法公开它,则类之外的任何东西都无法访问它:

// 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'

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

    这不适用于更多的子类。我们清除了第一个子类中的受保护数据,因此其他构造函数将不会收到它。 新实例的键中有“prot”。我们在子类构造函数中清除了该属性,但它仍会枚举。在这里使用delete 很诱人,但delete is very slow。

求解任意数量的子类很容易。如果我们被称为超级,只需保留受保护的数据即可:

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)【参考方案3】:

你的方法毫无意义。

符号不提供任何安全性,因为它们是公开的。您可以通过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;

【讨论】:

Mh.. 问题是最终用户会在console.log(new MyClass()) 时查看_secret 属性,例如,在chrome 调试器/控制台中。你知道一些解决方法吗? @Ciberman, Object.defineProperty(this, "_secret", value: "This is private", writable:true /*, enumerable: false (default if undefined) */ ) 使用 ES7 装饰器,您可以将其包装成非常好的语法 @Ciberman 他们还可以通过Object.getOwnPropertySymbols(new MyClass()) 看到您的符号。没有办法将私有数据安全地存储在公共实例中。你应该把它存放在别的地方。 @Ciberman:他们为什么不能在控制台中查看符号属性?毕竟,这就是调试器检查实现细节的目的。除了“不要使用调试器”之外没有其他解决方法。 我应该提一下,像 WebStorm 这样的 IDE 可以理解“_protected 模式”,并且不会在代码完成时突出显示前缀属性。【参考方案4】:

# 用于私有(例如#someProperty), 使用_ 表示受保护(例如_someProperty), public 没有前缀。

【讨论】:

以上是关于ES6 类中的受保护属性(使用符号?)的主要内容,如果未能解决你的问题,请参考以下文章

访问派生类中的受保护成员

为啥只有 clone 和 finalize 是对象类中的受保护方法?

C#:基类中的受保护方法;无法使用来自另一个类的派生类对象进行访问[重复]

具体类中的受保护构造函数与抽象类中的公共构造函数

为啥我不能访问静态多态派生类中的受保护成员?

无法访问派生类中的受保护方法