From f52020cc19ba5e83a459aeba0d0671b6e48d8a02 Mon Sep 17 00:00:00 2001 From: wizard04wsu Date: Wed, 19 May 2021 07:31:24 -0400 Subject: [PATCH 01/18] started changes, then a complete rewrite --- src/Class.mjs.js | 163 +++++++++++++++++++----------------------- src/Class2.mjs.js | 121 +++++++++++++++++++++++++++++++ test/Class testing.js | 51 +++++-------- 3 files changed, 212 insertions(+), 123 deletions(-) create mode 100644 src/Class2.mjs.js diff --git a/src/Class.mjs.js b/src/Class.mjs.js index 7b0657a..102b536 100644 --- a/src/Class.mjs.js +++ b/src/Class.mjs.js @@ -9,165 +9,148 @@ let _initializing = false; function defineProperty(object, propertyName, value, isWritable, isEnumerable, isConfigurable){ Object.defineProperty(object, propertyName, { value:value, writable:isWritable, enumerable:isEnumerable, configurable:isConfigurable }); } -function isPrimitive(o){ let t = typeof o; return (t !== "object" && t !== "function" ) || o === null; } -function warn(msg){ if(console) (console.warn || console.log)(msg); } - -function classNameIsValid(className){ -//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 defineNonEnumerableProperty(object, propertyName, value){ + defineProperty(object, propertyName, value, true, false, true); } - -/*** shared functions ***/ - -function _constructorFn(Super){ Super.apply(null, [].slice.call(arguments, 1)); } -function _emptyFn(){} -let _classToString = function toString(){ return "function Class() { [custom code] }"; }; -let _instanceToString = function toString(){ - return "[instance of "+this.constructor.name+"]"; -}; -let _extendToString = function toString(){ return "function extend() { [custom code] }"; }; - -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; -} +function isPrimitive(o){ return o === null || (typeof(o) !== "object" && typeof(o) !== "function"); } /*** base class ***/ -//the base Class constructor; it will have two static methods, 'extend' and 'noConflict' let _baseClass = function Class(){}; -defineProperty(_baseClass.prototype, "toString", _instanceToString, true, false, true); -defineProperty(_baseClass, "toString", _classToString, true, false, true); +defineNonEnumerableProperty(_baseClass.prototype, "toString", function toString(){ return "[object Class]"; }); /** - * Creates a new class that inherits from the parent class. + * Creates a new class that inherits from the super class. * * @param {object} [options] * @param {string} [options.className] - Used as .name for the class function and in .toString() for instances of the class. - * @param {function} [options.constructorFn] - Initializes new instances of the class. A function is passed as the first argument, used to initialize the instance using the parent class' constructor; be sure to call it inside constructorFn (before using `this` or protected members). - * @param {function} [options.returnFn] - Returns a value when the constructor is called without using the 'new' keyword. + * @param {function} [options.constructor] - Initializes a new instance of the class. Its first argument will be a function that can be used to call the constructor of the super class (like a `super` keyword). It must be called before using `this`, or to gain access to protected members of the super class. If not specified, the default constructor will call the constructor of the super class, passing any arguments. + * @param {function} [options.function] - Returns a value when the class is called as a function instead of as a constructor (i.e., without using the 'new' keyword). If not specified, a TypeError will be thrown instead. * @return {function} - The new class. */ let _extendFn = function extend(options){ if(options === void 0) options = {}; - else if(isPrimitive(options)) throw new TypeError("argument 'options' is not an object"); - - + else if(isPrimitive(options)) throw new TypeError("options is not an object"); + + let $className = options.className; + if($className === void 0 || String($className)===""){ + if(options.constructor.name !== "constructor") $className = options.constructor.name; + $className = $className || this.name || "Class"; + } + + let $constructor = options.constructor; + if($constructor === void 0){ + //default contructor calls the constructor of the super class, passing any remaining arguments + $constructor = function ($super){ $super.apply(void 0, [].slice.call(arguments, 1)); }; + } + else if(typeof($constructor) !== "function") throw new TypeError("options.constructor is not a function"); + + let $function = options.function; + if($function === void 0){ + $function = () => throw new TypeError(`${$className} class constructor cannot be invoked without 'new'`); + } + else if(typeof(options.function) !== "function") throw new TypeError("options.function is not a function"); + + /*** create the new constructor ***/ - - let $constructorFn = typeof(options.constructorFn) === "function" ? options.constructorFn : _constructorFn; - let $returnFn = typeof(options.returnFn) === "function" ? options.returnFn : _emptyFn; - let $warnedAboutSuper = false; - + let newClass = function Class(){ if(this && this instanceof newClass && (this.constructor === newClass.prototype.constructor || _initializing)){ - //A new instance is being created; initialize it. - //This condition will be true in these cases: - // 1) The 'new' operator was used to instantiate this class - // 2) The 'new' operator was used to instantiate a subclass, and the subclass' $constructorFn() calls its first argument (the bound superFn) - // 3) The 'new' operator was used to instantiate a subclass, and the subclass' $constructorFn() includes something like `MySuperClass.call(this)` - // 4) Possibly if the prototype chain has been manipulated + //A new instance is being created; initialize it. + //This condition will be true in these cases: + // 1) The 'new' operator was used to instantiate this class + // 2) The 'new' operator was used to instantiate a subclass, and the subclass' constructor calls the constructor of its super class + // 3) The 'new' operator was used to instantiate a subclass, and the subclass' constructor includes something like `MySuperClass.call(this)` + // 4) Possibly if the prototype chain has been manipulated let newInstance = this; if(newInstance.constructor === newClass.prototype.constructor){ - //this function is the constructor of the new instance (i.e., it's not a parent class' constructor) + //this function is the constructor of the new instance (i.e., it's not a super class' constructor) - defineProperty(newInstance, "constructor", newClass, true, false, true); + defineNonEnumerableProperty(newInstance, "constructor", newClass); } - let protectedAccessors, - superFnCalled = false; - let superFn = function Super(){ - - if(superFnCalled) return; //don't initialize it more than once + let superFnCalled = false; + function $super(){ + if(superFnCalled) throw new ReferenceError("Super constructor may only be called once"); superFnCalled = true; - //initialize the instance using the parent class - protectedAccessors = newClass.prototype.constructor.apply(newInstance, arguments) || {}; - - //add protected value accessors to the Super function - Object.defineProperty(superFn, "protected", { - get: ()=>protectedAccessors, - enumerable:false, configurable:false - }); + //initialize the instance using the super class + let protectedAccessors = newClass.prototype.constructor.apply(newInstance, arguments) || {}; + //add the protected value accessors to `$super` + defineProperty($super, "protected", {}, false, false, false); + for(const key in protectedAccessors){ + $super.protected[key] = protectedAccessors[key]; + } } //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.) + $constructor.apply(newInstance, [$super].concat([].slice.call(arguments))); - if(!superFnCalled && !$warnedAboutSuper){ - warn(newClass.name+" instance is not initialized by its parent class"); - $warnedAboutSuper = true; //prevent multiple warnings about the same issue - } + if(!superFnCalled) throw new ReferenceError("Must call super constructor in derived class before accessing 'this' or returning from derived constructor"); if(newInstance.constructor === newClass){ - //this function is the constructor of the new instance + //this function is the constructor of the new instance _initializing = false; } else{ - //this function is the constructor of a super-class + //this function is the constructor of a super class - return _generateProtectedAccessorsForSubclass(protectedAccessors); + //generate protected value accessors for the subclass + let protectedAccessors = {}; + for(const key in $super.protected){ + Object.defineProperty(protectedAccessors, key, { + get: ()=>$super[key], + set: value=>($super[key] = value), + enumerable:true, configurable:false + }); + } + return protectedAccessors; } - //else return this } else{ - //the 'new' operator was not used; it was just called as a function + //the 'new' operator was not used; it was just called as a function - return $returnFn.apply(null, arguments); + return $function.apply(void 0, arguments); } } //override .name - defineProperty(newClass, "name", - classNameIsValid(options.className) ? options.className : this.name /*parent class' name*/, false, false, true); + defineProperty(newClass, "name", $className, false, false, true); - //override .toString() - defineProperty(newClass, "toString", _classToString, true, false, true); - if(this.extend === _extendFn){ - //the 'extend' method of the parent class was not modified + //the 'extend' method of the super class was not modified //make extend() a static method of the new class - defineProperty(newClass, "extend", _extendFn, true, false, true); + defineNonEnumerableProperty(newClass, "extend", _extendFn); } /*** create the new prototype ***/ - //An uninitialized instance of the parent class will be the prototype of the new class. + //An uninitialized instance of the super class will be the prototype of the new class. //To create an instance without initializing it, we'll temporarily use an empty function as the constructor. let emptyFn = function (){}; emptyFn.prototype = this.prototype; let newPrototype = new emptyFn(); - emptyFn = null; - defineProperty(newPrototype, "constructor", this, true, false, true); + defineNonEnumerableProperty(newPrototype, "constructor", this); //override .toString() - defineProperty(newPrototype, "toString", _instanceToString, true, false, true); - + defineNonEnumerableProperty(newPrototype, "toString", function toString(){ return `[object ${$className}]`; }); + defineProperty(newClass, "prototype", newPrototype, false, false, false); @@ -175,10 +158,8 @@ let _extendFn = function extend(options){ } -defineProperty(_extendFn, "toString", _extendToString, true, false, true); - //make extend() a static method of Class -defineProperty(_baseClass, "extend", _extendFn, true, false, true); +defineNonEnumerableProperty(_baseClass, "extend", _extendFn); diff --git a/src/Class2.mjs.js b/src/Class2.mjs.js new file mode 100644 index 0000000..d4605c0 --- /dev/null +++ b/src/Class2.mjs.js @@ -0,0 +1,121 @@ +/* eslint-env es6 */ +// https://github.com/wizard04wsu/Class + +//let _initializing = false; +const protected = Symbol("protected members"); + + +/*** helper functions ***/ + +function isPrimitive(o){ return o === null || (typeof(o) !== "object" && typeof(o) !== "function"); } + + +/*** base class ***/ + +function Class(){ + Object.defineProperty(this, protected, { + writable: false, enumerable: false, configurable: true, + value: {} + }); +}; + +Object.defineProperty(Class.prototype, "toString", { + writable: true, enumerable: false, configurable: true, + value: function toString(){ return "[object "+this.constructor.name+"]"; } +}); + +Object.defineProperty(Class, "extend", { + writable: true, enumerable: false, configurable: true, + value: extend +}); + + +/** + * Creates a new class that inherits from the super class. + * + * @param {object} [options] + * @param {string} [options.className] - Used as .name for the class function and in .toString() for instances of the class. + * @param {function} [options.constructor] - Initializes a new instance of the class. Its first argument will be a function that can be used to call the constructor of the super class (like a `super` keyword). It must be called before using `this`, or to gain access to protected members of the super class. If not specified, the default constructor will call the constructor of the super class, passing any arguments. + * @param {function} [options.function] - Returns a value when the class is called as a function instead of as a constructor (i.e., without using the 'new' keyword). If not specified, a TypeError will be thrown instead. + * @return {function} - The new class. + */ +function extend(constructor, applier){ + if(typeof this !== "function" || !(this.prototype instanceof Class)) + throw new TypeError("extend requires that 'this' be a Class constructor"); + if(typeof constructor !== "function") + throw new TypeError("constructor is not a function"); + if(arguments.length > 1 && typeof applier !== "function") + throw new TypeError("applier is not a function"); + + const newClass = new Proxy(constructor, { + construct(target, argumentsList, newTarget){ //target===constructor, newTarget===the proxy (newClass) + let supCalled; + + const sup = new Proxy(target.prototype.constructor, { + apply(target, thisArg, [newInstance, ...argumentsList]){ //target===the super constructor + if(supCalled) throw new ReferenceError("Super constructor may only be called once"); + supCalled = true; + + target.apply(newInstance, argumentsList); + }, + get(target, property, receiver){ //target===the super constructor, receiver===the proxy (sup) + if(property === "_protected"){ + if(!supCalled) throw new ReferenceError("Must call super constructor from derived constructor before accessing protected members"); + return target[protected]; + } + return Reflect.get(...arguments); + }, + set(target, property, value){ //target===the super constructor + if(property === "_protected") return false; + return Reflect.set(...arguments); + }, + deleteProperty(target, property){ //target===the super constructor + throw new ReferenceError("invalid delete involving super constructor"); + } + }); + + const newInstance = Reflect.construct(target, [sup, ...argumentsList], newTarget); + + if(!supCalled) throw new ReferenceError("Must call super constructor before returning from derived constructor"); + + return newInstance; + }, + apply(target, thisArg, argumentsList){ //target===constructor + return typeof applier === "function" ? applier.apply(thisArg, argumentsList) : void 0; + } + }); + + newClass.prototype = Object.create(this.prototype); + + Object.defineProperty(newClass, "name", { + writable: false, enumerable: false, configurable: true, + value: constructor.name + }); +} + + + + + + + /*** create the new prototype ***/ + + //An uninitialized instance of the super class will be the prototype of the new class. + //To create an instance without initializing it, we'll temporarily use an empty function as the constructor. + let emptyFn = function (){}; + emptyFn.prototype = this.prototype; + let newPrototype = new emptyFn(); + defineNonEnumerableProperty(newPrototype, "constructor", this); + + //override .toString() + defineNonEnumerableProperty(newPrototype, "toString", function toString(){ return `[object ${$className}]`; }); + + defineProperty(newClass, "prototype", newPrototype, false, false, false); + + + return newClass; + + + + +export { _baseClass as default }; diff --git a/test/Class testing.js b/test/Class testing.js index c775a86..7678c6f 100644 --- a/test/Class testing.js +++ b/test/Class testing.js @@ -61,12 +61,12 @@ console.groupCollapsed("D - return function"); console.group("Delta class"); Delta = Class.extend({className:"Delta", - constructorFn:function (Super){ + constructor:function (Super){ Super(); this.bar = function (){return "bar"}; this.baz = function (){return this.bar()}; }, - returnFn:function (){return "foo"} + function:function (){return "foo"} }); console.dir(Delta); console.groupEnd(); @@ -93,10 +93,10 @@ console.groupCollapsed("E - return function using 'this'"); console.group("Echo class"); Echo = Class.extend({className:"Echo", - constructorFn:function (){ + constructor:function (){ this.foo = function (){return "foo"}; }, - returnFn:function (){return this.foo} + function:function (){return this.foo} }); console.dir(Echo); console.groupEnd(); @@ -114,14 +114,14 @@ console.groupCollapsed("F,G - subclass constructor without 'Super()'"); console.group("Foxtrot class"); Foxtrot = Class.extend({className:"Foxtrot", - constructorFn:function(Super){Super();this.foo = "foo"} + constructor:function($super){$super();this.foo = "foo"} }); console.dir(Foxtrot); console.groupEnd(); console.group("Golf class"); Golf = Class.extend({className:"Golf", - constructorFn:function(Super){} + constructor:function($super){} }); console.dir(Golf); console.groupEnd(); @@ -146,14 +146,14 @@ console.groupCollapsed("H,I - super-class constructor without 'Super()'"); console.group("Hotel class"); Hotel = Class.extend({className:"Hotel", - constructorFn:function(Super){this.foo = "foo"} + constructor:function($super){this.foo = "foo"} }); console.dir(Hotel); console.groupEnd(); console.group("India class"); India = Hotel.extend({className:"D", - constructorFn:function(Super){Super()} + constructor:function($super){$super()} }); console.dir(India); console.groupEnd(); @@ -177,14 +177,14 @@ console.groupCollapsed("J,K - constructors with 'Super()'"); console.group("Juliet class"); Juliet = Class.extend({className:"Juliet", - constructorFn:function(Super){Super();this.foo = "foo"} + constructor:function($super){$super();this.foo = "foo"} }); console.dir(Juliet); console.groupEnd(); console.group("Kilo class"); Kilo = Juliet.extend({className:"Kilo", - constructorFn:function(Super){Super()} + constructor:function($super){$super()} }); console.dir(Kilo); console.groupEnd(); @@ -202,16 +202,16 @@ console.groupCollapsed("L,M,N - protected properties"); console.group("Lima class"); Lima = Class.extend({className:"Lima", - constructorFn:function(Super){ - Super(); + constructor:function($super){ + $super(); let foo="bar"; this.bop = function (){return foo}; - Object.defineProperty(Super.protected, "foo", { + 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; + $super.protected.foo = foo; } }); console.dir(Lima); @@ -219,10 +219,10 @@ console.groupCollapsed("L,M,N - protected properties"); console.group("Mike class"); Mike = Lima.extend({className:"Mike", - constructorFn:function(Super){ + constructor:function($super){ Super(); - let $protected = Super.protected; - console.log('Super.protected', $protected); + 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 @@ -242,9 +242,9 @@ console.groupCollapsed("L,M,N - protected properties"); console.group("November class"); November = Mike.extend({className:"November", - constructorFn:function(Super){ + constructor:function($super){ Super(); - console.assert(Super.protected.foo === void 0, Super.protected.foo); //class November doesn't have access to the protected foo value + 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 } }); @@ -257,16 +257,3 @@ console.groupCollapsed("L,M,N - protected properties"); console.assert(november1.bop() === "baz", november1.bop()); //inherited function still has access console.groupEnd(); console.groupEnd(); - -//Class.noConflict() -console.groupCollapsed("O - noConflict"); - let Oscar; - - console.dir(Class); - console.assert(Object.getOwnPropertyDescriptor(Class, "noConflict").enumerable === false, Object.getOwnPropertyDescriptor(Class, "noConflict").enumerable); - Oscar = Class.noConflict(); - console.assert(Class === void 0, Class); - console.dir(Oscar); - console.assert(Object.getOwnPropertyDescriptor(Oscar, "noConflict") === void 0, Object.getOwnPropertyDescriptor(Oscar, "noConflict")); - console.assert(Oscar.noConflict === void 0, Oscar.noConflict); -console.groupEnd(); From c4c7af7f09b2a7fb8b7fe6df6147befeceb2b7d7 Mon Sep 17 00:00:00 2001 From: wizard04wsu Date: Thu, 20 May 2021 13:23:16 -0400 Subject: [PATCH 02/18] replaced older src and class tests with rewrites --- src/Class.mjs.js | 221 +++++++++++-------------- src/Class2.mjs.js | 121 -------------- test/Class testing.htm | 2 +- test/Class testing.js | 357 +++++++++++++---------------------------- 4 files changed, 210 insertions(+), 491 deletions(-) delete mode 100644 src/Class2.mjs.js diff --git a/src/Class.mjs.js b/src/Class.mjs.js index 102b536..cacc1b0 100644 --- a/src/Class.mjs.js +++ b/src/Class.mjs.js @@ -1,166 +1,131 @@ /* eslint-env es6 */ // https://github.com/wizard04wsu/Class -let _initializing = false; +const protectedMembers = Symbol("protected members"); +let _underConstruction = false; -/*** helper functions ***/ -function defineProperty(object, propertyName, value, isWritable, isEnumerable, isConfigurable){ - Object.defineProperty(object, propertyName, { value:value, writable:isWritable, enumerable:isEnumerable, configurable:isConfigurable }); +function Class(){ + Object.defineProperty(this, protectedMembers, { + writable: false, enumerable: false, configurable: true, + value: {} + }); } -function defineNonEnumerableProperty(object, propertyName, value){ - defineProperty(object, propertyName, value, true, false, true); -} - -function isPrimitive(o){ return o === null || (typeof(o) !== "object" && typeof(o) !== "function"); } - -/*** base class ***/ +Class.prototype.constructor = Class; -let _baseClass = function Class(){}; +Object.defineProperty(Class.prototype, "toString", { + writable: true, enumerable: false, configurable: true, + value: function toString(){ return `[object ${this.constructor.name}]`; } +}); -defineNonEnumerableProperty(_baseClass.prototype, "toString", function toString(){ return "[object Class]"; }); +Object.defineProperty(Class, "extend", { + writable: true, enumerable: false, configurable: true, + value: extend +}); /** * Creates a new class that inherits from the super class. * - * @param {object} [options] - * @param {string} [options.className] - Used as .name for the class function and in .toString() for instances of the class. - * @param {function} [options.constructor] - Initializes a new instance of the class. Its first argument will be a function that can be used to call the constructor of the super class (like a `super` keyword). It must be called before using `this`, or to gain access to protected members of the super class. If not specified, the default constructor will call the constructor of the super class, passing any arguments. - * @param {function} [options.function] - Returns a value when the class is called as a function instead of as a constructor (i.e., without using the 'new' keyword). If not specified, a TypeError will be thrown instead. + * @param {function} [constructor] - Initializes a new instance of the class. Its first argument will be a function that can be used to call the constructor of the super class (instead of the `super` keyword). It must be called in the constructor, it should be called before using `this`, and it must be called to gain access to protected members. + * @param {function} [applier] - Returns a value when the class is called without using the 'new' keyword. If not specified, a TypeError will be thrown instead of returning a value. * @return {function} - The new class. */ -let _extendFn = function extend(options){ - - if(options === void 0) options = {}; - else if(isPrimitive(options)) throw new TypeError("options is not an object"); - - let $className = options.className; - if($className === void 0 || String($className)===""){ - if(options.constructor.name !== "constructor") $className = options.constructor.name; - $className = $className || this.name || "Class"; - } - - let $constructor = options.constructor; - if($constructor === void 0){ - //default contructor calls the constructor of the super class, passing any remaining arguments - $constructor = function ($super){ $super.apply(void 0, [].slice.call(arguments, 1)); }; - } - else if(typeof($constructor) !== "function") throw new TypeError("options.constructor is not a function"); - - let $function = options.function; - if($function === void 0){ - $function = () => throw new TypeError(`${$className} class constructor cannot be invoked without 'new'`); - } - else if(typeof(options.function) !== "function") throw new TypeError("options.function is not a function"); - +function extend(constructor, applier){ + if(typeof this !== "function" || !(this === Class || this.prototype instanceof Class)) + throw new TypeError("extend requires that 'this' be a Class constructor"); + if(typeof constructor !== "function") + throw new TypeError("constructor is not a function"); + if(!constructor.name) + throw new TypeError("constructor must be a named function"); + if(arguments.length > 1 && typeof applier !== "function") + throw new TypeError("applier is not a function"); - /*** create the new constructor ***/ + const superClass = this; - let newClass = function Class(){ + function constructorWrapper(...argumentsList){ + const newInstance = this; + let _$superCalled = false; - if(this && this instanceof newClass && (this.constructor === newClass.prototype.constructor || _initializing)){ - //A new instance is being created; initialize it. - //This condition will be true in these cases: - // 1) The 'new' operator was used to instantiate this class - // 2) The 'new' operator was used to instantiate a subclass, and the subclass' constructor calls the constructor of its super class - // 3) The 'new' operator was used to instantiate a subclass, and the subclass' constructor includes something like `MySuperClass.call(this)` - // 4) Possibly if the prototype chain has been manipulated - - let newInstance = this; - - if(newInstance.constructor === newClass.prototype.constructor){ - //this function is the constructor of the new instance (i.e., it's not a super class' constructor) + const $super = new Proxy(superClass, { + apply(target, thisArg, argumentsList){ //target===superClass + if(_$superCalled) throw new ReferenceError("Super constructor may only be called once"); + _$superCalled = true; - defineNonEnumerableProperty(newInstance, "constructor", newClass); - } - - let superFnCalled = false; - function $super(){ - if(superFnCalled) throw new ReferenceError("Super constructor may only be called once"); - superFnCalled = true; - - //initialize the instance using the super class - let protectedAccessors = newClass.prototype.constructor.apply(newInstance, arguments) || {}; - - //add the protected value accessors to `$super` - defineProperty($super, "protected", {}, false, false, false); - for(const key in protectedAccessors){ - $super.protected[key] = protectedAccessors[key]; + target.apply(newInstance, argumentsList); + }, + get(target, property, receiver){ //target===superClass + if(property === "_protected"){ + if(!_$superCalled) throw new ReferenceError("Must call super constructor from derived constructor before accessing protected members"); + return target[protectedMembers]; } + return Reflect.get(...arguments); + }, + set(target, property, value, receiver){ //target===superClass + if(property === "_protected") return false; + return Reflect.set(...arguments); + }, + deleteProperty(target, property){ //target===superClass + throw new ReferenceError("invalid delete involving super constructor"); } - - //construct the new instance - _initializing = true; - $constructor.apply(newInstance, [$super].concat([].slice.call(arguments))); - - if(!superFnCalled) throw new ReferenceError("Must call super constructor in derived class before accessing 'this' or returning from derived constructor"); - - if(newInstance.constructor === newClass){ - //this function is the constructor of the new instance - - _initializing = false; - } - else{ - //this function is the constructor of a super class - - //generate protected value accessors for the subclass - let protectedAccessors = {}; - for(const key in $super.protected){ - Object.defineProperty(protectedAccessors, key, { - get: ()=>$super[key], - set: value=>($super[key] = value), - enumerable:true, configurable:false - }); - } - return protectedAccessors; - } - + }); + + _underConstruction = true; + try{ + constructor.apply(newInstance, [$super, ...argumentsList]); } - else{ - //the 'new' operator was not used; it was just called as a function - - return $function.apply(void 0, arguments); - + finally{ + _underConstruction = false; } + if(!_$superCalled) throw new ReferenceError("Must call super constructor before returning from derived constructor"); + + return newInstance; } - //override .name - defineProperty(newClass, "name", $className, false, false, true); + const newClass = new Proxy(constructorWrapper, { + construct(target, argumentsList, newTarget){ //target===constructorWrapper, newTarget===the proxy itself (newClass) + const newInstance = Reflect.construct(...arguments); + newInstance.constructor = newTarget; + return newInstance; + }, + apply(target, thisArg, argumentsList){ //target===constructorWrapper + if(_underConstruction){ + return target.apply(thisArg, argumentsList); //thisArg===the new instance + } + else if(applier){ + return applier.apply(thisArg, argumentsList); + } + throw new TypeError(`Class constructor ${constructor.name} cannot be invoked without 'new'`); + } + }); - if(this.extend === _extendFn){ - //the 'extend' method of the super class was not modified - - //make extend() a static method of the new class - defineNonEnumerableProperty(newClass, "extend", _extendFn); - } - + console.log("newClass:", newClass); + newClass.prototype = Object.create(superClass.prototype); + newClass.prototype.constructor = superClass; + console.log(newClass.prototype instanceof superClass); - /*** create the new prototype ***/ - - //An uninitialized instance of the super class will be the prototype of the new class. - //To create an instance without initializing it, we'll temporarily use an empty function as the constructor. - let emptyFn = function (){}; - emptyFn.prototype = this.prototype; - let newPrototype = new emptyFn(); - defineNonEnumerableProperty(newPrototype, "constructor", this); + Object.defineProperty(newClass, "name", { + writable: false, enumerable: false, configurable: true, + value: constructor.name + }); - //override .toString() - defineNonEnumerableProperty(newPrototype, "toString", function toString(){ return `[object ${$className}]`; }); - - defineProperty(newClass, "prototype", newPrototype, false, false, false); + Object.defineProperty(newClass, "toString", { + writable: true, enumerable: false, configurable: true, + value: function toString(){ return constructor.toString(); } + }); + //make extend() a static method of the new class + Object.defineProperty(newClass, "extend", { + writable: true, enumerable: false, configurable: true, + value: extend + }); return newClass; - } -//make extend() a static method of Class -defineNonEnumerableProperty(_baseClass, "extend", _extendFn); - -export { _baseClass as default }; +export { Class as default }; diff --git a/src/Class2.mjs.js b/src/Class2.mjs.js deleted file mode 100644 index d4605c0..0000000 --- a/src/Class2.mjs.js +++ /dev/null @@ -1,121 +0,0 @@ -/* eslint-env es6 */ -// https://github.com/wizard04wsu/Class - -//let _initializing = false; -const protected = Symbol("protected members"); - - -/*** helper functions ***/ - -function isPrimitive(o){ return o === null || (typeof(o) !== "object" && typeof(o) !== "function"); } - - -/*** base class ***/ - -function Class(){ - Object.defineProperty(this, protected, { - writable: false, enumerable: false, configurable: true, - value: {} - }); -}; - -Object.defineProperty(Class.prototype, "toString", { - writable: true, enumerable: false, configurable: true, - value: function toString(){ return "[object "+this.constructor.name+"]"; } -}); - -Object.defineProperty(Class, "extend", { - writable: true, enumerable: false, configurable: true, - value: extend -}); - - -/** - * Creates a new class that inherits from the super class. - * - * @param {object} [options] - * @param {string} [options.className] - Used as .name for the class function and in .toString() for instances of the class. - * @param {function} [options.constructor] - Initializes a new instance of the class. Its first argument will be a function that can be used to call the constructor of the super class (like a `super` keyword). It must be called before using `this`, or to gain access to protected members of the super class. If not specified, the default constructor will call the constructor of the super class, passing any arguments. - * @param {function} [options.function] - Returns a value when the class is called as a function instead of as a constructor (i.e., without using the 'new' keyword). If not specified, a TypeError will be thrown instead. - * @return {function} - The new class. - */ -function extend(constructor, applier){ - if(typeof this !== "function" || !(this.prototype instanceof Class)) - throw new TypeError("extend requires that 'this' be a Class constructor"); - if(typeof constructor !== "function") - throw new TypeError("constructor is not a function"); - if(arguments.length > 1 && typeof applier !== "function") - throw new TypeError("applier is not a function"); - - const newClass = new Proxy(constructor, { - construct(target, argumentsList, newTarget){ //target===constructor, newTarget===the proxy (newClass) - let supCalled; - - const sup = new Proxy(target.prototype.constructor, { - apply(target, thisArg, [newInstance, ...argumentsList]){ //target===the super constructor - if(supCalled) throw new ReferenceError("Super constructor may only be called once"); - supCalled = true; - - target.apply(newInstance, argumentsList); - }, - get(target, property, receiver){ //target===the super constructor, receiver===the proxy (sup) - if(property === "_protected"){ - if(!supCalled) throw new ReferenceError("Must call super constructor from derived constructor before accessing protected members"); - return target[protected]; - } - return Reflect.get(...arguments); - }, - set(target, property, value){ //target===the super constructor - if(property === "_protected") return false; - return Reflect.set(...arguments); - }, - deleteProperty(target, property){ //target===the super constructor - throw new ReferenceError("invalid delete involving super constructor"); - } - }); - - const newInstance = Reflect.construct(target, [sup, ...argumentsList], newTarget); - - if(!supCalled) throw new ReferenceError("Must call super constructor before returning from derived constructor"); - - return newInstance; - }, - apply(target, thisArg, argumentsList){ //target===constructor - return typeof applier === "function" ? applier.apply(thisArg, argumentsList) : void 0; - } - }); - - newClass.prototype = Object.create(this.prototype); - - Object.defineProperty(newClass, "name", { - writable: false, enumerable: false, configurable: true, - value: constructor.name - }); -} - - - - - - - /*** create the new prototype ***/ - - //An uninitialized instance of the super class will be the prototype of the new class. - //To create an instance without initializing it, we'll temporarily use an empty function as the constructor. - let emptyFn = function (){}; - emptyFn.prototype = this.prototype; - let newPrototype = new emptyFn(); - defineNonEnumerableProperty(newPrototype, "constructor", this); - - //override .toString() - defineNonEnumerableProperty(newPrototype, "toString", function toString(){ return `[object ${$className}]`; }); - - defineProperty(newClass, "prototype", newPrototype, false, false, false); - - - return newClass; - - - - -export { _baseClass as default }; diff --git a/test/Class testing.htm b/test/Class testing.htm index 0fbe431..c9672b9 100644 --- a/test/Class testing.htm +++ b/test/Class testing.htm @@ -7,7 +7,7 @@ Class testing - + diff --git a/test/Class testing.js b/test/Class testing.js index 7678c6f..204c10e 100644 --- a/test/Class testing.js +++ b/test/Class testing.js @@ -1,259 +1,134 @@ import Class from "../src/Class.mjs.js" -//create a class without adding anything new to it -console.groupCollapsed("A - empty class"); - let Alpha, alpha1; - - console.group("Alpha class"); - Alpha = Class.extend(); - console.dir(Alpha); - console.assert(Alpha.name === "Class", Alpha.name); //function's .name property is the class name - console.assert(Alpha.extend === Class.extend, Alpha.extend); //.extend() is inherited (copied to new class) - console.assert(Alpha.toString() === "function Class() { [custom code] }", Alpha.toString()); //.toString() output - console.groupEnd(); - - console.group("Alpha instance"); - alpha1 = new Alpha(); - console.dir(alpha1); - console.assert(alpha1.constructor === Alpha, alpha1.constructor); //the Alpha class is the constructor of the instance - console.assert(alpha1.toString() === "[instance of Class]", alpha1.toString()); //.toString() output +console.group("Class"); + console.group("class"); + console.dir(Class); + console.log(typeof Class); + console.assert(Class.toString() === `function Class(){\r + Object.defineProperty(this, protectedMembers, {\r + writable: false, enumerable: false, configurable: true,\r + value: {}\r + });\r +}`, Class.toString()); + console.assert(Class.name === "Class", Class.name); + console.assert(Class.prototype.toString() === "[object Class]", Class.prototype.toString()); + console.assert(Class.prototype.constructor === Class, Class.prototype.constructor); + console.groupEnd(); + console.group("instance"); + let cl = new Class(); + console.dir(cl); + console.log(typeof cl); + console.assert(Class.prototype.toString() === "[object Class]", Class.prototype.toString()); + console.assert(cl.toString() === "[object Class]", cl.toString()); console.groupEnd(); console.groupEnd(); -//create a class with a custom class name -console.groupCollapsed("B - class name"); - let Bravo, bravo1; - - console.group("Bravo class"); - Bravo = Class.extend({className:"Bravo"}); - console.dir(Bravo); - console.assert(Bravo.name === "Bravo", Bravo.name); //class name is 'Bravo' - console.assert(Bravo.toString() === "function Class() { [custom code] }", Bravo.toString()); //.toString() output - console.groupEnd(); - - console.group("Bravo instance"); - bravo1 = new Bravo(); - console.dir(bravo1); - console.assert(bravo1.toString() === "[instance of Bravo]", bravo1.toString()); //.toString() output +console.group("Alpha"); + console.group("class"); + let Alpha = Class.extend(function Alpha($super){ $super(); }); + console.dir(Alpha); + console.log(typeof Alpha); + console.assert(Alpha.toString() === "function Alpha($super){ $super(); }", Alpha.toString()); + console.assert(Alpha.name === "Alpha", Alpha.name); + console.assert(Alpha.prototype.toString() === "[object Class]", Alpha.prototype.toString()); + console.assert(Alpha.prototype.constructor === Class, Alpha.prototype.constructor); + console.groupEnd(); + console.group("instance"); + let a = new Alpha(); + console.dir(a); + console.log(typeof a); + console.assert(a.toString() === "[object Alpha]", a.toString()); console.groupEnd(); console.groupEnd(); -//create a class, specifying an invalid class name -console.groupCollapsed("C - invalid class name"); - let Charlie, charlie1; - - console.group("Charlie class"); - Charlie = Class.extend({className:"5"}); - console.dir(Charlie); - console.assert(Charlie.name === "Class", Charlie.name); //class name is inherited (copied to new class) - console.groupEnd(); - - console.group("Charlie instance"); - charlie1 = new Charlie(); - console.dir(charlie1); - console.assert(charlie1.toString() === "[instance of Class]", charlie1.toString()); //.toString() output +console.group("Bravo"); + console.group("class"); + let Bravo = Alpha.extend(function Bravo($super){ $super(); }); + console.dir(Bravo); + console.log(typeof Bravo); + console.assert(Bravo.toString() === "function Bravo($super){ $super(); }", Bravo.toString()); + console.assert(Bravo.name === "Bravo", Bravo.name); + console.assert(Bravo.prototype.toString() === "[object Alpha]", Alpha.prototype.toString()); + console.assert(Bravo.prototype.constructor === Alpha, Bravo.prototype.constructor); + console.groupEnd(); + console.group("instance"); + let b = new Bravo(); + console.dir(b); + console.log(typeof b); + console.assert(b.toString() === "[object Bravo]", b.toString()); console.groupEnd(); console.groupEnd(); -//specify returnFn() in case class is called without the 'new' keyword -console.groupCollapsed("D - return function"); - let Delta, delta1, delta2; +console.group("Error Traps"); + let X, x, Y, y; - console.group("Delta class"); - Delta = Class.extend({className:"Delta", - constructor:function (Super){ - Super(); - this.bar = function (){return "bar"}; - this.baz = function (){return this.bar()}; - }, - function:function (){return "foo"} - }); - console.dir(Delta); - console.groupEnd(); - - console.group("Delta call"); - delta1 = Delta(); - console.dir(delta1); - console.assert(delta1 === "foo", delta1); //returns output of returnFn() - console.assert(delta1.bar === void 0, delta1.bar); //.bar() is not added to the instance - console.assert(delta1.baz === void 0, delta1.baz); //.baz() is not added to the instance - console.groupEnd(); - - console.group("Delta instance"); - delta2 = new Delta(); - console.dir(delta2); - console.assert(delta2.bar() === "bar", delta2.bar()); //.bar() returns 'bar' - console.assert(delta2.baz() === "bar", delta2.baz()); //.baz() returns 'bar' - console.groupEnd(); -console.groupEnd(); - -//use 'this' keyword inside of returnFn() -console.groupCollapsed("E - return function using 'this'"); - let Echo, echo1; + X = {}; + X.extend = Class.extend; + try{ + Y = X.extend(function X($super){ $super(); }); + throw new Error("error was not thrown"); + }catch(e){ + console.assert(e.message === "extend requires that 'this' be a Class constructor", e.message); + } - console.group("Echo class"); - Echo = Class.extend({className:"Echo", - constructor:function (){ - this.foo = function (){return "foo"}; - }, - function:function (){return this.foo} - }); - console.dir(Echo); - console.groupEnd(); - - console.group("Echo call"); - echo1 = Echo(); - console.dir(echo1); - console.assert(echo1 === window.foo, echo1); //'this' refers to the window, not an instance of the class - console.groupEnd(); -console.groupEnd(); - -//don't call Super() within the Golf class' constructorFn() -console.groupCollapsed("F,G - subclass constructor without 'Super()'"); - let Foxtrot, Golf, golf1, golf2; + try{ + X = Class.extend(); + throw new Error("error was not thrown"); + }catch(e){ + console.assert(e.message === "constructor is not a function", e.message); + } - console.group("Foxtrot class"); - Foxtrot = Class.extend({className:"Foxtrot", - constructor:function($super){$super();this.foo = "foo"} - }); - console.dir(Foxtrot); - console.groupEnd(); - - console.group("Golf class"); - Golf = Class.extend({className:"Golf", - constructor:function($super){} - }); - console.dir(Golf); - console.groupEnd(); - - console.group("Golf instance 1"); - golf1 = new Golf(); //logs a warning to the console from the Golf constructor - console.info("\u2B11 should have gotten a warning about 'Super'"); - console.dir(golf1); - console.assert(golf1.foo === void 0, golf1.foo); //.foo was not added to the instance via the super class constructor - console.groupEnd(); - - console.group("Golf instance 2"); - console.info("\u2B10 should not get additional warnings"); - golf2 = new Golf(); //should not log a warning again - console.dir(golf2); - console.groupEnd(); -console.groupEnd(); - -//don't call Super() within the Hotel class' constructorFn() -console.groupCollapsed("H,I - super-class constructor without 'Super()'"); - let Hotel, India, india1, india2; + try{ + X = Class.extend(function (){}); + throw new Error("error was not thrown"); + }catch(e){ + console.assert(e.message === "constructor must be a named function", e.message); + } - console.group("Hotel class"); - Hotel = Class.extend({className:"Hotel", - constructor:function($super){this.foo = "foo"} - }); - console.dir(Hotel); - console.groupEnd(); - - console.group("India class"); - India = Hotel.extend({className:"D", - constructor:function($super){$super()} - }); - console.dir(India); - console.groupEnd(); - - console.group("India instance 1"); - india1 = new India(); //logs a warning to the console from the Hotel constructor - console.info("\u2B11 should have gotten a warning about 'Super'"); - console.dir(india1); - console.groupEnd(); - - console.group("India instance 2"); - console.info("\u2B10 should not get additional warnings"); - india2 = new India(); - console.dir(india2); - console.groupEnd(); -console.groupEnd(); - -//call Super() inside all the constructor functions -console.groupCollapsed("J,K - constructors with 'Super()'"); - let Juliet, Kilo, kilo1; + try{ + X = Class.extend(function X(){}, 0); + throw new Error("error was not thrown"); + }catch(e){ + console.assert(e.message === "applier is not a function", e.message); + } - console.group("Juliet class"); - Juliet = Class.extend({className:"Juliet", - constructor:function($super){$super();this.foo = "foo"} - }); - console.dir(Juliet); - console.groupEnd(); - - console.group("Kilo class"); - Kilo = Juliet.extend({className:"Kilo", - constructor:function($super){$super()} - }); - console.dir(Kilo); - console.groupEnd(); - - console.group("Kilo instance"); - kilo1 = new Kilo(); - console.dir(kilo1); - console.assert(kilo1.foo === "foo", kilo1.foo); //.foo was added to the instance via the super class constructor - console.groupEnd(); -console.groupEnd(); - -//protected properties -console.groupCollapsed("L,M,N - protected properties"); - let Lima, Mike, mike1, November, november1; + try{ + X = Class.extend(function X(){}); + x = new X(); + throw new Error("error was not thrown"); + }catch(e){ + console.assert(e.message === "Must call super constructor before returning from derived constructor", e.message); + } - console.group("Lima class"); - Lima = Class.extend({className:"Lima", - constructor:function($super){ - $super(); - let foo="bar"; - this.bop = function (){return foo}; - 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); - console.groupEnd(); - - console.group("Mike class"); - Mike = Lima.extend({className:"Mike", - constructor:function($super){ - Super(); - 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); - console.groupEnd(); - - 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 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", - constructor:function($super){ - Super(); - 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 - } - }); - console.dir(November); - console.groupEnd(); - - console.group("November instance"); - november1 = new November(); - console.dir(november1); - console.assert(november1.bop() === "baz", november1.bop()); //inherited function still has access - console.groupEnd(); + try{ + X = Class.extend(function X($super){ $super(); $super(); }); + x = new X(); + throw new Error("error was not thrown"); + }catch(e){ + console.assert(e.message === "Super constructor may only be called once", e.message); + } + + try{ + X = Class.extend(function X($super){ $super._protected; }); + x = new X(); + throw new Error("error was not thrown"); + }catch(e){ + console.assert(e.message === "Must call super constructor from derived constructor before accessing protected members", e.message); + } + + try{ + X = Class.extend(function X($super){ delete $super.foo; }); + x = new X(); + throw new Error("error was not thrown"); + }catch(e){ + console.assert(e.message === "invalid delete involving super constructor", e.message); + } + + try{ + X = Class.extend(function X(){}); + x = X(); + throw new Error("error was not thrown"); + }catch(e){ + console.assert(e.message === "Class constructor X cannot be invoked without 'new'", e.message); + } console.groupEnd(); From 92cb974eac59aa4f666c873e393e623d9c54fc91 Mon Sep 17 00:00:00 2001 From: wizard04wsu Date: Thu, 20 May 2021 18:41:28 -0400 Subject: [PATCH 03/18] modify the class wrapper instead of the proxy --- src/Class.mjs.js | 68 ++++++++++++++++++++----------------------- test/Class testing.js | 11 +++---- 2 files changed, 35 insertions(+), 44 deletions(-) diff --git a/src/Class.mjs.js b/src/Class.mjs.js index cacc1b0..5cc3290 100644 --- a/src/Class.mjs.js +++ b/src/Class.mjs.js @@ -1,6 +1,14 @@ /* eslint-env es6 */ // https://github.com/wizard04wsu/Class +function defineNonEnumerableProperty(object, property, value){ + Object.defineProperty(object, property, { + writable: true, enumerable: false, configurable: true, + value: value + }); +} + + const protectedMembers = Symbol("protected members"); let _underConstruction = false; @@ -13,17 +21,9 @@ function Class(){ }); } -Class.prototype.constructor = Class; - -Object.defineProperty(Class.prototype, "toString", { - writable: true, enumerable: false, configurable: true, - value: function toString(){ return `[object ${this.constructor.name}]`; } -}); +defineNonEnumerableProperty(Class.prototype, "toString", function toString(){ return `[object ${this.constructor.name}]`; }); -Object.defineProperty(Class, "extend", { - writable: true, enumerable: false, configurable: true, - value: extend -}); +defineNonEnumerableProperty(Class, "extend", extend); /** @@ -45,7 +45,7 @@ function extend(constructor, applier){ const superClass = this; - function constructorWrapper(...argumentsList){ + function newClass(...argumentsList){ const newInstance = this; let _$superCalled = false; @@ -85,13 +85,28 @@ function extend(constructor, applier){ return newInstance; } - const newClass = new Proxy(constructorWrapper, { - construct(target, argumentsList, newTarget){ //target===constructorWrapper, newTarget===the proxy itself (newClass) + newClass.prototype = Object.create(superClass.prototype); + defineNonEnumerableProperty(newClass.prototype, "constructor", superClass); + + Object.defineProperty(newClass, "name", { + writable: false, enumerable: false, configurable: true, + value: constructor.name + }); + + defineNonEnumerableProperty(newClass, "toString", function toString(){ return constructor.toString(); }); + + //make extend() a static method of the new class + defineNonEnumerableProperty(newClass, "extend", extend); + + const classProxy = new Proxy(newClass, { + construct(target, argumentsList, newTarget){ //target===newClass, newTarget===the proxy itself (classProxy) const newInstance = Reflect.construct(...arguments); - newInstance.constructor = newTarget; + + defineNonEnumerableProperty(newInstance, "constructor", newTarget); + return newInstance; }, - apply(target, thisArg, argumentsList){ //target===constructorWrapper + apply(target, thisArg, argumentsList){ //target===newClass if(_underConstruction){ return target.apply(thisArg, argumentsList); //thisArg===the new instance } @@ -102,28 +117,7 @@ function extend(constructor, applier){ } }); - console.log("newClass:", newClass); - newClass.prototype = Object.create(superClass.prototype); - newClass.prototype.constructor = superClass; - console.log(newClass.prototype instanceof superClass); - - Object.defineProperty(newClass, "name", { - writable: false, enumerable: false, configurable: true, - value: constructor.name - }); - - Object.defineProperty(newClass, "toString", { - writable: true, enumerable: false, configurable: true, - value: function toString(){ return constructor.toString(); } - }); - - //make extend() a static method of the new class - Object.defineProperty(newClass, "extend", { - writable: true, enumerable: false, configurable: true, - value: extend - }); - - return newClass; + return classProxy; } diff --git a/test/Class testing.js b/test/Class testing.js index 204c10e..a9b66e2 100644 --- a/test/Class testing.js +++ b/test/Class testing.js @@ -1,9 +1,8 @@ -import Class from "../src/Class.mjs.js" +import Class from "./Class.mjs.js" console.group("Class"); console.group("class"); console.dir(Class); - console.log(typeof Class); console.assert(Class.toString() === `function Class(){\r Object.defineProperty(this, protectedMembers, {\r writable: false, enumerable: false, configurable: true,\r @@ -17,7 +16,7 @@ console.group("Class"); console.group("instance"); let cl = new Class(); console.dir(cl); - console.log(typeof cl); + //console.log(cl); console.assert(Class.prototype.toString() === "[object Class]", Class.prototype.toString()); console.assert(cl.toString() === "[object Class]", cl.toString()); console.groupEnd(); @@ -27,7 +26,6 @@ console.group("Alpha"); console.group("class"); let Alpha = Class.extend(function Alpha($super){ $super(); }); console.dir(Alpha); - console.log(typeof Alpha); console.assert(Alpha.toString() === "function Alpha($super){ $super(); }", Alpha.toString()); console.assert(Alpha.name === "Alpha", Alpha.name); console.assert(Alpha.prototype.toString() === "[object Class]", Alpha.prototype.toString()); @@ -36,7 +34,7 @@ console.group("Alpha"); console.group("instance"); let a = new Alpha(); console.dir(a); - console.log(typeof a); + //console.log(a); console.assert(a.toString() === "[object Alpha]", a.toString()); console.groupEnd(); console.groupEnd(); @@ -45,7 +43,6 @@ console.group("Bravo"); console.group("class"); let Bravo = Alpha.extend(function Bravo($super){ $super(); }); console.dir(Bravo); - console.log(typeof Bravo); console.assert(Bravo.toString() === "function Bravo($super){ $super(); }", Bravo.toString()); console.assert(Bravo.name === "Bravo", Bravo.name); console.assert(Bravo.prototype.toString() === "[object Alpha]", Alpha.prototype.toString()); @@ -54,7 +51,7 @@ console.group("Bravo"); console.group("instance"); let b = new Bravo(); console.dir(b); - console.log(typeof b); + //console.log(b); console.assert(b.toString() === "[object Bravo]", b.toString()); console.groupEnd(); console.groupEnd(); From 01148d9ec45505efa77d9a30c41300e786b89e12 Mon Sep 17 00:00:00 2001 From: wizard04wsu Date: Fri, 21 May 2021 12:03:36 -0400 Subject: [PATCH 04/18] - made protected members be returned from $super() instead of added as a property - added more tests --- src/Class.mjs.js | 13 ++------- test/Class testing.js | 68 ++++++++++++++++++++++++++++++++++++++----- test/examples.htm | 18 ------------ test/examples.js | 64 ---------------------------------------- 4 files changed, 62 insertions(+), 101 deletions(-) delete mode 100644 test/examples.htm delete mode 100644 test/examples.js diff --git a/src/Class.mjs.js b/src/Class.mjs.js index 5cc3290..228c3f9 100644 --- a/src/Class.mjs.js +++ b/src/Class.mjs.js @@ -55,17 +55,8 @@ function extend(constructor, applier){ _$superCalled = true; target.apply(newInstance, argumentsList); - }, - get(target, property, receiver){ //target===superClass - if(property === "_protected"){ - if(!_$superCalled) throw new ReferenceError("Must call super constructor from derived constructor before accessing protected members"); - return target[protectedMembers]; - } - return Reflect.get(...arguments); - }, - set(target, property, value, receiver){ //target===superClass - if(property === "_protected") return false; - return Reflect.set(...arguments); + + return newInstance[protectedMembers]; }, deleteProperty(target, property){ //target===superClass throw new ReferenceError("invalid delete involving super constructor"); diff --git a/test/Class testing.js b/test/Class testing.js index a9b66e2..256304e 100644 --- a/test/Class testing.js +++ b/test/Class testing.js @@ -105,14 +105,6 @@ console.group("Error Traps"); console.assert(e.message === "Super constructor may only be called once", e.message); } - try{ - X = Class.extend(function X($super){ $super._protected; }); - x = new X(); - throw new Error("error was not thrown"); - }catch(e){ - console.assert(e.message === "Must call super constructor from derived constructor before accessing protected members", e.message); - } - try{ X = Class.extend(function X($super){ delete $super.foo; }); x = new X(); @@ -129,3 +121,63 @@ console.group("Error Traps"); console.assert(e.message === "Class constructor X cannot be invoked without 'new'", e.message); } console.groupEnd(); + +console.group("Inheritance"); + console.assert(b instanceof Bravo, b); + console.assert(b instanceof Alpha, b); + console.assert(b instanceof Class, b); + console.assert(b.constructor === Bravo, b.constructor); + console.assert(Bravo.prototype instanceof Alpha, Bravo.prototype); + console.assert(!(Bravo.prototype instanceof Bravo), Bravo.prototype); + console.assert(Bravo.prototype.constructor === Alpha, Bravo.prototype.constructor); + console.assert(!(a instanceof Bravo), a); + console.assert(a instanceof Alpha, a); + console.assert(a instanceof Class, a); + console.assert(a.constructor === Alpha, a.constructor); + console.assert(Alpha.prototype instanceof Class, Alpha.prototype); + console.assert(Alpha.prototype.constructor === Class, Alpha.prototype.constructor); +console.groupEnd(); + +console.group("Rectangle & Square"); + let Rectangle = Class.extend(function Rectangle($super, width, height){ + let protectedMembers = $super(); + this.width = 1*width||0; + this.height = 1*height||0; + this.area = function (){ return Math.abs(this.width * this.height); }; + protectedMembers.color = "red"; + this.whatAmI = function (){ return `I am a ${protectedMembers.color} rectangle.`; }; + }, + (width, height)=>Math.abs((1*width||0) * (1*height||0)) + ); + Rectangle.draw = function (obj){ console.log(`Drawing: ${obj.whatAmI()}`); }; + + let Square = Rectangle.extend(function Square($super, width){ + let protectedMembers = $super(width, width); + Object.defineProperty(this, "height", { + get:function (){ return this.width; }, + set:function (val){ return this.width = 1*val||0; }, + enumerable:true, configurable:true + }); + let superIs = this.whatAmI; + this.whatAmI = function (){ return `${superIs()} I am a ${protectedMembers.color} square.`; }; + this.changeColor = function (color){ protectedMembers.color = color; }; + $super.draw(this); + }, + (width)=>Math.pow(1*width||0, 2) + ); + + let s = new Square(3); + + console.assert(s.toString() === "[object Square]", s.toString()); + console.assert(s.width === 3, s.width); + console.assert(s.height === 3, s.height); + console.assert(s.area() === 9, s.area()); + s.height = 4; + console.assert(s.width === 4, s.width); + console.assert(s.height === 4, s.height); + console.assert(s.area() === 16, s.area()); + console.assert(s.whatAmI() === "I am a red rectangle. I am a red square.", s.whatAmI()); + console.assert(s.color === void 0, s.color); + s.changeColor("blue"); + console.assert(s.whatAmI() === "I am a blue rectangle. I am a blue square.", s.whatAmI()); +console.groupEnd(); diff --git a/test/examples.htm b/test/examples.htm deleted file mode 100644 index 5b03b1c..0000000 --- a/test/examples.htm +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - Class testing - - - - - - -

See console for assertions.

- - - diff --git a/test/examples.js b/test/examples.js deleted file mode 100644 index f6ce87b..0000000 --- a/test/examples.js +++ /dev/null @@ -1,64 +0,0 @@ -import Class from "./Class.mjs.js" - -console.group("Rectangle & Square"); - let Rectangle = Class.extend({ - className:"Rectangle", - constructorFn:function (Super, width, height){ - 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 }); - }, - returnFn:function (width, height){ - return Math.abs((width||0) * (height||0)); - } - }); - - let Square = Rectangle.extend({ - className:"Square", - 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 }); - }, - returnFn:function (width){ - return Math.pow(width||0, 2); - } - }); - - let s = new Square(3); - - console.assert(s.toString() === "[instance of Square]", s.toString()); - console.assert(s.area === 9, s.area); - s.height = 4; - console.assert(s.area === 16, s.area); - console.assert(s.whatAmI === "I am a rectangle. I am a square.", s.whatAmI); -console.groupEnd(); - -console.group("Alpha & Bravo"); - let Alpha = Class.extend({ - className:"Alpha", - constructorFn:function (Super){ - Super(); - let randomInstanceID = Math.random(); - Object.defineProperty(Super.protected, "rando", { - get:function(){return randomInstanceID}, - enumerable:true, configurable:true - }); - } - }); - - let Bravo = Alpha.extend({ - className:"Bravo", - constructorFn:function (Super){ - Super(); - this.foo = "My ID is "+Super.protected.rando; - } - }); - - let b = new Bravo(); - - console.log(b.foo); //My ID is ... -console.groupEnd(); From 4752b2e627cbf195fac7ddc425b82074569b67cb Mon Sep 17 00:00:00 2001 From: wizard04wsu Date: Fri, 28 May 2021 03:47:11 -0400 Subject: [PATCH 05/18] - improve comments and variable names - update tests --- .eslintrc.json | 13 ++++ dist/Class_8_0_0.min.js | 1 - src/Class.mjs.js | 138 +++++++++++++++++++++++++--------------- test/Class testing.js | 12 ++-- 4 files changed, 107 insertions(+), 57 deletions(-) create mode 100644 .eslintrc.json delete mode 100644 dist/Class_8_0_0.min.js diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..c027659 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,13 @@ +{ + "env": "es6", + "extends": "eslint:recommended", + "ecmaFeatures": { + "impliedStrict": true + } + "files": [ + { + "patterns": [ "**/*.mjs", "**/*.mjs.js" ], + "sourceType": "module" + } + ], +} \ No newline at end of file diff --git a/dist/Class_8_0_0.min.js b/dist/Class_8_0_0.min.js deleted file mode 100644 index 868a03d..0000000 --- a/dist/Class_8_0_0.min.js +++ /dev/null @@ -1 +0,0 @@ -(function(){"use strict";let t=!1;function n(t,n,e,o,r,c){Object.defineProperty(t,n,{value:e,writable:o,enumerable:r,configurable:c})}function e(t){t.apply(null,[].slice.call(arguments,1))}function o(){}let r=function(){return"function Class() { [custom code] }"},c=function(){return"[instance of "+this.constructor.name+"]"};let i=function(){};n(i.prototype,"toString",c,!0,!1,!0),n(i,"toString",r,!0,!1,!0);let u=function(i){if(void 0===i)i={};else if(function(t){let n=typeof t;return"object"!==n&&"function"!==n||null===t}(i))throw new TypeError("argument 'options' is not an object");let l="function"==typeof i.constructorFn?i.constructorFn:e,s="function"==typeof i.returnFn?i.returnFn:o,a=!1,f=function(){if(!(this&&this instanceof f)||this.constructor!==f.prototype.constructor&&!t)return s.apply(null,arguments);{let o=this;o.constructor===f.prototype.constructor&&n(o,"constructor",f,!0,!1,!0);let r,c=!1,i=function(){c||(c=!0,r=f.prototype.constructor.apply(o,arguments)||{},Object.defineProperty(i,"protected",{get:()=>r,enumerable:!1,configurable:!1}))};if(t=!0,l.apply(o,[i].concat([].slice.call(arguments))),c||a||(e=f.name+" constructor does not call Super()",console&&(console.warn||console.log)(e),a=!0),o.constructor!==f)return function(t={}){let n={};for(let e in t)Object.defineProperty(n,e,{get:()=>t[e],set:n=>t[e]=n,enumerable:!0,configurable:!0});return n}(r);t=!1}var e};var p;n(f,"name",void 0!==(p=i.className)&&/^[a-z_$][a-z0-9_$]*$/i.test(p)?i.className:this.name,!1,!1,!0),n(f,"toString",r,!0,!1,!0),this.extend===u&&n(f,"extend",u,!0,!1,!0);let y=function(){};y.prototype=this.prototype;let d=new y;return y=null,n(d,"constructor",this,!0,!1,!0),n(d,"toString",c,!0,!1,!0),n(f,"prototype",d,!1,!1,!1),f};n(u,"toString",function(){return"function extend() { [custom code] }"},!0,!1,!0),n(i,"extend",u,!0,!1,!0);let l=this,s=l.Class;n(i,"noConflict",function(){return l.Class=s,l=s=null,delete i.noConflict,i},!0,!1,!0),l.Class=i}).call(this); diff --git a/src/Class.mjs.js b/src/Class.mjs.js index 228c3f9..f77525b 100644 --- a/src/Class.mjs.js +++ b/src/Class.mjs.js @@ -1,116 +1,154 @@ -/* eslint-env es6 */ // https://github.com/wizard04wsu/Class -function defineNonEnumerableProperty(object, property, value){ - Object.defineProperty(object, property, { - writable: true, enumerable: false, configurable: true, - value: value - }); -} +/** @module Class */ +export { BaseClass as default }; +//key of a property of Class instances; the property is an object with the instance's protected members const protectedMembers = Symbol("protected members"); -let _underConstruction = false; + +//state: true iif an instance of a class is being constructed +let _instanceIsUnderConstruction = false; -function Class(){ +/** + * @alias module:Class-Class + * @abstract + * @class + */ +const BaseClass = function Class(){ Object.defineProperty(this, protectedMembers, { writable: false, enumerable: false, configurable: true, value: {} }); } -defineNonEnumerableProperty(Class.prototype, "toString", function toString(){ return `[object ${this.constructor.name}]`; }); +defineNonEnumerableProperty(BaseClass.prototype, "toString", function toString(){ return `[object ${this.constructor.name}]`; }); -defineNonEnumerableProperty(Class, "extend", extend); +//make extend() a static member of the base class +defineNonEnumerableProperty(BaseClass, "extend", extend); /** - * Creates a new class that inherits from the super class. - * - * @param {function} [constructor] - Initializes a new instance of the class. Its first argument will be a function that can be used to call the constructor of the super class (instead of the `super` keyword). It must be called in the constructor, it should be called before using `this`, and it must be called to gain access to protected members. - * @param {function} [applier] - Returns a value when the class is called without using the 'new' keyword. If not specified, a TypeError will be thrown instead of returning a value. - * @return {function} - The new class. + * Creates a child class. + * @static + * @param {initializer} init - Handler to initialize a new instance of the child class. The name of the function is used as the name of the class. + * @param {function} [call] - Handler for when the class is called without using the `new` keyword. Default behavior is to throw a TypeError. + * @return {Class} - The new child class. + * @throws {TypeError} - 'extend' method requires that 'this' be a Class constructor + * @throws {TypeError} - 'init' is not a function + * @throws {TypeError} - 'init' must be a named function + * @throws {TypeError} - 'call' is not a function */ -function extend(constructor, applier){ - if(typeof this !== "function" || !(this === Class || this.prototype instanceof Class)) - throw new TypeError("extend requires that 'this' be a Class constructor"); - if(typeof constructor !== "function") - throw new TypeError("constructor is not a function"); - if(!constructor.name) - throw new TypeError("constructor must be a named function"); - if(arguments.length > 1 && typeof applier !== "function") - throw new TypeError("applier is not a function"); +function extend(init, call){ + /** + * @typedef {function} initializer + * @param {function} $super - The parent class's constructor, bound as the first argument. It is to be used like the `super` keyword. It *must* be called within the constructor, and it *should* be called before using `this`. + * @param {...*} args + * @returns {object} - An object providing access to protected members. + */ - const superClass = this; + if(typeof this !== "function" || !(this === BaseClass || this.prototype instanceof BaseClass)) + throw new TypeError("'extend' method requires that 'this' be a Class constructor"); + if(typeof init !== "function") + throw new TypeError("'init' is not a function"); + if(!init.name) + throw new TypeError("'init' must be a named function"); + if(arguments.length > 1 && typeof call !== "function") + throw new TypeError("'call' is not a function"); - function newClass(...argumentsList){ + const ParentClass = this; + const className = init.name; + + /** + * @class + * @augments ParentClass + * @private + * @throws {ReferenceError} - super constructor may only be called once + * @throws {ReferenceError} - invalid delete involving super constructor + * @throws {ReferenceError} - must call super constructor before returning from derived constructor + */ + function ChildClass(...argumentsList){ const newInstance = this; let _$superCalled = false; - const $super = new Proxy(superClass, { - apply(target, thisArg, argumentsList){ //target===superClass - if(_$superCalled) throw new ReferenceError("Super constructor may only be called once"); + const $super = new Proxy(ParentClass, { + apply(target, thisArg, argumentsList){ //target===ParentClass + if(_$superCalled) throw new ReferenceError("super constructor may only be called once"); _$superCalled = true; target.apply(newInstance, argumentsList); return newInstance[protectedMembers]; }, - deleteProperty(target, property){ //target===superClass + deleteProperty(target, property){ //target===ParentClass + //disallow deletion of static members of a parent class throw new ReferenceError("invalid delete involving super constructor"); } }); - _underConstruction = true; + _instanceIsUnderConstruction = true; try{ - constructor.apply(newInstance, [$super, ...argumentsList]); + init.apply(newInstance, [$super, ...argumentsList]); } finally{ - _underConstruction = false; + _instanceIsUnderConstruction = false; } - if(!_$superCalled) throw new ReferenceError("Must call super constructor before returning from derived constructor"); + if(!_$superCalled) throw new ReferenceError("must call super constructor before returning from derived constructor"); return newInstance; } - newClass.prototype = Object.create(superClass.prototype); - defineNonEnumerableProperty(newClass.prototype, "constructor", superClass); + ChildClass.prototype = Object.create(ParentClass.prototype); + defineNonEnumerableProperty(ChildClass.prototype, "constructor", ParentClass); - Object.defineProperty(newClass, "name", { + Object.defineProperty(ChildClass, "name", { writable: false, enumerable: false, configurable: true, - value: constructor.name + value: className }); - defineNonEnumerableProperty(newClass, "toString", function toString(){ return constructor.toString(); }); + //ChildClass.toString() outputs the 'init' function + defineNonEnumerableProperty(ChildClass, "toString", function toString(){ return init.toString(); }); //make extend() a static method of the new class - defineNonEnumerableProperty(newClass, "extend", extend); + defineNonEnumerableProperty(ChildClass, "extend", extend); - const classProxy = new Proxy(newClass, { - construct(target, argumentsList, newTarget){ //target===newClass, newTarget===the proxy itself (classProxy) + //use a Proxy to distinguish calls to ChildClass between those that do and do not use the `new` keyword + //@throws {TypeError} if called without the `new` keyword and without the `{@link call}` argument. + const proxyForChildClass = new Proxy(ChildClass, { + construct(target, argumentsList, newTarget){ //target===ChildClass, newTarget===the proxy itself (proxyForChildClass) const newInstance = Reflect.construct(...arguments); defineNonEnumerableProperty(newInstance, "constructor", newTarget); return newInstance; }, - apply(target, thisArg, argumentsList){ //target===newClass - if(_underConstruction){ + apply(target, thisArg, argumentsList){ //target===ChildClass + if(_instanceIsUnderConstruction){ + //the 'new' keyword was used, and this proxy handler is for the constructor of a super class + return target.apply(thisArg, argumentsList); //thisArg===the new instance } - else if(applier){ - return applier.apply(thisArg, argumentsList); + else if(call){ + //the 'new' keyword was not used, and a 'call' function was passed + return call.apply(thisArg, argumentsList); } - throw new TypeError(`Class constructor ${constructor.name} cannot be invoked without 'new'`); + throw new TypeError(`Class constructor ${className} cannot be invoked without 'new'`); } }); - return classProxy; + return proxyForChildClass; } -export { Class as default }; +/* helper functions */ + +function defineNonEnumerableProperty(object, property, value){ + Object.defineProperty(object, property, { + writable: true, enumerable: false, configurable: true, + value: value + }); +} diff --git a/test/Class testing.js b/test/Class testing.js index 256304e..1587c9b 100644 --- a/test/Class testing.js +++ b/test/Class testing.js @@ -65,28 +65,28 @@ console.group("Error Traps"); Y = X.extend(function X($super){ $super(); }); throw new Error("error was not thrown"); }catch(e){ - console.assert(e.message === "extend requires that 'this' be a Class constructor", e.message); + console.assert(e.message === "'extend' method requires that 'this' be a Class constructor", e.message); } try{ X = Class.extend(); throw new Error("error was not thrown"); }catch(e){ - console.assert(e.message === "constructor is not a function", e.message); + console.assert(e.message === "'init' is not a function", e.message); } try{ X = Class.extend(function (){}); throw new Error("error was not thrown"); }catch(e){ - console.assert(e.message === "constructor must be a named function", e.message); + console.assert(e.message === "'init' must be a named function", e.message); } try{ X = Class.extend(function X(){}, 0); throw new Error("error was not thrown"); }catch(e){ - console.assert(e.message === "applier is not a function", e.message); + console.assert(e.message === "'call' is not a function", e.message); } try{ @@ -94,7 +94,7 @@ console.group("Error Traps"); x = new X(); throw new Error("error was not thrown"); }catch(e){ - console.assert(e.message === "Must call super constructor before returning from derived constructor", e.message); + console.assert(e.message === "must call super constructor before returning from derived constructor", e.message); } try{ @@ -102,7 +102,7 @@ console.group("Error Traps"); x = new X(); throw new Error("error was not thrown"); }catch(e){ - console.assert(e.message === "Super constructor may only be called once", e.message); + console.assert(e.message === "super constructor may only be called once", e.message); } try{ From 31cd7098176f97401ec9ec451b84372b75940268 Mon Sep 17 00:00:00 2001 From: wizard04wsu Date: Fri, 28 May 2021 21:29:32 -0400 Subject: [PATCH 06/18] some more error catching, especially for multiple calls of `$super` within a constructor --- src/Class.mjs.js | 26 +++++++++++--------- test/Class testing.js | 55 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 69 insertions(+), 12 deletions(-) diff --git a/src/Class.mjs.js b/src/Class.mjs.js index f77525b..98072a7 100644 --- a/src/Class.mjs.js +++ b/src/Class.mjs.js @@ -18,6 +18,7 @@ let _instanceIsUnderConstruction = false; * @class */ const BaseClass = function Class(){ + _instanceIsUnderConstruction = false; Object.defineProperty(this, protectedMembers, { writable: false, enumerable: false, configurable: true, value: {} @@ -65,6 +66,8 @@ function extend(init, call){ * @class * @augments ParentClass * @private + * @throws {ReferenceError} - must call super constructor in derived constructor before accessing 'this' + * @throws {ReferenceError} - unexpected use of 'new' keyword * @throws {ReferenceError} - super constructor may only be called once * @throws {ReferenceError} - invalid delete involving super constructor * @throws {ReferenceError} - must call super constructor before returning from derived constructor @@ -74,10 +77,14 @@ function extend(init, call){ let _$superCalled = false; const $super = new Proxy(ParentClass, { + construct(target, argumentsList, newTarget){ //target===ParentClass, newTarget===the proxy itself ($super) + throw new ReferenceError("unexpected use of 'new' keyword"); + }, apply(target, thisArg, argumentsList){ //target===ParentClass - if(_$superCalled) throw new ReferenceError("super constructor may only be called once"); + if(_$superCalled) throw new ReferenceError("super constructor may be called only once during execution of derived constructor"); _$superCalled = true; + _instanceIsUnderConstruction = true; target.apply(newInstance, argumentsList); return newInstance[protectedMembers]; @@ -88,13 +95,7 @@ function extend(init, call){ } }); - _instanceIsUnderConstruction = true; - try{ - init.apply(newInstance, [$super, ...argumentsList]); - } - finally{ - _instanceIsUnderConstruction = false; - } + init.apply(newInstance, [$super, ...argumentsList]); if(!_$superCalled) throw new ReferenceError("must call super constructor before returning from derived constructor"); @@ -119,21 +120,24 @@ function extend(init, call){ //@throws {TypeError} if called without the `new` keyword and without the `{@link call}` argument. const proxyForChildClass = new Proxy(ChildClass, { construct(target, argumentsList, newTarget){ //target===ChildClass, newTarget===the proxy itself (proxyForChildClass) + _instanceIsUnderConstruction = false; const newInstance = Reflect.construct(...arguments); defineNonEnumerableProperty(newInstance, "constructor", newTarget); return newInstance; }, - apply(target, thisArg, argumentsList){ //target===ChildClass + apply(target, thisArg, argumentsList){ //target===ChildClass or a super class if(_instanceIsUnderConstruction){ //the 'new' keyword was used, and this proxy handler is for the constructor of a super class + _instanceIsUnderConstruction = false; return target.apply(thisArg, argumentsList); //thisArg===the new instance } else if(call){ - //the 'new' keyword was not used, and a 'call' function was passed - return call.apply(thisArg, argumentsList); + //the 'new' keyword was not used, and a 'call' function was passed to 'extend' + + return call.apply(thisArg, argumentsList); //thisArg==='this' in the the caller's context } throw new TypeError(`Class constructor ${className} cannot be invoked without 'new'`); } diff --git a/test/Class testing.js b/test/Class testing.js index 1587c9b..90ff2fe 100644 --- a/test/Class testing.js +++ b/test/Class testing.js @@ -4,6 +4,7 @@ console.group("Class"); console.group("class"); console.dir(Class); console.assert(Class.toString() === `function Class(){\r + _instanceIsUnderConstruction = false;\r Object.defineProperty(this, protectedMembers, {\r writable: false, enumerable: false, configurable: true,\r value: {}\r @@ -102,7 +103,7 @@ console.group("Error Traps"); x = new X(); throw new Error("error was not thrown"); }catch(e){ - console.assert(e.message === "super constructor may only be called once", e.message); + console.assert(e.message === "super constructor may be called only once during execution of derived constructor", e.message); } try{ @@ -113,6 +114,45 @@ console.group("Error Traps"); console.assert(e.message === "invalid delete involving super constructor", e.message); } + try{ + X = Class.extend(function X($super){ new $super(); }); + x = new X(); + throw new Error("error was not thrown"); + }catch(e){ + console.assert(e.message === "unexpected use of 'new' keyword", e.message); + } + + /*try{ + X = Class.extend(function X($super){ this; $super(); }); + x = new X(); + throw new Error("error was not thrown"); + }catch(e){ + console.assert(e.message === "must call super constructor before accessing 'this'", e.message); + }*/ + + try{ + X = Class.extend(function X($super){ this(); $super(); }); + x = new X(); + throw new Error("error was not thrown"); + }catch(e){ + console.assert(e.message === "this is not a function", e.message); + } + + /*try{ + X = Class.extend(function X($super){ this.test; $super(); }); + x = new X(); + throw new Error("error was not thrown"); + }catch(e){ + console.assert(e.message === "must call super constructor before accessing 'this'", e.message); + }*/ + + try{ + X = Class.extend(function X($super){ $super(); this; }); + x = new X(); + }catch(e){ + throw e; + } + try{ X = Class.extend(function X(){}); x = X(); @@ -139,6 +179,7 @@ console.group("Inheritance"); console.groupEnd(); console.group("Rectangle & Square"); + let testCount = 0; let Rectangle = Class.extend(function Rectangle($super, width, height){ let protectedMembers = $super(); this.width = 1*width||0; @@ -146,6 +187,18 @@ console.group("Rectangle & Square"); this.area = function (){ return Math.abs(this.width * this.height); }; protectedMembers.color = "red"; this.whatAmI = function (){ return `I am a ${protectedMembers.color} rectangle.`; }; + + if(testCount++ < 1){ + let expectedArea = Math.abs((1*width||0) * (1*height||0)), + area; + try{ + area = Rectangle($super, width, height); + }catch(e){ + console.assert(e.message === "super constructor may be called only once during execution of derived constructor", e.message); + } + area = this.constructor(width, height); + console.assert(area === expectedArea, area, this.constructor); + } }, (width, height)=>Math.abs((1*width||0) * (1*height||0)) ); From 22a19c5a99b500dbe4baf8fa9e9961f3da719e10 Mon Sep 17 00:00:00 2001 From: wizard04wsu Date: Fri, 28 May 2021 21:42:51 -0400 Subject: [PATCH 07/18] comments --- src/Class.mjs.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Class.mjs.js b/src/Class.mjs.js index 98072a7..06707dd 100644 --- a/src/Class.mjs.js +++ b/src/Class.mjs.js @@ -66,11 +66,12 @@ function extend(init, call){ * @class * @augments ParentClass * @private - * @throws {ReferenceError} - must call super constructor in derived constructor before accessing 'this' * @throws {ReferenceError} - unexpected use of 'new' keyword - * @throws {ReferenceError} - super constructor may only be called once + * @throws {ReferenceError} - super constructor may be called only once during execution of derived constructor * @throws {ReferenceError} - invalid delete involving super constructor * @throws {ReferenceError} - must call super constructor before returning from derived constructor + * #@throws {ReferenceError} - must call super constructor before accessing 'this' + * @throws {ReferenceError} - class constructor cannot be invoked without 'new' */ function ChildClass(...argumentsList){ const newInstance = this; @@ -139,7 +140,7 @@ function extend(init, call){ return call.apply(thisArg, argumentsList); //thisArg==='this' in the the caller's context } - throw new TypeError(`Class constructor ${className} cannot be invoked without 'new'`); + throw new TypeError(`class constructor ${className} cannot be invoked without 'new'`); } }); From b03ee60e179adf9e5eff9d5927cfbd66c3bfd3d6 Mon Sep 17 00:00:00 2001 From: wizard04wsu Date: Fri, 28 May 2021 22:10:45 -0400 Subject: [PATCH 08/18] - throw ReferenceError when a member of `this` is accessed before calling super constructor --- src/Class.mjs.js | 22 ++++++++++++++++++++-- test/Class testing.js | 6 +++--- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/Class.mjs.js b/src/Class.mjs.js index 06707dd..b9bf5f2 100644 --- a/src/Class.mjs.js +++ b/src/Class.mjs.js @@ -69,8 +69,8 @@ function extend(init, call){ * @throws {ReferenceError} - unexpected use of 'new' keyword * @throws {ReferenceError} - super constructor may be called only once during execution of derived constructor * @throws {ReferenceError} - invalid delete involving super constructor + * @throws {ReferenceError} - must call super constructor before accessing 'this' * @throws {ReferenceError} - must call super constructor before returning from derived constructor - * #@throws {ReferenceError} - must call super constructor before accessing 'this' * @throws {ReferenceError} - class constructor cannot be invoked without 'new' */ function ChildClass(...argumentsList){ @@ -96,7 +96,25 @@ function extend(init, call){ } }); - init.apply(newInstance, [$super, ...argumentsList]); + function denyAccessToKeywordThis(){ + if(!_$superCalled) throw new ReferenceError("must call super constructor before accessing 'this'"); + } + let proxyForKeywordThis = new Proxy(newInstance, { + apply(){ denyAccessToKeywordThis(); return Reflect.apply(...arguments); }, + defineProperty(){ denyAccessToKeywordThis(); return Reflect.defineProperty(...arguments); }, + deleteProperty(){ denyAccessToKeywordThis(); return Reflect.deleteProperty(...arguments); }, + get(){ denyAccessToKeywordThis(); return Reflect.get(...arguments); }, + getOwnPropertyDescriptor(){ denyAccessToKeywordThis(); return Reflect.getOwnPropertyDescriptor(...arguments); }, + getPrototypeOf(){ denyAccessToKeywordThis(); return Reflect.getPrototypeOf(...arguments); }, + has(){ denyAccessToKeywordThis(); return Reflect.has(...arguments); }, + isExtensible(){ denyAccessToKeywordThis(); return Reflect.isExtensible(...arguments); }, + ownKeys(){ denyAccessToKeywordThis(); return Reflect.ownKeys(...arguments); }, + preventExtensions(){ denyAccessToKeywordThis(); return Reflect.preventExtensions(...arguments); }, + set(){ denyAccessToKeywordThis(); return Reflect.set(...arguments); }, + setPrototypeOf(){ denyAccessToKeywordThis(); return Reflect.setPrototypeOf(...arguments); } + }); + + init.apply(proxyForKeywordThis, [$super, ...argumentsList]); if(!_$superCalled) throw new ReferenceError("must call super constructor before returning from derived constructor"); diff --git a/test/Class testing.js b/test/Class testing.js index 90ff2fe..a303da9 100644 --- a/test/Class testing.js +++ b/test/Class testing.js @@ -138,13 +138,13 @@ console.group("Error Traps"); console.assert(e.message === "this is not a function", e.message); } - /*try{ + try{ X = Class.extend(function X($super){ this.test; $super(); }); x = new X(); throw new Error("error was not thrown"); }catch(e){ console.assert(e.message === "must call super constructor before accessing 'this'", e.message); - }*/ + } try{ X = Class.extend(function X($super){ $super(); this; }); @@ -158,7 +158,7 @@ console.group("Error Traps"); x = X(); throw new Error("error was not thrown"); }catch(e){ - console.assert(e.message === "Class constructor X cannot be invoked without 'new'", e.message); + console.assert(e.message === "class constructor X cannot be invoked without 'new'", e.message); } console.groupEnd(); From 573f1dd9b9a069aa17a9110e0e2b20dcbcef7943 Mon Sep 17 00:00:00 2001 From: wizard04wsu Date: Fri, 28 May 2021 22:23:33 -0400 Subject: [PATCH 09/18] 'this' testing --- src/Class.mjs.js | 4 +++- test/Class testing.js | 12 ++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/Class.mjs.js b/src/Class.mjs.js index b9bf5f2..b8196a2 100644 --- a/src/Class.mjs.js +++ b/src/Class.mjs.js @@ -96,11 +96,13 @@ function extend(init, call){ } }); + //I don't believe there's a way to trap access to 'this', but we can at least trap access to its members: function denyAccessToKeywordThis(){ if(!_$superCalled) throw new ReferenceError("must call super constructor before accessing 'this'"); } let proxyForKeywordThis = new Proxy(newInstance, { - apply(){ denyAccessToKeywordThis(); return Reflect.apply(...arguments); }, + //apply(){ denyAccessToKeywordThis(); return Reflect.apply(...arguments); }, + //construct(){ denyAccessToKeywordThis(); return Reflect.construct(...arguments); }, defineProperty(){ denyAccessToKeywordThis(); return Reflect.defineProperty(...arguments); }, deleteProperty(){ denyAccessToKeywordThis(); return Reflect.deleteProperty(...arguments); }, get(){ denyAccessToKeywordThis(); return Reflect.get(...arguments); }, diff --git a/test/Class testing.js b/test/Class testing.js index a303da9..be0605a 100644 --- a/test/Class testing.js +++ b/test/Class testing.js @@ -130,6 +130,14 @@ console.group("Error Traps"); console.assert(e.message === "must call super constructor before accessing 'this'", e.message); }*/ + try{ + X = Class.extend(function X($super){ this.test; $super(); }); + x = new X(); + throw new Error("error was not thrown"); + }catch(e){ + console.assert(e.message === "must call super constructor before accessing 'this'", e.message); + } + try{ X = Class.extend(function X($super){ this(); $super(); }); x = new X(); @@ -139,11 +147,11 @@ console.group("Error Traps"); } try{ - X = Class.extend(function X($super){ this.test; $super(); }); + X = Class.extend(function X($super){ new this(); $super(); }); x = new X(); throw new Error("error was not thrown"); }catch(e){ - console.assert(e.message === "must call super constructor before accessing 'this'", e.message); + console.assert(e.message === "this is not a constructor", e.message); } try{ From f9aa12e81c7216fb0f6ba0cdf2330ebc0d220cbc Mon Sep 17 00:00:00 2001 From: wizard04wsu Date: Fri, 28 May 2021 22:30:19 -0400 Subject: [PATCH 10/18] comments --- src/Class.mjs.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Class.mjs.js b/src/Class.mjs.js index b8196a2..a219526 100644 --- a/src/Class.mjs.js +++ b/src/Class.mjs.js @@ -45,7 +45,7 @@ defineNonEnumerableProperty(BaseClass, "extend", extend); function extend(init, call){ /** * @typedef {function} initializer - * @param {function} $super - The parent class's constructor, bound as the first argument. It is to be used like the `super` keyword. It *must* be called within the constructor, and it *should* be called before using `this`. + * @param {function} $super - The parent class's constructor, bound as the first argument. It is to be used like the `super` keyword. It *must* be called exactly once during the execution of the constructor, before any use of the `this` keyword. * @param {...*} args * @returns {object} - An object providing access to protected members. */ @@ -66,12 +66,12 @@ function extend(init, call){ * @class * @augments ParentClass * @private - * @throws {ReferenceError} - unexpected use of 'new' keyword + * @throws {ReferenceError} - unexpected use of `new` keyword * @throws {ReferenceError} - super constructor may be called only once during execution of derived constructor * @throws {ReferenceError} - invalid delete involving super constructor - * @throws {ReferenceError} - must call super constructor before accessing 'this' + * @throws {ReferenceError} - must call super constructor before accessing `this` * @throws {ReferenceError} - must call super constructor before returning from derived constructor - * @throws {ReferenceError} - class constructor cannot be invoked without 'new' + * @throws {ReferenceError} - class constructor cannot be invoked without `new` */ function ChildClass(...argumentsList){ const newInstance = this; @@ -96,7 +96,7 @@ function extend(init, call){ } }); - //I don't believe there's a way to trap access to 'this', but we can at least trap access to its members: + //I don't believe there's a way to trap access to `this`, but we can at least trap access to its members: function denyAccessToKeywordThis(){ if(!_$superCalled) throw new ReferenceError("must call super constructor before accessing 'this'"); } @@ -138,7 +138,7 @@ function extend(init, call){ defineNonEnumerableProperty(ChildClass, "extend", extend); //use a Proxy to distinguish calls to ChildClass between those that do and do not use the `new` keyword - //@throws {TypeError} if called without the `new` keyword and without the `{@link call}` argument. + //@throws {TypeError} if called without the `new` keyword and without the '{@link call}' argument. const proxyForChildClass = new Proxy(ChildClass, { construct(target, argumentsList, newTarget){ //target===ChildClass, newTarget===the proxy itself (proxyForChildClass) _instanceIsUnderConstruction = false; @@ -150,15 +150,15 @@ function extend(init, call){ }, apply(target, thisArg, argumentsList){ //target===ChildClass or a super class if(_instanceIsUnderConstruction){ - //the 'new' keyword was used, and this proxy handler is for the constructor of a super class + //the `new` keyword was used, and this proxy handler is for the constructor of a super class _instanceIsUnderConstruction = false; return target.apply(thisArg, argumentsList); //thisArg===the new instance } else if(call){ - //the 'new' keyword was not used, and a 'call' function was passed to 'extend' + //the `new` keyword was not used, and a 'call' function was passed to 'extend' - return call.apply(thisArg, argumentsList); //thisArg==='this' in the the caller's context + return call.apply(thisArg, argumentsList); //thisArg===`this` in the the caller's context } throw new TypeError(`class constructor ${className} cannot be invoked without 'new'`); } From ea5ca4f75e2a117c1933bde0d8edac3095a56315 Mon Sep 17 00:00:00 2001 From: wizard04wsu Date: Fri, 28 May 2021 22:31:31 -0400 Subject: [PATCH 11/18] comments --- src/Class.mjs.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Class.mjs.js b/src/Class.mjs.js index a219526..fb6e11f 100644 --- a/src/Class.mjs.js +++ b/src/Class.mjs.js @@ -66,12 +66,12 @@ function extend(init, call){ * @class * @augments ParentClass * @private - * @throws {ReferenceError} - unexpected use of `new` keyword + * @throws {ReferenceError} - unexpected use of 'new' keyword * @throws {ReferenceError} - super constructor may be called only once during execution of derived constructor * @throws {ReferenceError} - invalid delete involving super constructor - * @throws {ReferenceError} - must call super constructor before accessing `this` + * @throws {ReferenceError} - must call super constructor before accessing 'this' * @throws {ReferenceError} - must call super constructor before returning from derived constructor - * @throws {ReferenceError} - class constructor cannot be invoked without `new` + * @throws {ReferenceError} - class constructor cannot be invoked without 'new' */ function ChildClass(...argumentsList){ const newInstance = this; From d4d6a27579e4978792f504286f0874867629773a Mon Sep 17 00:00:00 2001 From: wizard04wsu Date: Fri, 28 May 2021 22:35:55 -0400 Subject: [PATCH 12/18] comments --- src/Class.mjs.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Class.mjs.js b/src/Class.mjs.js index fb6e11f..d7f7fcb 100644 --- a/src/Class.mjs.js +++ b/src/Class.mjs.js @@ -5,10 +5,10 @@ export { BaseClass as default }; -//key of a property of Class instances; the property is an object with the instance's protected members +//for a Class instance property, an object with the instance's protected members const protectedMembers = Symbol("protected members"); -//state: true iif an instance of a class is being constructed +//state: true when indicating that a class is being constructed let _instanceIsUnderConstruction = false; From 5b41066e7bd8949d88f0c4ed77509d12467dfb07 Mon Sep 17 00:00:00 2001 From: wizard04wsu Date: Sat, 29 May 2021 01:10:50 -0400 Subject: [PATCH 13/18] - rewrite readme.md --- README.md | 143 +++++++++++++----------------------------------------- 1 file changed, 34 insertions(+), 109 deletions(-) diff --git a/README.md b/README.md index ab6fe02..1753d3a 100644 --- a/README.md +++ b/README.md @@ -1,130 +1,55 @@ # JavaScript Class Implementation -This implementation allows for classes to be given protected access to items in a super-class. +This implementation adds the capability for class instances to have [**protected members**](#readme-protected) that can be accessed by derivative class constructors. -This is a JavaScript module. +This is a JavaScript module. It can be imported into your script like so: `import Class from "Class.mjs.js"` ---- +# Class.extend() -## Creating subclasses +Creates a child class. This is a static method of `Class` and its derivative classes. -***Class*.extend([*options*])** - -Creates a new class that inherits from the parent class. - -Parameters: -- ***options*** {object} -This can include any of the following: - - - **className** {string} - Used for the `name` property of the class constructor, and in the `toString` method for instances of the class. If not specified, it will be the same as the parent class. - - - **constructorFn** {function} - Initializes new instances of the class.

