Skip to content
This repository was archived by the owner on Jul 12, 2025. It is now read-only.

Commit e056f3d

Browse files
authored
Merge pull request #8 from wizard04wsu/dev
v10.0.1
2 parents 6795960 + 1d36fdc commit e056f3d

5 files changed

Lines changed: 216 additions & 255 deletions

File tree

README.md

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# JavaScript Class Implementation
22

3-
This implementation adds the capability for class instances to have [**protected members**](#readme-protected) that can be accessed by derivative class constructors.
3+
This implementation adds the capability for a class to have [protected members](#readme-protected) that can be accessed by derivative class constructors.
44

5-
This is a JavaScript module. It can be imported into your script like so: `import Class from "Class.mjs.js"`
5+
This is a JavaScript module. It can be imported into your script like so: `import Class from "Class.mjs"`
66

77
# Class.extend()
88

@@ -11,17 +11,17 @@ Creates a child class. This is a static method of `Class` and its derivative cla
1111
## Syntax
1212

1313
```javascript
14-
Class.extend(initializer)
15-
Class.extend(initializer, applier)
14+
Class.extend(init)
15+
Class.extend(init, call)
1616
```
1717

1818
### Parameters
1919

20-
[**<code>*initializer*</code>**](#readme-initializer)
21-
A function to be executed by the constructor during the process of constructing a new instance of the child class. The name of the *<code>initializer</code>* is used as the name of the class.
20+
**<code>*init*</code>**
21+
An [initializer](#readme-initializer) function that is called as part of the child class's constructor. The name of the initializer is used as the name of the class.
2222

23-
**<code>*applier*</code>** *optional*
24-
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).
23+
**<code>*call*</code>** *optional*
24+
A handler function for when the class is called without using the `new` keyword. Default behavior is to throw a TypeError.
2525

2626
### Return value
2727

@@ -30,21 +30,21 @@ The new class constructor. It has its own static copy of the `extend` method.
3030
<a name="readme-initializer"></a>
3131
### Initializer
3232

33-
The signature of the *<code>initializer</code>* function is expected to be:
33+
The signature of an initializer function is expected to be:
3434
```javascript
3535
function MyClassName($super, ...args){
3636
//code that does not include `this`
37-
const protectedMembers = $super(...args);
37+
const protectedMembers = $super(arg1, arg2, ...);
3838
//code that may include `this`
3939
}
4040
```
4141

4242
**<code>*$super*</code>**
43-
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 *<code>initializer</code>*. 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`.
43+
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 *<code>initializer</code>*. 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 initializer, *before* any reference to `this`.
4444

4545
<a name="readme-protected"></a>
4646
**<code>*protectedMembers*</code>**
47-
An object whose members are shared among all the <i><code>initializer</code></i>s that are executed when a new instance of the class is created. This allows a protected value defined in the *<code>initializer</code>* of a class to be accessed and modified within the *<code>initializer</code>* of a derivative class directly, without needing static getters and setters.
47+
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.
4848

4949
## Examples
5050

@@ -63,18 +63,18 @@ console.log(r.toString()); // [object Rectangle]
6363
console.log(r.dimensions()); // 2 x 3
6464
```
6565

66-
### Use an applier function
66+
### Use a call handler
6767

6868
```javascript
69-
const Rectangle = Class.extend(function Rectangle($super, width, height){
69+
const Rectangle = Class.extend(
70+
function Rectangle($super, width, height){
7071
$super();
7172
this.dimensions = ()=>width+" x "+height;
7273
},
73-
(width, height)=>"area = "+(width*height) //applier function for when Rectangle() is called without using `new`
74+
(width, height)=>"area is "+(width*height)+" square units" //handler for when Rectangle() is called without using `new`
7475
);
7576

76-
console.log((new Rectangle(2, 3)).toString()); // [object Rectangle]
77-
console.log(Rectangle(2, 3)); // area = 6
77+
console.log(Rectangle(2, 3)); // area is 6 square units
7878
```
7979

8080
### Inherit from a superclass

src/Class.mjs

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
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

Comments
 (0)