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 87e76a3..8ac665b 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,31 +32,28 @@ 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]; - } - else{ - protectedObj[name] = { get: options.get, set: options.set }; + + 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 + }); } + //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); @@ -96,8 +97,8 @@ defineProperty(newInstance, "constructor", newClass, true, false, true); } - - let protectedMembers, + + let protectedAccessors, superFnCalled = false; let superFn = function Super(){ @@ -105,24 +106,15 @@ superFnCalled = true; //initialize the instance using the parent class - protectedMembers = newClass.prototype.constructor.apply(newInstance, arguments) || {}; + protectedAccessors = 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); + //add protected value accessors to the Super function + Object.defineProperty(superFn, "protected", { + get: ()=>protectedAccessors, + enumerable:false, configurable:false + }); } - - let className = newClass.name; //store the provided class name in case the constructor changes the .name attribute //construct the new instance _initializing = true; @@ -130,7 +122,7 @@ $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 } @@ -138,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 @@ -161,8 +150,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); 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; } });