|
| 1 | +// https://github.com/wizard04wsu/Class |
| 2 | + |
| 3 | +/** @module Class */ |
| 4 | + |
| 5 | +export { BaseClass as default }; |
| 6 | + |
| 7 | + |
| 8 | +//for a Class instance property, an object with the instance's protected members |
| 9 | +const protectedMembersSymbol = Symbol("protected members"); |
| 10 | + |
| 11 | +//state: when true, indicates that an instance of a class is being constructed and that there are still super class constructors that need to be invoked using $super |
| 12 | +let _invokingSuperConstructor = false; |
| 13 | + |
| 14 | + |
| 15 | +/** |
| 16 | + * @alias module:Class-Class |
| 17 | + * @abstract |
| 18 | + * @class |
| 19 | + */ |
| 20 | +const BaseClass = function Class(){ |
| 21 | + |
| 22 | + if(!new.target && !_invokingSuperConstructor){ |
| 23 | + //the 'new' keyword was not used |
| 24 | + |
| 25 | + throw new TypeError(`class constructor 'Class' cannot be invoked without 'new'`); |
| 26 | + } |
| 27 | + |
| 28 | + _invokingSuperConstructor = false; |
| 29 | + defineNonEnumerableProperty(this, protectedMembersSymbol, {}, true); |
| 30 | + |
| 31 | +} |
| 32 | + |
| 33 | +//*** for the prototype *** |
| 34 | + |
| 35 | +//rename it so Object.prototype.toString() will use the base class's name |
| 36 | +defineNonEnumerableProperty(BaseClass.prototype, Symbol.toStringTag, "Class", true); |
| 37 | + |
| 38 | +//*** for the constructor *** |
| 39 | + |
| 40 | +//make extend() a static member of the base class |
| 41 | +defineNonEnumerableProperty(BaseClass, "extend", extend); |
| 42 | + |
| 43 | + |
| 44 | +/** |
| 45 | + * Creates a child class. |
| 46 | + * @static |
| 47 | + * @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. |
| 48 | + * @param {function} [call] - Handler for when the class is called without using the `new` keyword. Default behavior is to throw a TypeError. |
| 49 | + * @return {Class} - The new child class. |
| 50 | + * @throws {TypeError} - 'extend' method requires that 'this' be a Class constructor |
| 51 | + * @throws {TypeError} - 'init' is not a function |
| 52 | + * @throws {TypeError} - 'init' must be a named function |
| 53 | + * @throws {TypeError} - 'call' is not a function |
| 54 | + */ |
| 55 | +function extend(init, call){ |
| 56 | + /** |
| 57 | + * @typedef {function} initializer |
| 58 | + * @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 initializer, before any use of the `this` keyword. |
| 59 | + * @param {...*} args |
| 60 | + * @returns {object} - An object providing access to protected members. |
| 61 | + */ |
| 62 | + |
| 63 | + if(typeof this !== "function" || !(this === BaseClass || this.prototype instanceof BaseClass)) |
| 64 | + throw new TypeError("'extend' method requires that 'this' be a Class constructor"); |
| 65 | + if(typeof init !== "function") |
| 66 | + throw new TypeError("'init' is not a function"); |
| 67 | + if(!init.name) |
| 68 | + throw new TypeError("'init' must be a named function"); |
| 69 | + if(arguments.length > 1 && typeof call !== "function") |
| 70 | + throw new TypeError("'call' is not a function"); |
| 71 | + |
| 72 | + const ParentClass = this; |
| 73 | + const className = init.name; |
| 74 | + |
| 75 | + /** |
| 76 | + * The constructor for the new class. |
| 77 | + * @class |
| 78 | + * @augments ParentClass |
| 79 | + * @private |
| 80 | + * @throws {ReferenceError} - unexpected use of 'new' keyword |
| 81 | + * @throws {ReferenceError} - super constructor may be called only once during execution of derived constructor |
| 82 | + * @throws {ReferenceError} - invalid delete involving super constructor |
| 83 | + * @throws {ReferenceError} - must call super constructor before accessing 'this' |
| 84 | + * @throws {ReferenceError} - must call super constructor before returning from derived constructor |
| 85 | + * @throws {ReferenceError} - class constructor cannot be invoked without 'new' |
| 86 | + */ |
| 87 | + function ChildClass(...argumentsList){ |
| 88 | + |
| 89 | + if(!new.target && !_invokingSuperConstructor){ |
| 90 | + //the 'new' keyword was not used |
| 91 | + |
| 92 | + //if a 'call' function was passed to 'extend', return its result |
| 93 | + if(call) |
| 94 | + return call(...argumentsList); |
| 95 | + |
| 96 | + throw new TypeError(`class constructor '${className}' cannot be invoked without 'new'`); |
| 97 | + } |
| 98 | + |
| 99 | + const newInstance = this; |
| 100 | + |
| 101 | + _invokingSuperConstructor = false; |
| 102 | + let _$superCalled = false; |
| 103 | + const $super = new Proxy(ParentClass, { |
| 104 | + construct(target, argumentsList, newTarget){ |
| 105 | + //target = ParentClass |
| 106 | + //newTarget = $super |
| 107 | + |
| 108 | + //disallow use of the 'new' keyword when calling '$super' |
| 109 | + throw new ReferenceError("unexpected use of 'new' keyword"); |
| 110 | + }, |
| 111 | + apply(target, thisArg, argumentsList){ |
| 112 | + //target = ParentClass |
| 113 | + |
| 114 | + if(_$superCalled) |
| 115 | + throw new ReferenceError("super constructor may be called only once during execution of derived constructor"); |
| 116 | + _$superCalled = true; |
| 117 | + |
| 118 | + _invokingSuperConstructor = true; |
| 119 | + target.apply(newInstance, argumentsList); |
| 120 | + |
| 121 | + return newInstance[protectedMembersSymbol]; |
| 122 | + }, |
| 123 | + deleteProperty(target, property){ |
| 124 | + //target = ParentClass |
| 125 | + |
| 126 | + //disallow deletion of static members of a parent class |
| 127 | + throw new ReferenceError("invalid delete involving super constructor"); |
| 128 | + } |
| 129 | + }); |
| 130 | + |
| 131 | + //I don't believe there's a way to trap access to `this` itself, but we can at least trap access to its properties: |
| 132 | + function proxyThisMethod(methodName, argumentsList){ |
| 133 | + if(!_$superCalled) |
| 134 | + throw new ReferenceError("must call super constructor before accessing 'this'"); |
| 135 | + return Reflect[methodName](...argumentsList); |
| 136 | + } |
| 137 | + let proxyForKeywordThis = new Proxy(newInstance, { |
| 138 | + defineProperty(){ return proxyThisMethod("defineProperty", arguments); }, |
| 139 | + deleteProperty(){ return proxyThisMethod("deleteProperty", arguments); }, |
| 140 | + get(){ return proxyThisMethod("get", arguments); }, |
| 141 | + getOwnPropertyDescriptor(){ return proxyThisMethod("getOwnPropertyDescriptor", arguments); }, |
| 142 | + getPrototypeOf(){ return proxyThisMethod("getPrototypeOf", arguments); }, |
| 143 | + has(){ return proxyThisMethod("has", arguments); }, |
| 144 | + isExtensible(){ return proxyThisMethod("isExtensible", arguments); }, |
| 145 | + ownKeys(){ return proxyThisMethod("ownKeys", arguments); }, |
| 146 | + preventExtensions(){ return proxyThisMethod("preventExtensions", arguments); }, |
| 147 | + set(){ return proxyThisMethod("set", arguments); }, |
| 148 | + setPrototypeOf(){ return proxyThisMethod("setPrototypeOf", arguments); } |
| 149 | + }); |
| 150 | + |
| 151 | + init.apply(proxyForKeywordThis, [$super, ...argumentsList]); |
| 152 | + |
| 153 | + if(!_$superCalled) throw new ReferenceError("must call super constructor before returning from derived constructor"); |
| 154 | + |
| 155 | + return newInstance; |
| 156 | + } |
| 157 | + |
| 158 | + //*** for the prototype *** |
| 159 | + |
| 160 | + //create the prototype (an instance of the parent class) |
| 161 | + ChildClass.prototype = Object.create(ParentClass.prototype); |
| 162 | + //rename it so Object.prototype.toString() will use the new class's name |
| 163 | + defineNonEnumerableProperty(ChildClass.prototype, Symbol.toStringTag, className, true); |
| 164 | + //set its constructor to be that of the new class |
| 165 | + defineNonEnumerableProperty(ChildClass.prototype, "constructor", ChildClass); |
| 166 | + |
| 167 | + //*** for the constructor *** |
| 168 | + |
| 169 | + //rename it to be that of the initializer |
| 170 | + defineNonEnumerableProperty(ChildClass, "name", className, true); |
| 171 | + //override .toString() to only output the initializer function |
| 172 | + defineNonEnumerableProperty(ChildClass, "toString", function toString(){ return init.toString(); }); |
| 173 | + |
| 174 | + //make extend() a static method of the new class |
| 175 | + defineNonEnumerableProperty(ChildClass, "extend", extend); |
| 176 | + |
| 177 | + return ChildClass; |
| 178 | +} |
| 179 | + |
| 180 | + |
| 181 | + |
| 182 | +/* helper functions */ |
| 183 | + |
| 184 | +function defineNonEnumerableProperty(object, property, value, readonly){ |
| 185 | + Object.defineProperty(object, property, { |
| 186 | + writable: !readonly, enumerable: false, configurable: true, |
| 187 | + value: value |
| 188 | + }); |
| 189 | +} |
0 commit comments