JavaScript ES5中的受保护方法

关于JavaScript中的对象模型,已经有很多很棒的文章。 关于在Internet上创建私人班级成员的各种方法充满了值得描述。 但是关于受保护的方法-数据很少。 我想填补这一空白,并告诉您如何在没有JavaScript纯ECMAScript 5库的情况下创建受保护的方法。

在本文中:


链接到带有源代码和测试的git-hub存储库。

为什么需要受保护的班级成员


简而言之

  • 可以更轻松地了解课程的操作并查找其中的错误。 (您可以立即看到在这种情况下使用了类成员。如果是私有的,则只需要分析该类,如果是受保护的,则只需分析此类和派生类。)
  • 更容易管理变更。 (例如,您可以删除私人成员,而不必担心可编辑类之外的内容会中断。)
  • 错误跟踪器中的应用程序数量减少了,因为 库或控件的用户可以“缝制”我们的“私有”成员,我们决定在新版本的类中将其删除,或更改其工作逻辑。
  • 通常,受保护的类成员是一种设计工具。 方便并经过良好测试是很好的。

让我提醒您,受保护成员的主要思想是向类实例的用户隐藏方法和属性,但同时允许派生类可以访问它们。

使用TypeScript不允许调用受保护的方法,但是,使用JavaScript编译后,所有私有成员和受保护成员都将变为公共成员。 例如,我们开发了一个控件或库,用户可以将其安装在其站点或应用程序上。 这些用户将能够对受保护的成员执行任何他们想做的事情,从而破坏了类的完整性。 结果,我们的错误跟踪器爆满了关于我们的库或控件无法正常工作的抱怨。 我们花费时间和精力进行整理- “对象在客户端的这种状态下是否会这样,从而导致错误?!” 。 因此,为了使所有人的生活更加轻松,需要这种保护,而这将无法改变私人和受保护阶级成员的含义。

需要什么才能理解所讨论的方法


要了解声明受保护的类成员的方法,您需要丰富的知识:

  • JavaScript中的设备类和对象。
  • 创建私有类成员的方法(至少通过闭包)。
  • 方法Object.defineProperty和Object.getOwnPropertyDescriptor

