Skip to content

Commit

Permalink
Merge pull request #2 from wizard04wsu/access-modifiers
Browse files Browse the repository at this point in the history
Protected values reworked
  • Loading branch information
wizard04wsu authored Oct 30, 2019
2 parents 3ef1e1c + 528d441 commit 5baadf7
Show file tree
Hide file tree
Showing 5 changed files with 96 additions and 158 deletions.
90 changes: 16 additions & 74 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand All @@ -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);
Expand All @@ -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 <code>*Super*()</code> is called within the constructor, the protected properties are made available as static properties of <code>*Super*</code>. The function also gains a method that allows you to grant protected access to (or revoke access from) descendant classes.

**<samp>*Super*.defineProtectedMember(*name*[, *options*])</samp>**

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.

<code>*options*</code> is an object with two optional methods, <code>get</code> and <code>set</code>.
Additionally, a class can give its descendants protected access to its private variables. Once <code>*Super*()</code> is called within the constructor, the protected properties of its parent class are made available via <code>*Super*.protected</code>. 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

Expand All @@ -89,15 +83,15 @@ let Alpha = Class.extend({
constructorFn:function (Super){
Super();
let randomInstanceID = Math.random();
Super.defineProtectedMember("rando", { get:function(){return randomInstanceID} });
Super.protected.rando = randomInstanceID;
}
});
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;
}
});
Expand All @@ -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
```


Expand Down
72 changes: 30 additions & 42 deletions src/Class.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@

"use strict";

let _initializing = false;
const PRIVATE = 4, PROTECTED = 2, STATIC = 1;

let _initializing = false,
_scopes = new WeakMap();


/*** helper functions ***/
Expand All @@ -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 ***/
Expand All @@ -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);
Expand Down Expand Up @@ -96,56 +97,44 @@

defineProperty(newInstance, "constructor", newClass, true, false, true);
}

let protectedMembers,
let protectedAccessors,
superFnCalled = false;
let superFn = function Super(){

if(superFnCalled) return; //don't initialize it more than once
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;
//$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
}

if(newInstance.constructor === newClass){
//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

Expand All @@ -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);
Expand Down
42 changes: 25 additions & 17 deletions test/Class testing.htm
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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();
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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
}
});
Expand Down
Loading

0 comments on commit 5baadf7

Please sign in to comment.