关于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);
必须执行这种隐藏,但这当然是不够的,因为 还有可能直接调用方法/属性:
console.log(obj1.protectedNumber);
助手类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;
BaseClass类构造函数的描述
支票可能会让您感到困惑:
if (!(this instanceof BaseClass)) return new BaseClass();
该测试是“业余”。 您可以删除它;它与受保护的方法无关。 但是,我个人将其保留在代码中,因为 在类实例未正确创建的情况下(例如 没有关键字new。 例如,像这样:
var obj1 = BaseClass();
在这种情况下,请按照您的意愿进行操作。 例如,您可以生成错误:
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);
对于派生类,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) }; function checkAccess() { if (this.constructor === DerivedClass) throw new ProtectedError(); }
派生类构造函数描述
在派生类中,我们创建一个_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(){
在派生类中,使用属性会更加复杂,因为 属性不能被上下文替换。 因此,我们将使用标准的Object.getOwnPropertyDescriptor方法来从基类的属性获取getter和setter,因为它们已经可以更改调用上下文:
function DerivedClass(){ function checkAccess(){ ... } var _base = { protectedMethod: _self.protectedMethod.bind(_self), }; var _baseProtectedPropertyDescriptor = Object.getOwnPropertyDescriptor(_self, 'protectedProperty');
继承说明
最后我要评论的是从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 });