关于JavaScript中的设备模型,例如,我可以推荐Andrey Akinshin( DreamWalker撰写的出色文章“了解JS中的OOP [Part No.1]”
关于私有财产,在我看来,在MDN网站上有多达4种不同的创建私有类成员的方式的不错的,相当完整的描述。

至于Object.defineProperty方法,它将允许我们在for-in循环以及序列化算法中隐藏属性和方法:

function MyClass(){ Object.defineProperty(MyClass.prototype, 'protectedNumber', { value: 12, enumerable: false }); this.publicNumber = 25; }; var obj1 = new MyClass(); for(var prop in obj1){ console.log('property:' prop); //prop     'protectedNumber' } console.log(JSON.stringify(obj1)); //  { 'publicNumber': 25 } 

必须执行这种隐藏,但这当然是不够的,因为 还有可能直接调用方法/属性:

  console.log(obj1.protectedNumber); //  12. 

助手类ProtectedError


首先,我们需要ProtectedError类,该类继承自Error,如果无法访问受保护的方法或属性,则将抛出该类。

 function ProtectedError(){ this.message = "Encapsulation error, the object member you are trying to address is protected."; } ProtectedError.prototype = new Error(); ProtectedError.prototype.constructor = ProtectedError; 

在ES5中实施受保护的班级成员


现在,我们有了ProtectedError类,并且我们了解了Object.defineProperty使用可枚举的值是什么:false,让我们分析一个基类的创建,该基类希望与其所有派生类共享protectedMethod方法,但对其他所有人隐藏它:

 function BaseClass(){ if (!(this instanceof BaseClass)) return new BaseClass(); var _self = this; //   ,        /** @summary       */ function checkAccess() { if (!(this instanceof BaseClass)) throw new ProtectedError(); if (this.constructor === BaseClass) throw new ProtectedError() } Object.defineProperty(_self, 'protectedMethod', { enumerable: false, //    for-in  configurable:false, //     value: function(){ //   , ,          Base,     checkAccess.call(this); //  . protectedMethod(); } }); function protectedMethod(){ //       , //       this,   _self return 'example value'; } this.method = function (){ protectedMethod(); //          BaseClass //this.protectedMethod(); //   , ..      ProtectedError } } 

BaseClass类构造函数的描述


支票可能会让您感到困惑:

  if (!(this instanceof BaseClass)) return new BaseClass(); 
该测试是“业余”。 您可以删除它;它与受保护的方法无关。 但是,我个人将其保留在代码中,因为 在类实例未正确创建的情况下(例如 没有关键字new。 例如,像这样:

 var obj1 = BaseClass(); //  : var obj2 = BaseClass.call({}); 

在这种情况下,请按照您的意愿进行操作。 例如,您可以生成错误:

  if (!(this instanceof BaseClass)) throw new Error('Wrong instance creation. Maybe operator "new" was forgotten'); 

或者,您可以像在BaseClass中那样简单地正确实例化。

接下来,我们将新实例保存在_self变量中(为什么我以后需要对此进行解释)。

名为protectedMethod的公共属性的描述


输入该方法,我们将调用被调用的上下文检查。 最好使用单独的方法(例如,checkAccess)签出,因为 所有受保护的方法和类的属性都需要进行相同的检查。 因此,首先,检查对此的调用的上下文类型。 如果它的类型不是BaseClass,则该类型既不是BaseClass本身,也不是其派生类。 我们禁止此类电话。

 if(!(this instanceof BaseClass)) throw new ProtectedError(); 

怎么会这样 例如,像这样:

 var b = new BaseClass(); var someObject = {}; b.protectedMethod.call(someObject); //   ,  protectedMethod this   someObject    , .. someObject instanceof BaseClass   

对于派生类,BaseClass的此instance的表达式将为true。 但是对于BaseClass实例,此BaseClass表达式的实例将为true。 因此,为了区分BaseClass类的实例和派生类的实例,我们检查构造函数。 如果构造函数与BaseClass相匹配,则在BaseClass实例上调用我们的protectedMethod,就像常规的公共方法一样:

 var b = new BaseClass(); b.protectedMethod(); 

我们禁止此类呼叫:

 if(this.constructor === BaseClass) throw new ProtectedError(); 

接下来是protectedMethod封闭方法的调用,该方法实际上是我们保护的方法。 在方法内部,如果您需要引用BaseClass类的成员,则可以使用_self的存储实例进行此操作。 正是_self被创建为可以从所有私有方法/私有方法访问类成员的。 因此,如果您不需要在受保护的方法或属性中访问类成员,则不能创建_self变量。

在BaseClass类中调用受保护的方法


在BaseClass类内部,protectedMethod必须仅通过名称访问,而不能通过此名称访问。 否则,在protectedMethod内部,我们无法区分是作为公共方法调用还是从类内部调用。 在这种情况下,闭包可以节省我们的时间-protectedMethod的行为类似于常规的私有方法,在类内部是封闭的,并且仅在BaseClass函数的范围内可见。

DerivedClass派生类说明


现在,让我们看一下派生类,以及如何使它可被基类的受保护方法访问。

 function DerivedClass(){ var _base = { protectedMethod: this.protectedMethod.bind(this) }; /** @summary       */ function checkAccess() { if (this.constructor === DerivedClass) throw new ProtectedError(); } //     Object.defineProperty(this, 'protectedMethod', { enumerable: false, // ..       this configurable: false,//         for-in  //      value: function(){ checkAccess.call(_self); return _base.protectedMethod(); } }); //        this.someMethod = function(){ console.log(_base.protectedMethod()); } } DerivedClass.prototype = new BaseClass(); Object.defineProperty(DerivedClass.prototype, 'constructor', { value : DerivedClass, configurable: false }); 

派生类构造函数描述


在派生类中,我们创建一个_base对象,在其中放置对基类的protectedMethod方法的引用,并通过标准bind方法将其与派生类的上下文隔离。 这意味着调用_base.protectedMethod();。 在protectedMethod内部,这不是_base对象,而是DerivedClass类的实例。

ProtectedMethod方法说明DerivedClass内部


在DerivedClass类中,有必要以与通过Object.defineProperty在基类中相同的方式声明protectedMethod公共方法,并通过调用checkAccess方法或直接在该方法中进行检查来检查对其的访问:

  Object.defineProperty(DerivedClass.prototype, 'protectedMethod', { enumerable: false, configurable: false, value: function(){ if(this.constructor === DerivedClass) throw new ProtectedError() return _base.protectedMethod(); } }); 

我们检查- “但是我们是否被称为简单的公共方法?” 对于DerivedClass类的实例,构造函数将等于DerivedClass。 如果是这样,则产生一个错误。 否则,我们将其发送给基类,并且它将进行所有其他检查。

因此,在派生类中,我们有两个功能。 一个通过Object.defineProperty声明,并且对于DerivedClass派生的类是必需的。 它是公开的,因此具有禁止公开呼叫的支票。 第二个方法位于_base对象中,该对象在DerivedClass类内部关闭,因此对任何人都不可见,它是用来从所有DerivedClass方法访问受保护的方法的。

财产保护


使用属性,工作发生的方式略有不同。 通常通过Object.defineProperty定义BaseClass中的属性,仅在首先需要添加检查的getter和setter中,即 致电checkAccess:

 function BaseClass(){ function checkAccess(){ ... } var _protectedProperty; Object.defineProperty(this, 'protectedProperty', { get: function () { checkAccess.call(this); return _protectedProperty; }, set: function (value) { checkAccess.call(this); _protectedProperty = value; }, enumerable: false, configurable: false }); } 

在BaseClass类内部,不是通过此方法访问protected属性,而是通过封闭变量_protectedProperty访问该属性。 如果在使用BaseClass类中的属性时对getter和setter起作用对我们很重要,则我们需要创建私有方法getProtectedPropety和setProtectedProperty,在其中将不进行检查,并且它们应该已经被调用。

 function BaseClass(){ function checkAccess(){ ... } var _protectedProperty; Object.defineProperty(this, 'protectedProperty', { get: function () { checkAccess.call(this); return getProtectedProperty(); }, set: function (value) { checkAccess.call(this); setProtectedProperty(value); }, enumerable: false, configurable: false }); function getProtectedProperty(){ //    return _protectedProperty; } function setProtectedProperty(value){ //    _protectedProperty = value; } } 

在派生类中,使用属性会更加复杂,因为 属性不能被上下文替换。 因此,我们将使用标准的Object.getOwnPropertyDescriptor方法来从基类的属性获取getter和setter,因为它们已经可以更改调用上下文:

 function DerivedClass(){ function checkAccess(){ ... } var _base = { protectedMethod: _self.protectedMethod.bind(_self), }; var _baseProtectedPropertyDescriptor = Object.getOwnPropertyDescriptor(_self, 'protectedProperty'); //      _base //    DerivedClass     Object.defineProperty(_base, 'protectedProperty', { get: function() { return _baseProtectedPropertyDescriptor.get.call(_self); }, set: function(value){ _baseProtectedPropertyDescriptor.set.call(_self, value); } }) //      ,      DerivedClass      . Object.defineProperty(_self, 'protectedProperty', { get: function () { checkAccess.call(_self); return base.protectedProperty; }, set: function (value) { checkAccess.call(_self); _base.protectedProperty = value; }, enumerable: false, configurable: false }); } 

继承说明


最后我要评论的是从BaseClass继承DerivedClass。 如您所知,DerivedClass.prototype = new BaseClass(); 不仅创建原型,还重写其构造函数属性。 因此,对于DerivedClass的每个实例,构造函数的属性都等于BaseClass。 要解决此问题,通常在创建原型后,重写Constructor属性:

 DerivedClass.prototype = new BaseClass(); DerivedClass.prototype.constructor = DerivedClass; 

但是,为了使我们之后没有人重写此属性,我们使用相同的Object.defineProperty。 可配置的:false属性可防止再次覆盖该属性:

 DerivedClass.prototype = new BaseClass(); Object.defineProperty(DerivedClass.prototype, 'constructor', { value : DerivedClass, configurable: false }); 

Source: https://habr.com/ru/post/zh-CN425521/


All Articles