- ***options*.constructorFn(*Super*[, ...])**

- ***Super*** {function} is to be called from inside `constructorFn` to initialize the class, using the class's parent's constructor. It should be called as soon as possible, before using the `this` keyword, to ensure that the instance is properly initialized.

- Additionally, *Super* provides access to protected members (see below). - - - **returnFn** {function} - Returns a value when the constructor is called without using the `new` keyword. - -#### Example +## Syntax +```javascript +Class.extend(initializer) +Class.extend(initializer, applier) ``` -let Rectangle = Class.extend({ - className:"Rectangle", - constructorFn:function (Super, width, height){ - Super(); - this.width = width||0; - this.height = height||0; - 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)); - } -}); - -let Square = Rectangle.extend({ - className:"Square", - 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(" "); - this.whatAmI = function (){ return iAm; }; - }, - returnFn:function (width){ - return Math.pow(width||0, 2); - } -}); - -let s = new Square(3); - -s.toString(); //[instance of Square] -s.area(); //9 -s.height = 4; -s.area(); //16 -s.whatAmI(); //I am a rectangle. I am a square. -``` +### Parameters -### Protected members +[***initializer***](#readme-initializer) +A function to be executed by the constructor, during the process of constructing a new instance of the child class. The name of the *initializer* is used as the name of the class. -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/deletions/overloads of its members that are made here in the constructor will be reflected in the class' descendants. +***applier*** *optional* +A handler function for when the class is called without using the `new` keyword. Default behavior is to throw a [TypeError](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypeError). -#### Example +### Return value -``` -let Alpha = Class.extend({ - className:"Alpha", - constructorFn:function (Super){ - Super(); - let randomInstanceID = Math.random(); - Super.protected.rando = randomInstanceID; - } -}); - -let Bravo = Alpha.extend({ - className:"Bravo", - constructorFn:function (Super){ - Super(); - this.foo = "My ID is "+Super.protected.rando; - } -}); - -let b = new Bravo(); - -b.foo; //My ID is ... +The new class constructor. It has its own static copy of the `extend` method. + + +### Initializer +The signature of the *initializer* function is expected to be: +```javascript +function MyClassName($super, ...args){ + //code that does not include `this` + const protectedMembers = $super(...args); + //code that may include `this` +} ``` +***$super*** +A [Proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) to the parent class's constructor, bound as the first argument of the *initializer*. It is to be used like the [`super`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/super) keyword. It *must* be called exactly once during the execution of the constructor, *before* any reference to `this`. ---- + +***protectedMembers*** +An object whose members are shared among all the initializers that are executed when a new instance of the class is created. This allows a protected value defined in the *initializer* of a class to be accessed and modified within the *initializer* of a derivative class directly, without needing static getters and setters. -### Private members +## Description -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. +TODO -#### Example using a WeakMap +## Examples -``` -let Cuber = (function (){ - - const private = new WeakMap(); - - function cube(){ return Math.pow(private.get(this).val, 3); } - - return Class.extend({ - constructorFn: function (Super, myVal){ - Super(); - private.set(this, { val: myVal }); - this.cube = cube; - } - }); - -})(); - -let c = new Cuber(5); - -c.cube(); //125 -``` +TODO From b7e43123cd369cd45022ffd0c2d37447c559b313 Mon Sep 17 00:00:00 2001 From: wizard04wsu Date: Sat, 29 May 2021 14:20:29 -0400 Subject: [PATCH 14/18] - add examples to readme - add tests - delete old tests --- README.md | 58 +++++++++++++++++++++++++++--- test/Class testing.js | 50 +++++++++++++++++++++++++- test/Privates using symbols.htm | 18 ---------- test/privates.js | 64 --------------------------------- 4 files changed, 103 insertions(+), 87 deletions(-) delete mode 100644 test/Privates using symbols.htm delete mode 100644 test/privates.js diff --git a/README.md b/README.md index 1753d3a..759d372 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Class.extend(initializer, applier) ### Parameters [***initializer***](#readme-initializer) -A function to be executed by the constructor, during the process of constructing a new instance of the child class. The name of the *initializer* is used as the name of the class. +A function to be executed by the constructor during the process of constructing a new instance of the child class. The name of the *initializer* is used as the name of the class. ***applier*** *optional* A handler function for when the class is called without using the `new` keyword. Default behavior is to throw a [TypeError](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypeError). @@ -46,10 +46,60 @@ A [Proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Glob ***protectedMembers*** An object whose members are shared among all the initializers that are executed when a new instance of the class is created. This allows a protected value defined in the *initializer* of a class to be accessed and modified within the *initializer* of a derivative class directly, without needing static getters and setters. -## Description +## Examples -TODO +### Create a new class -## Examples +```javascript +const MyClass = Class.extend(function Rectangle($super, width, height){ + $super(); + this.area = function (){ return width * height; }; +}); + +let r = new MyClass(2, 3); + +console.log(MyClass.name); // Rectangle +console.log(r.toString()); // [object Rectangle] +console.log(r.area()); // 6 +``` + +### Inherit from a parent class + +```javascript +const Rectangle = Class.extend(function Rectangle($super, width, height){ + $super(); + this.dimensions = ()=>width+" x "+height; +}); + +const Square = Rectangle.extend(function Square($super, width){ + $super(width, width); + //this.dimensions() is inherited from Rectangle +}); + +let s = new Square(2); + +console.log(s.dimensions()); // 2 x 2 +``` + +### Use static methods of a parent class + +```javascript +const Rectangle = Class.extend(function Rectangle($super){ + $super(); +}); +Rectangle.area = function (width, height){ return width * height; }; + +const Square = Rectangle.extend(function Square($super, width){ + $super(); + this.area = function (){ + return $super.area(width, width); //using `$super` is equivalent to using `Rectangle` + }; +}); + +let s = new Square(2); + +console.log(Rectangle.area(2, 2)); // 4 +console.log(s.area()); // 4 +``` TODO diff --git a/test/Class testing.js b/test/Class testing.js index be0605a..3cf1acd 100644 --- a/test/Class testing.js +++ b/test/Class testing.js @@ -18,7 +18,6 @@ console.group("Class"); let cl = new Class(); console.dir(cl); //console.log(cl); - console.assert(Class.prototype.toString() === "[object Class]", Class.prototype.toString()); console.assert(cl.toString() === "[object Class]", cl.toString()); console.groupEnd(); console.groupEnd(); @@ -242,3 +241,52 @@ console.group("Rectangle & Square"); s.changeColor("blue"); console.assert(s.whatAmI() === "I am a blue rectangle. I am a blue square.", s.whatAmI()); console.groupEnd(); + +console.group("Readme Examples"); + console.group("Create a new class"); + let MyClass = Class.extend(function Rectangle($super, width, height){ + $super(); + this.area = function (){ return width * height; }; + }); + + let r = new MyClass(2, 3); + + console.assert(MyClass.name === "Rectangle", MyClass.name); + console.assert(r.toString() === "[object Rectangle]", r.toString()); + console.assert(r.area() === 6, r.area()); + console.groupEnd(); + console.group("Inherit from a parent class"); + Rectangle = Class.extend(function Rectangle($super, width, height){ + $super(); + this.dimensions = ()=>width+" x "+height; + }); + + Square = Rectangle.extend(function Square($super, width){ + $super(width, width); + //this.dimensions() is inherited from Rectangle + }); + + s = new Square(2); + + console.assert(s.dimensions() === "2 x 2", s.dimensions()); + console.groupEnd(); + console.group("Use static methods of a parent class"); + Rectangle = Class.extend(function Rectangle($super){ + $super(); + }); + Rectangle.area = function (width, height){ return width * height; }; + + Square = Rectangle.extend(function Square($super, width){ + console.assert($super.area(3, 3) === 9, $super.area(3, 3)); + $super(); + this.area = function (){ + return $super.area(width, width); //using `$super` is equivalent to using `Rectangle` + }; + }); + + s = new Square(2); + + console.assert(Rectangle.area(2, 2) === 4, Rectangle.area(2, 2)); + console.assert(s.area() === 4, s.area()); + console.groupEnd(); +console.groupEnd(); diff --git a/test/Privates using symbols.htm b/test/Privates using symbols.htm deleted file mode 100644 index b9b994e..0000000 --- a/test/Privates using symbols.htm +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - Class testing - - - - - - -

