From 85861de3403f40b69e5bedaf126e71ec5d826e38 Mon Sep 17 00:00:00 2001 From: Andy Harrison Date: Tue, 24 Sep 2019 16:53:19 -0400 Subject: [PATCH 1/3] - add functionality to allow private or protected access to properties - allow for static properties - remove previous protected-like functionality - remove double-checking of class name validity --- src/Class.js | 118 +++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 86 insertions(+), 32 deletions(-) diff --git a/src/Class.js b/src/Class.js index 87e76a3..2b843a8 100644 --- a/src/Class.js +++ b/src/Class.js @@ -5,7 +5,10 @@ "use strict"; - let _initializing = false; + const PRIVATE = 4, PROTECTED = 2, STATIC = 1; + + let _initializing = false, + _scopes = new WeakMap(); /*** helper functions ***/ @@ -20,6 +23,7 @@ //checks if the specified classname is valid (note: this doesn't check for reserved words) return className !== (void 0) && /^[a-z_$][a-z0-9_$]*$/i.test(className); } + function xor(a, b){ return !!(a ? !b : b); } /*** shared functions ***/ @@ -28,26 +32,84 @@ function _emptyFn(){} let _classToString = function toString(){ return "function Class() { [custom code] }"; }; let _instanceToString = function toString(){ - return "[instance of "+(classNameIsValid(this.constructor.name) ? this.constructor.name : "Class")+"]"; + return "[instance of "+this.constructor.name+"]"; }; let _extendToString = function toString(){ return "function extend() { [custom code] }"; }; - - //Stores a getter and/or setter, or removes them. The getter/setter allows a subclass' constructorFn to access a variable or function that is inside the new class' constructorFn. - function _defineProtectedMember(protectedObj, name, options){ - if(!_initializing) throw new Error("protected members cannot be added or removed outside of the constructor"); //in case the function is referenced elsewhere - if(name === (void 0) || ""+name === "") throw new TypeError("argument 'name' is required"); - - options = new Object(options); - if(options.get !== (void 0) && typeof(options.get) !== "function") throw new TypeError("option 'get' is not a function"); - if(options.set !== (void 0) && typeof(options.set) !== "function") throw new TypeError("option 'set' is not a function"); - if(!options.get && !options.set){ - delete protectedObj[name]; + + function _defineAccessor(propertyName, accessModifier, getter, setter){ + + let classConstructor = this.constructor, + classScope = _scopes.get(classConstructor); + + if(classScope && classScope.hasOwnProperty(propertyName)){ + if(classScope[propertyName].accessModifier === accessModifier) return; + throw new Error("the access modifier of an existing property cannot be modified"); + } + if(!accessModifier || !xor(accessModifier & PRIVATE, accessModifier & PROTECTED)){ + throw new Error("invalid access modifier"); + } + + if(!classScope){ + classScope = {}; + _scopes.set(classConstructor, classScope); + } + classScope[propertyName].accessModifier = accessModifier; + if(accessModifier & STATIC){ + Object.defineProperty(classScope[propertyName], "value", { + get:getter, set:setter, enumerable:true, configurable:true + }); } else{ - protectedObj[name] = { get: options.get, set: options.set }; + classScope[propertyName].instances = new WeakMap(); + Object.defineProperty(classScope[propertyName].instances[this], "value", { + get:getter, set:setter, enumerable:true, configurable:true + }); } + } - + function _getAccessors(instance){ + + if(instance === void 0){ + instance = this; + } + else if(!(instance instanceof _baseClass)){ + throw new Error("argument is not an instance of Class"); + } + + let requestedScope = _scopes.get(instance.constructor), + returnedScope = {}; + + for(let key in requestedScope){ + let accessModifier = requestedScope[key].accessModifier; + if( ((accessModifier & PRIVATE) && this.constructor === instance.constructor) + || ((accessModifier & PROTECTED) && this instanceof instance.constructor) ){ + if(accessModifier & STATIC){ + Object.defineProperty(returnedScope, key, { + get:function (){ return requestedScope[key].value; }, + set:function (val){ + requestedScope[key].value = val; + return requestedScope[key].value; + }, + enumerable:true, configurable:true + }); + } + else{ + Object.defineProperty(returnedScope, key, { + get:function (){ return requestedScope[key].instances[instance].value; }, + set:function (val){ + requestedScope[key].instances[instance].value = val; + return requestedScope[key].instances[instance].value; + }, + enumerable:true, configurable:true + }); + } + } + } + + return returnedScope; + + } + /*** base class ***/ @@ -57,6 +119,11 @@ defineProperty(_baseClass.prototype, "toString", _instanceToString, true, false, true); defineProperty(_baseClass, "toString", _classToString, true, false, true); + defineProperty(_baseClass, "accessModifier", {}, false, false, true); + defineProperty(_baseClass.accessModifier, "PRIVATE", PRIVATE, false, false, true); + defineProperty(_baseClass.accessModifier, "PROTECTED", PROTECTED, false, false, true); + defineProperty(_baseClass.accessModifier, "STATIC", STATIC, false, false, true); + /** * Creates a new class that inherits from the parent class. @@ -107,30 +174,18 @@ //initialize the instance using the parent class protectedMembers = newClass.prototype.constructor.apply(newInstance, arguments) || {}; - //add the protected getters/setters to superFn - for(let name in protectedMembers){ - if(Object.prototype.hasOwnProperty.call(protectedMembers, name)){ - Object.defineProperty(superFn, name, { - get:(protectedMembers[name].get ? protectedMembers[name].get.bind(newInstance) : void 0), - set:(protectedMembers[name].set ? protectedMembers[name].set.bind(newInstance) : void 0), - enumerable:true, configurable:true - }); - } - } - - defineProperty(superFn, "defineProtectedMember", _defineProtectedMember.bind(null, protectedMembers), false, false, true); + defineProperty(superFn, "getAccessors", _getAccessors.bind(newInstance), false, false, true); + defineProperty(superFn, "defineAccessor", _defineAccessor.bind(newInstance), false, false, true); } - let className = newClass.name; //store the provided class name in case the constructor changes the .name attribute - //construct the new instance _initializing = true; //$constructorFn.bind(newInstance, superFn).apply(null, arguments); $constructorFn.apply(newInstance, [superFn].concat([].slice.call(arguments))); //(This way it doesn't create another new function every time a constructor is run.) if(!superFnCalled && !$warnedAboutSuper){ - warn(className+" constructor does not call Super()"); + warn(newClass.name+" constructor does not call Super()"); $warnedAboutSuper = true; //prevent multiple warnings about the same issue } @@ -161,8 +216,7 @@ //override .name defineProperty(newClass, "name", - classNameIsValid(options.className) ? options.className : classNameIsValid(this.name) ? this.name /*parent class' name*/ : "Class", - false, false, true); + classNameIsValid(options.className) ? options.className : this.name /*parent class' name*/, false, false, true); //override .toString() defineProperty(newClass, "toString", _classToString, true, false, true); From fb6f9c882f862b08a191b4a509fb56ad94e0b31d Mon Sep 17 00:00:00 2001 From: Andy Harrison Date: Thu, 3 Oct 2019 16:53:04 -0400 Subject: [PATCH 2/3] - allow modification of classScope getters & setters --- src/Class.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Class.js b/src/Class.js index 2b843a8..37abcab 100644 --- a/src/Class.js +++ b/src/Class.js @@ -41,8 +41,7 @@ let classConstructor = this.constructor, classScope = _scopes.get(classConstructor); - if(classScope && classScope.hasOwnProperty(propertyName)){ - if(classScope[propertyName].accessModifier === accessModifier) return; + if(classScope && classScope.hasOwnProperty(propertyName) && classScope[propertyName].accessModifier !== accessModifier){ throw new Error("the access modifier of an existing property cannot be modified"); } if(!accessModifier || !xor(accessModifier & PRIVATE, accessModifier & PROTECTED)){ From 528d44138e90768eb5cb487d130df6057580f638 Mon Sep 17 00:00:00 2001 From: Andy Harrison Date: Wed, 30 Oct 2019 11:57:23 -0400 Subject: [PATCH 3/3] - rework the protected variables functionality --- README.md | 90 +++++---------------------- src/Class.js | 105 ++++++-------------------------- test/Class testing.htm | 42 +++++++------ test/Privates using symbols.htm | 43 ++++++------- test/examples.htm | 7 ++- 5 files changed, 86 insertions(+), 201 deletions(-) diff --git a/README.md b/README.md index e355e2d..7a7bef7 100644 --- a/README.md +++ b/README.md @@ -40,8 +40,8 @@ let Rectangle = Class.extend({ Super(); this.width = width||0; this.height = height||0; - Object.defineProperty(this, "area", { get:function (){ return Math.abs(this.width * this.height); }, enumerable:true, configurable:true }); - Object.defineProperty(this, "whatAmI", { get:function (){ return "I am a rectangle."; }, enumerable:true, configurable:true }); + this.area = function (){ return Math.abs(this.width * this.height); }; + this.whatAmI = function (){ return "I am a rectangle."; }; }, returnFn:function (width, height){ return Math.abs((width||0) * (height||0)); @@ -53,8 +53,8 @@ let Square = Rectangle.extend({ constructorFn:function (Super, width){ Super(width, width); Object.defineProperty(this, "height", { get:function (){ return this.width; }, set:function (val){ this.width = 1*val||0; }, enumerable:true, configurable:true }); - let iAm = [this.whatAmI, "I am a square."].join(" "); - Object.defineProperty(this, "whatAmI", { get:function (){ return iAm; }, enumerable:true, configurable:true }); + let iAm = [this.whatAmI(), "I am a square."].join(" "); + this.whatAmI = function (){ return iAm; }; }, returnFn:function (width){ return Math.pow(width||0, 2); @@ -64,22 +64,16 @@ let Square = Rectangle.extend({ let s = new Square(3); s.toString(); //[instance of Square] -s.area; //9 +s.area(); //9 s.height = 4; -s.area; //16 -s.whatAmI; //I am a rectangle. I am a square. +s.area(); //16 +s.whatAmI(); //I am a rectangle. I am a square. ``` ### Protected members -Additionally, descendant classes can be given protected access to items in a super-class' constructor. This is done by providing getters and setters that are inherited. Once *Super*() is called within the constructor, the protected properties are made available as static properties of *Super*. The function also gains a method that allows you to grant protected access to (or revoke access from) descendant classes. - -***Super*.defineProtectedMember(*name*[, *options*])** - -Adds a getter and/or setter that will be accessible within the constructors of descendant classes. If neither is specified, the protected member is removed so that it is not accessible from any descendants of this class. - -*options* is an object with two optional methods, get and set. +Additionally, a class can give its descendants protected access to its private variables. Once *Super*() is called within the constructor, the protected properties of its parent class are made available via *Super*.protected. This object will be available to child classes as well; any additions to or deletions of its members that are made here in the constructor will be reflected in the class' descendants. #### Example @@ -89,7 +83,7 @@ let Alpha = Class.extend({ constructorFn:function (Super){ Super(); let randomInstanceID = Math.random(); - Super.defineProtectedMember("rando", { get:function(){return randomInstanceID} }); + Super.protected.rando = randomInstanceID; } }); @@ -97,7 +91,7 @@ let Bravo = Alpha.extend({ className:"Bravo", constructorFn:function (Super){ Super(); - this.foo = "My ID is "+Super.rando; + this.foo = "My ID is "+Super.protected.rando; } }); @@ -112,82 +106,30 @@ b.foo; //My ID is ... ### Private members -A WeakMap or a symbol can be used to implement private members for the class, allowing functions defined both inside and outside of the constructor to share data. This can also be used to pass along access to the protected members. +A WeakMap or a symbol can be used to implement private members for class instances, allowing functions defined both inside and outside of the constructor to share data. #### Example using a WeakMap ``` -let Alpha = Class.extend({ - constructorFn:function (Super){ - Super(); - let foo = "foo"; - Super.defineProtectedMember("foo", { get:function(){return foo} }); - } -}); - -let Bravo = (function (){ +let Cuber = (function (){ const private = new WeakMap(); function cube(){ return Math.pow(private.get(this).val, 3); } - return Alpha.extend({ - constructorFn: function (Super, myVal){ - Super(); - let that = this; - private.set(this, { - val: myVal, - square: function (){ return Math.pow(private.get(that).val, 2); }, - protected: Super - }); - this.cube = cube; - this.test = function (){ console.log(private.get(this).val, private.get(this).square(), this.cube(), private.get(this).protected.foo); }; - } - }); - -})(); - -let b = new Bravo(5); - -b.test() //5 25 125 "foo" -``` - -#### Example using a symbol - -``` -let Alpha = Class.extend({ - constructorFn:function (Super){ - Super(); - let foo = "foo"; - Super.defineProtectedMember("foo", { get:function(){return foo} }); - } -}); - -let Bravo = (function (){ - - const private = Symbol(); - - function cube(){ return Math.pow(this[private].val, 3); } - - return Alpha.extend({ + return Class.extend({ constructorFn: function (Super, myVal){ Super(); - let that = this; - this[private] = { - val: myVal, - square: function (){ return Math.pow(that[private].val, 2); }, - protected: Super - }; + private.set(this, { val: myVal }); this.cube = cube; - this.test = function (){ console.log(this[private].val, this[private].square(), this.cube(), this[private].protected.foo); }; } }); })(); -let b = new Bravo(5); +let c = new Cuber(5); -b.test() //5 25 125 "foo" +c.cube(); //125 ``` diff --git a/src/Class.js b/src/Class.js index 37abcab..8ac665b 100644 --- a/src/Class.js +++ b/src/Class.js @@ -36,93 +36,28 @@ }; let _extendToString = function toString(){ return "function extend() { [custom code] }"; }; - function _defineAccessor(propertyName, accessModifier, getter, setter){ - - let classConstructor = this.constructor, - classScope = _scopes.get(classConstructor); - - if(classScope && classScope.hasOwnProperty(propertyName) && classScope[propertyName].accessModifier !== accessModifier){ - throw new Error("the access modifier of an existing property cannot be modified"); - } - if(!accessModifier || !xor(accessModifier & PRIVATE, accessModifier & PROTECTED)){ - throw new Error("invalid access modifier"); - } - - if(!classScope){ - classScope = {}; - _scopes.set(classConstructor, classScope); - } - classScope[propertyName].accessModifier = accessModifier; - if(accessModifier & STATIC){ - Object.defineProperty(classScope[propertyName], "value", { - get:getter, set:setter, enumerable:true, configurable:true - }); - } - else{ - classScope[propertyName].instances = new WeakMap(); - Object.defineProperty(classScope[propertyName].instances[this], "value", { - get:getter, set:setter, enumerable:true, configurable:true + function _generateProtectedAccessorsForSubclass(protectedAccessors_parent = {}){ + let protectedAccessors_child = {}; + for(let key in protectedAccessors_parent){ + Object.defineProperty(protectedAccessors_child, key, { + get: ()=>protectedAccessors_parent[key], + set: value=>(protectedAccessors_parent[key] = value), + enumerable:true, configurable:true }); } - - } - function _getAccessors(instance){ - - if(instance === void 0){ - instance = this; - } - else if(!(instance instanceof _baseClass)){ - throw new Error("argument is not an instance of Class"); - } - - let requestedScope = _scopes.get(instance.constructor), - returnedScope = {}; - - for(let key in requestedScope){ - let accessModifier = requestedScope[key].accessModifier; - if( ((accessModifier & PRIVATE) && this.constructor === instance.constructor) - || ((accessModifier & PROTECTED) && this instanceof instance.constructor) ){ - if(accessModifier & STATIC){ - Object.defineProperty(returnedScope, key, { - get:function (){ return requestedScope[key].value; }, - set:function (val){ - requestedScope[key].value = val; - return requestedScope[key].value; - }, - enumerable:true, configurable:true - }); - } - else{ - Object.defineProperty(returnedScope, key, { - get:function (){ return requestedScope[key].instances[instance].value; }, - set:function (val){ - requestedScope[key].instances[instance].value = val; - return requestedScope[key].instances[instance].value; - }, - enumerable:true, configurable:true - }); - } - } - } - - return returnedScope; - + //protectedAccessors_child.foo = 'test'; + return protectedAccessors_child; } /*** base class ***/ //the base Class constructor; it will have two static methods, 'extend' and 'noConflict' - let _baseClass = function Class(){} + let _baseClass = function Class(){}; defineProperty(_baseClass.prototype, "toString", _instanceToString, true, false, true); defineProperty(_baseClass, "toString", _classToString, true, false, true); - defineProperty(_baseClass, "accessModifier", {}, false, false, true); - defineProperty(_baseClass.accessModifier, "PRIVATE", PRIVATE, false, false, true); - defineProperty(_baseClass.accessModifier, "PROTECTED", PROTECTED, false, false, true); - defineProperty(_baseClass.accessModifier, "STATIC", STATIC, false, false, true); - /** * Creates a new class that inherits from the parent class. @@ -162,8 +97,8 @@ defineProperty(newInstance, "constructor", newClass, true, false, true); } - - let protectedMembers, + + let protectedAccessors, superFnCalled = false; let superFn = function Super(){ @@ -171,13 +106,16 @@ superFnCalled = true; //initialize the instance using the parent class - protectedMembers = newClass.prototype.constructor.apply(newInstance, arguments) || {}; + protectedAccessors = newClass.prototype.constructor.apply(newInstance, arguments) || {}; - defineProperty(superFn, "getAccessors", _getAccessors.bind(newInstance), false, false, true); - defineProperty(superFn, "defineAccessor", _defineAccessor.bind(newInstance), false, false, true); + //add protected value accessors to the Super function + Object.defineProperty(superFn, "protected", { + get: ()=>protectedAccessors, + enumerable:false, configurable:false + }); } - + //construct the new instance _initializing = true; //$constructorFn.bind(newInstance, superFn).apply(null, arguments); @@ -192,14 +130,11 @@ //this function is the constructor of the new instance _initializing = false; - - //In case the 'Super' argument gets referenced elsewhere, remove this since it's not allowed to be used outside of the constructor anyway. - delete superFn.defineProtectedMember; } else{ //this function is the constructor of a super-class - return protectedMembers; + return _generateProtectedAccessorsForSubclass(protectedAccessors); } //else return this diff --git a/test/Class testing.htm b/test/Class testing.htm index 1293d8c..7437b8b 100644 --- a/test/Class testing.htm +++ b/test/Class testing.htm @@ -62,11 +62,12 @@ console.group("Delta class"); Delta = Class.extend({className:"Delta", - returnFn:function (){return "foo"}, - extensions:{ - bar:function (){return "bar"}, - baz:function (){return this.bar()} - } + constructorFn:function (Super){ + Super(); + this.bar = function (){return "bar"}; + this.baz = function (){return this.bar()}; + }, + returnFn:function (){return "foo"} }); console.dir(Delta); console.groupEnd(); @@ -93,10 +94,10 @@ console.group("Echo class"); Echo = Class.extend({className:"Echo", - returnFn:function (){return this.foo}, - extensions:{ - foo:function (){return "foo"} - } + constructorFn:function (){ + this.foo = function (){return "foo"}; + }, + returnFn:function (){return this.foo} }); console.dir(Echo); console.groupEnd(); @@ -206,7 +207,12 @@ Super(); let foo="bar"; this.bop = function (){return foo}; - Super.defineProtectedMember("foo", { get:function (){return foo}, set:function(v){foo=v} }); //subclasses of Lima will have access to the 'foo' variable + Object.defineProperty(Super.protected, "foo", { + get:function (){return foo}, + set:function(v){foo=v}, + enumerable:true, configurable:true + }); //subclasses of Lima will have access to the 'foo' variable + Super.protected.foo = foo; } }); console.dir(Lima); @@ -216,11 +222,13 @@ Mike = Lima.extend({className:"Mike", constructorFn:function(Super){ Super(); - Super.defineProtectedMember("foo"); //subclasses of Mike will not have access to the protected property - console.assert(Super.foo === "bar", Super.foo); //Mike constructor has access to the protected property - Super.foo = "baz"; - console.assert(Super.foo === "baz", Super.foo); //protected property can be changed via the Mike constructor + let protected = Super.protected; + console.log('Super.protected', protected); + console.assert(protected.foo === "bar", protected.foo); //Mike constructor has access to the protected foo value + protected.foo = "baz"; + console.assert(protected.foo === "baz", protected.foo); //protected foo value can be changed via the Mike constructor console.assert(this.bop() === "baz", this.bop()); //confirms that the value in the Lima constructor's variable is what changed + delete protected.foo; //subclasses of Mike will not have access to the protected foo value } }); console.dir(Mike); @@ -229,15 +237,15 @@ console.group("Mike instance"); mike1 = new Mike(); console.dir(mike1); - console.assert(mike1.foo === void 0, mike1.foo); //instance doesn't have access to the protected property - console.assert(mike1.bop() === "baz", mike1.bop()); //instance's constructor-created method does have access to the protected property + console.assert(mike1.foo === void 0, mike1.foo); //instance doesn't have access to the protected foo value + console.assert(mike1.bop() === "baz", mike1.bop()); //instance's constructor-created method does have access to the protected foo value console.groupEnd(); console.group("November class"); November = Mike.extend({className:"November", constructorFn:function(Super){ Super(); - console.assert(Super.foo === void 0, Super.foo); //class November doesn't have access to the protected property + console.assert(Super.protected.foo === void 0, Super.protected.foo); //class November doesn't have access to the protected foo value console.assert(this.bop() === "baz", this.bop()); //inherited function still has access } }); diff --git a/test/Privates using symbols.htm b/test/Privates using symbols.htm index 404f0d2..28e5910 100644 --- a/test/Privates using symbols.htm +++ b/test/Privates using symbols.htm @@ -3,7 +3,11 @@ let Alpha = (function (){ - const private = Symbol("private members of Alpha"); + const private = Symbol("private members of Alpha instances"); + + function getMyVal(){ + return this[private].val; + } return Class.extend({ className: "Alpha", @@ -11,12 +15,11 @@ Super(); this[private] = {}; this[private].val = myVal; - Super.defineProtectedMember("myVal", { get:function (){ return this[private].val; } }); - }, - extensions: { - getMyVal: function (){ - return this[private].val; - } + Object.defineProperty(Super.protected, "myVal", { + get: ()=>this[private].val, + enumerable:true, configurable:true + }); + this.getMyVal = getMyVal; } }); @@ -24,30 +27,25 @@ let Bravo = (function (){ - const private = Symbol("private members of Bravo"); + const private = Symbol("private members of Bravo instances"); - function viaStandalone(){ + function viaPrivate(){ return this[private].getVal(); } + function viaReferenceToProtected(){ + return this[private].protected.myVal; //Super.protected.myVal + } return Alpha.extend({ className: "Bravo", constructorFn: function (Super, myVal){ Super(myVal); - this.viaConstructor = function (){ return Super.myVal; }; + this.viaConstructor = function (){ return Super.protected.myVal; }; this[private] = {}; - this[private].getVal = function (){ return Super.myVal; }; - this[private].protected = Super; - this.viaStandalone = viaStandalone; - }, - extensions: { - viaPrivate: function (){ - return this[private].getVal(); - }, - viaReferenceToProtected: function (){ - console.assert(this[private].protected.defineProtectedMember === void 0, ".defineProtectedMember() was not deleted from Super"); - return this[private].protected.myVal; //Super.myVal - } + this[private].getVal = function (){ return Super.protected.myVal; }; + this[private].protected = Super.protected; + this.viaPrivate = viaPrivate, + this.viaReferenceToProtected = viaReferenceToProtected } }); @@ -64,7 +62,6 @@ console.assert(b.viaConstructor() === 10, "viaConstructor()", b.viaConstructor()); console.assert(b.viaPrivate() === 10, "viaPrivate()", b.viaPrivate()); console.assert(b.viaReferenceToProtected() === 10, "viaReferenceToProtected()", b.viaReferenceToProtected()); - console.assert(b.viaStandalone() === 10, "viaStandalone()", b.viaStandalone()); console.groupEnd(); diff --git a/test/examples.htm b/test/examples.htm index 9ada50f..2a14339 100644 --- a/test/examples.htm +++ b/test/examples.htm @@ -44,7 +44,10 @@ constructorFn:function (Super){ Super(); let randomInstanceID = Math.random(); - Super.defineProtectedMember("rando", { get:function(){return randomInstanceID} }); + Object.defineProperty(Super.protected, "rando", { + get:function(){return randomInstanceID}, + enumerable:true, configurable:true + }); } }); @@ -52,7 +55,7 @@ className:"Bravo", constructorFn:function (Super){ Super(); - this.foo = "My ID is "+Super.rando; + this.foo = "My ID is "+Super.protected.rando; } });