See console for assertions.

- - - diff --git a/test/privates.js b/test/privates.js deleted file mode 100644 index 5157329..0000000 --- a/test/privates.js +++ /dev/null @@ -1,64 +0,0 @@ -import Class from "../src/Class.mjs.js" - -let Alpha = (function (){ - - const $private = Symbol("private members of Alpha instances"); - - function getMyVal(){ - return this[$private].val; - } - - return Class.extend({ - className: "Alpha", - constructorFn: function (Super, myVal){ - Super(); - this[$private] = {}; - this[$private].val = myVal; - Object.defineProperty(Super.protected, "myVal", { - get: ()=>this[$private].val, - enumerable:true, configurable:true - }); - this.getMyVal = getMyVal; - } - }); - -})(); - -let Bravo = (function (){ - - const $private = Symbol("private members of Bravo instances"); - - 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.protected.myVal; }; - this[$private] = {}; - this[$private].getVal = function (){ return Super.protected.myVal; }; - this[$private].protected = Super.protected; - this.viaPrivate = viaPrivate, - this.viaReferenceToProtected = viaReferenceToProtected - } - }); - -})(); - -console.group("Alpha instance"); -let a = new Alpha(5); -console.assert(a.getMyVal() === 5, "getMyVal()", a.getMyVal()); -console.groupEnd(); - -console.group("Bravo instance"); -let b = new Bravo(10); -console.assert(b.getMyVal() === 10, "getMyVal()", b.getMyVal()); //via inherited method -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.groupEnd(); From c2792526732abb2b911a6b20643c05a58b8b9be7 Mon Sep 17 00:00:00 2001 From: wizard04wsu Date: Sat, 29 May 2021 14:39:34 -0400 Subject: [PATCH 15/18] - update readme & tests --- README.md | 15 ++++++++------- src/Class.mjs.js.md | 41 +++++++++++++++++++++++++++++++++++++++++ test/Class testing.js | 11 ++++++----- 3 files changed, 55 insertions(+), 12 deletions(-) create mode 100644 src/Class.mjs.js.md diff --git a/README.md b/README.md index 759d372..a2e7c85 100644 --- a/README.md +++ b/README.md @@ -53,14 +53,14 @@ An object whose members are shared among all the initializer ```javascript const MyClass = Class.extend(function Rectangle($super, width, height){ $super(); - this.area = function (){ return width * height; }; + this.dimensions = ()=>width+" x "+height; }); let r = new MyClass(2, 3); -console.log(MyClass.name); // Rectangle -console.log(r.toString()); // [object Rectangle] -console.log(r.area()); // 6 +console.log(MyClass.name); // Rectangle +console.log(r.toString()); // [object Rectangle] +console.log(r.dimensions()); // 2 x 3 ``` ### Inherit from a parent class @@ -84,15 +84,16 @@ console.log(s.dimensions()); // 2 x 2 ### Use static methods of a parent class ```javascript -const Rectangle = Class.extend(function Rectangle($super){ +const Rectangle = Class.extend(function Rectangle($super, width, height){ $super(); + this.dimensions = ()=>width+" x "+height; }); Rectangle.area = function (width, height){ return width * height; }; const Square = Rectangle.extend(function Square($super, width){ - $super(); + $super(width, width); this.area = function (){ - return $super.area(width, width); //using `$super` is equivalent to using `Rectangle` + return $super.area(width, width); //here, using `$super` as an object is equivalent to using `Rectangle` }; }); diff --git a/src/Class.mjs.js.md b/src/Class.mjs.js.md new file mode 100644 index 0000000..99574de --- /dev/null +++ b/src/Class.mjs.js.md @@ -0,0 +1,41 @@ + + +## Class + +* [Class](#module_Class) + * _static_ + * [.extend(init, [call])](#module_Class.extend) ⇒ Class + * _inner_ + * [~initializer](#module_Class..initializer) ⇒ object + + + +### Class.extend(init, [call]) ⇒ Class +Creates a child class. + +**Kind**: static method of [Class](#module_Class) +**Returns**: Class - - The new child class. +**Throws**: + +- TypeError - 'extend' method requires that 'this' be a Class constructor +- TypeError - 'init' is not a function +- TypeError - 'init' must be a named function +- TypeError - 'call' is not a function + + +| Param | Type | Description | +| --- | --- | --- | +| init | initializer | Handler to initialize a new instance of the child class. The name of the function is used as the name of the class. | +| [call] | function | Handler for when the class is called without using the `new` keyword. Default behavior is to throw a TypeError. | + + + +### Class~initializer ⇒ object +**Kind**: inner typedef of [Class](#module_Class) +**Returns**: object - - An object providing access to protected members. + +| Param | Type | Description | +| --- | --- | --- | +| $super | function | The parent class's constructor, bound as the first argument. It is to be used like the `super` keyword. It *must* be called exactly once during the execution of the constructor, before any use of the `this` keyword. | +| ...args | \* | | + diff --git a/test/Class testing.js b/test/Class testing.js index 3cf1acd..59f298b 100644 --- a/test/Class testing.js +++ b/test/Class testing.js @@ -246,14 +246,14 @@ console.group("Readme Examples"); console.group("Create a new class"); let MyClass = Class.extend(function Rectangle($super, width, height){ $super(); - this.area = function (){ return width * height; }; + this.dimensions = ()=>width+" x "+height; }); let r = new MyClass(2, 3); console.assert(MyClass.name === "Rectangle", MyClass.name); console.assert(r.toString() === "[object Rectangle]", r.toString()); - console.assert(r.area() === 6, r.area()); + console.assert(r.dimensions() === "2 x 3", r.dimensions()); console.groupEnd(); console.group("Inherit from a parent class"); Rectangle = Class.extend(function Rectangle($super, width, height){ @@ -271,16 +271,17 @@ console.group("Readme Examples"); console.assert(s.dimensions() === "2 x 2", s.dimensions()); console.groupEnd(); console.group("Use static methods of a parent class"); - Rectangle = Class.extend(function Rectangle($super){ + Rectangle = Class.extend(function Rectangle($super, width, height){ $super(); + this.dimensions = ()=>width+" x "+height; }); Rectangle.area = function (width, height){ return width * height; }; Square = Rectangle.extend(function Square($super, width){ console.assert($super.area(3, 3) === 9, $super.area(3, 3)); - $super(); + $super(width, width); this.area = function (){ - return $super.area(width, width); //using `$super` is equivalent to using `Rectangle` + return $super.area(width, width); //here, using `$super` as an object is equivalent to using `Rectangle` }; }); From e44b3951fb3760a3d06a136033d39d84df781993 Mon Sep 17 00:00:00 2001 From: wizard04wsu Date: Sat, 29 May 2021 14:47:36 -0400 Subject: [PATCH 16/18] - update readme & tests --- README.md | 11 ++++++++--- test/Class testing.js | 9 ++++++--- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index a2e7c85..e7c351e 100644 --- a/README.md +++ b/README.md @@ -52,15 +52,20 @@ An object whose members are shared among all the initializer ```javascript const MyClass = Class.extend(function Rectangle($super, width, height){ - $super(); - this.dimensions = ()=>width+" x "+height; -}); + $super(); + this.dimensions = ()=>width+" x "+height; + }, + (width, height)=>"area = "+(width*height) //applier function for when Rectangle() is called without using `new` +); let r = new MyClass(2, 3); console.log(MyClass.name); // Rectangle + console.log(r.toString()); // [object Rectangle] console.log(r.dimensions()); // 2 x 3 + +console.log(MyClass(2, 3)); // area = 6 ``` ### Inherit from a parent class diff --git a/test/Class testing.js b/test/Class testing.js index 59f298b..8967cb3 100644 --- a/test/Class testing.js +++ b/test/Class testing.js @@ -245,15 +245,18 @@ console.groupEnd(); console.group("Readme Examples"); console.group("Create a new class"); let MyClass = Class.extend(function Rectangle($super, width, height){ - $super(); - this.dimensions = ()=>width+" x "+height; - }); + $super(); + this.dimensions = ()=>width+" x "+height; + }, + (width, height)=>"area = "+(width*height) //applier function for when Rectangle() is called without using `new` + ); let r = new MyClass(2, 3); console.assert(MyClass.name === "Rectangle", MyClass.name); console.assert(r.toString() === "[object Rectangle]", r.toString()); console.assert(r.dimensions() === "2 x 3", r.dimensions()); + console.assert(MyClass(2, 3) === "area = 6", MyClass(2, 3)); console.groupEnd(); console.group("Inherit from a parent class"); Rectangle = Class.extend(function Rectangle($super, width, height){ From 7169f41bcfd66b24a2b265d83faba067d5f767e1 Mon Sep 17 00:00:00 2001 From: wizard04wsu Date: Sat, 29 May 2021 15:00:32 -0400 Subject: [PATCH 17/18] - add test --- README.md | 25 ++++++++++++++++++------- test/Class testing.js | 22 +++++++++++++++------- 2 files changed, 33 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index e7c351e..84fc0b8 100644 --- a/README.md +++ b/README.md @@ -52,20 +52,29 @@ An object whose members are shared among all the initializer ```javascript const MyClass = Class.extend(function Rectangle($super, width, height){ - $super(); - this.dimensions = ()=>width+" x "+height; - }, - (width, height)=>"area = "+(width*height) //applier function for when Rectangle() is called without using `new` -); + $super(); + this.dimensions = ()=>width+" x "+height; +}); let r = new MyClass(2, 3); console.log(MyClass.name); // Rectangle - console.log(r.toString()); // [object Rectangle] console.log(r.dimensions()); // 2 x 3 +``` -console.log(MyClass(2, 3)); // area = 6 +### Use an applier function + +```javascript +const Rectangle = Class.extend(function Rectangle($super, width, height){ + $super(); + this.dimensions = ()=>width+" x "+height; + }, + (width, height)=>"area = "+(width*height) //applier function for when Rectangle() is called without using `new` +); + +console.log((new Rectangle(2, 3)).toString()); // [object Rectangle] +console.log(Rectangle(2, 3)); // area = 6 ``` ### Inherit from a parent class @@ -108,4 +117,6 @@ console.log(Rectangle.area(2, 2)); // 4 console.log(s.area()); // 4 ``` +### Use protected members + TODO diff --git a/test/Class testing.js b/test/Class testing.js index 8967cb3..a97323a 100644 --- a/test/Class testing.js +++ b/test/Class testing.js @@ -245,18 +245,26 @@ console.groupEnd(); console.group("Readme Examples"); console.group("Create a new class"); let MyClass = Class.extend(function Rectangle($super, width, height){ - $super(); - this.dimensions = ()=>width+" x "+height; - }, - (width, height)=>"area = "+(width*height) //applier function for when Rectangle() is called without using `new` - ); + $super(); + this.area = function (){ return width * height; }; + }); let r = new MyClass(2, 3); console.assert(MyClass.name === "Rectangle", MyClass.name); console.assert(r.toString() === "[object Rectangle]", r.toString()); - console.assert(r.dimensions() === "2 x 3", r.dimensions()); - console.assert(MyClass(2, 3) === "area = 6", MyClass(2, 3)); + console.assert(r.area() === 6, r.area()); + console.groupEnd(); + console.group("Use an applier function"); + Rectangle = Class.extend(function Rectangle($super, width, height){ + $super(); + this.dimensions = ()=>width+" x "+height; + }, + (width, height)=>"area = "+(width*height) //applier function for when Rectangle() is called without using `new` + ); + + console.assert((new Rectangle(2, 3)).toString() === "[object Rectangle]", (new Rectangle(2, 3)).toString()); + console.assert(Rectangle(2, 3) === "area = 6", Rectangle(2, 3)); console.groupEnd(); console.group("Inherit from a parent class"); Rectangle = Class.extend(function Rectangle($super, width, height){ From c7e3ecd0690c91926c1392445f856777ba79db5a Mon Sep 17 00:00:00 2001 From: wizard04wsu Date: Sat, 29 May 2021 15:19:27 -0400 Subject: [PATCH 18/18] - add test for protected members --- README.md | 46 ++++++++++++++++++++++++++++++++++++++++--- test/Class testing.js | 45 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 84fc0b8..475f87f 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ console.log((new Rectangle(2, 3)).toString()); // [object Rectangle] console.log(Rectangle(2, 3)); // area = 6 ``` -### Inherit from a parent class +### Inherit from a superclass ```javascript const Rectangle = Class.extend(function Rectangle($super, width, height){ @@ -95,7 +95,7 @@ let s = new Square(2); console.log(s.dimensions()); // 2 x 2 ``` -### Use static methods of a parent class +### Use static methods of the parent class ```javascript const Rectangle = Class.extend(function Rectangle($super, width, height){ @@ -119,4 +119,44 @@ console.log(s.area()); // 4 ### Use protected members -TODO +```javascript +const Rectangle = Class.extend(function Rectangle($super, width, height){ + const prot = $super(); + + prot.width = width; + prot.height = height; + + Object.defineProperty(this, "width", { + enumerable: true, configurable: true, + get(){ return prot.width; }, + set(width){ return prot.width = width; } + }); + Object.defineProperty(this, "height", { + enumerable: true, configurable: true, + get(){ return prot.height; }, + set(height){ return prot.height = height; } + }); + + this.dimensions = ()=>prot.width+" x "+prot.height; +}); + +const Square = Rectangle.extend(function Square($super, width){ + const prot = $super(width, width); + + Object.defineProperty(this, "width", { + enumerable: true, configurable: true, + get(){ return prot.width; }, + set(width){ return prot.width = prot.height = width; } + }); + Object.defineProperty(this, "height", { + enumerable: true, configurable: true, + get(){ return prot.height; }, + set(height){ return prot.height = prot.width = height; } + }); +}); + +let s = new Square(2); +console.log(s.dimensions()); // 2 x 2 +s.height = 3; +console.log(s.dimensions()); // 3 x 3 +``` diff --git a/test/Class testing.js b/test/Class testing.js index a97323a..ae8c02f 100644 --- a/test/Class testing.js +++ b/test/Class testing.js @@ -255,6 +255,7 @@ console.group("Readme Examples"); console.assert(r.toString() === "[object Rectangle]", r.toString()); console.assert(r.area() === 6, r.area()); console.groupEnd(); + console.group("Use an applier function"); Rectangle = Class.extend(function Rectangle($super, width, height){ $super(); @@ -266,6 +267,7 @@ console.group("Readme Examples"); console.assert((new Rectangle(2, 3)).toString() === "[object Rectangle]", (new Rectangle(2, 3)).toString()); console.assert(Rectangle(2, 3) === "area = 6", Rectangle(2, 3)); console.groupEnd(); + console.group("Inherit from a parent class"); Rectangle = Class.extend(function Rectangle($super, width, height){ $super(); @@ -281,6 +283,7 @@ console.group("Readme Examples"); console.assert(s.dimensions() === "2 x 2", s.dimensions()); console.groupEnd(); + console.group("Use static methods of a parent class"); Rectangle = Class.extend(function Rectangle($super, width, height){ $super(); @@ -301,4 +304,46 @@ console.group("Readme Examples"); console.assert(Rectangle.area(2, 2) === 4, Rectangle.area(2, 2)); console.assert(s.area() === 4, s.area()); console.groupEnd(); + + console.group("Use protected members"); + Rectangle = Class.extend(function Rectangle($super, width, height){ + const prot = $super(); + + prot.width = width; + prot.height = height; + + Object.defineProperty(this, "width", { + enumerable: true, configurable: true, + get(){ return prot.width; }, + set(width){ return prot.width = width; } + }); + Object.defineProperty(this, "height", { + enumerable: true, configurable: true, + get(){ return prot.height; }, + set(height){ return prot.height = height; } + }); + + this.dimensions = ()=>prot.width+" x "+prot.height; + }); + + Square = Rectangle.extend(function Square($super, width){ + const prot = $super(width, width); + + Object.defineProperty(this, "width", { + enumerable: true, configurable: true, + get(){ return prot.width; }, + set(width){ return prot.width = prot.height = width; } + }); + Object.defineProperty(this, "height", { + enumerable: true, configurable: true, + get(){ return prot.height; }, + set(height){ return prot.height = prot.width = height; } + }); + }); + + s = new Square(2); + console.assert(s.dimensions() === "2 x 2", s.dimensions()); + s.height = 3; + console.assert(s.dimensions() === "3 x 3", s.dimensions()); + console.groupEnd(); console.groupEnd();