Skip to content

Commit

Permalink
Merge pull request #8 from wizard04wsu/dev
Browse files Browse the repository at this point in the history
v10.0.1
  • Loading branch information
wizard04wsu authored Jun 21, 2021
2 parents 6795960 + 1d36fdc commit e056f3d
Show file tree
Hide file tree
Showing 5 changed files with 216 additions and 255 deletions.
34 changes: 17 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# JavaScript Class Implementation

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

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

# Class.extend()

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

```javascript
Class.extend(initializer)
Class.extend(initializer, applier)
Class.extend(init)
Class.extend(init, call)
```

### Parameters

[**<code>*initializer*</code>**](#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 *<code>initializer</code>* is used as the name of the class.
**<code>*init*</code>**
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.

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

### Return value

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

The signature of the *<code>initializer</code>* function is expected to be:
The signature of an initializer function is expected to be:
```javascript
function MyClassName($super, ...args){
//code that does not include `this`
const protectedMembers = $super(...args);
const protectedMembers = $super(arg1, arg2, ...);
//code that may include `this`
}
```

**<code>*$super*</code>**
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`.
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`.

<a name="readme-protected"></a>
**<code>*protectedMembers*</code>**
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.
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.

## Examples

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

### Use an applier function
### Use a call handler

```javascript
const Rectangle = Class.extend(function Rectangle($super, width, height){
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`
(width, height)=>"area is "+(width*height)+" square units" //handler for when Rectangle() is called without using `new`
);

console.log((new Rectangle(2, 3)).toString()); // [object Rectangle]
console.log(Rectangle(2, 3)); // area = 6
console.log(Rectangle(2, 3)); // area is 6 square units
```

### Inherit from a superclass
Expand Down
189 changes: 189 additions & 0 deletions src/Class.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
// https://github.com/wizard04wsu/Class

/** @module Class */

export { BaseClass as default };


//for a Class instance property, an object with the instance's protected members
const protectedMembersSymbol = Symbol("protected members");

//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
let _invokingSuperConstructor = false;


/**
* @alias module:Class-Class
* @abstract
* @class
*/
const BaseClass = function Class(){

if(!new.target && !_invokingSuperConstructor){
//the 'new' keyword was not used

throw new TypeError(`class constructor 'Class' cannot be invoked without 'new'`);
}

_invokingSuperConstructor = false;
defineNonEnumerableProperty(this, protectedMembersSymbol, {}, true);

}

//*** for the prototype ***

//rename it so Object.prototype.toString() will use the base class's name
defineNonEnumerableProperty(BaseClass.prototype, Symbol.toStringTag, "Class", true);

//*** for the constructor ***

//make extend() a static member of the base class
defineNonEnumerableProperty(BaseClass, "extend", extend);


/**
* 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(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 exactly once during the execution of the initializer, before any use of the `this` keyword.
* @param {...*} args
* @returns {object} - An object providing access to protected members.
*/

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");

const ParentClass = this;
const className = init.name;

/**
* The constructor for the new class.
* @class
* @augments ParentClass
* @private
* @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} - class constructor cannot be invoked without 'new'
*/
function ChildClass(...argumentsList){

if(!new.target && !_invokingSuperConstructor){
//the 'new' keyword was not used

//if a 'call' function was passed to 'extend', return its result
if(call)
return call(...argumentsList);

throw new TypeError(`class constructor '${className}' cannot be invoked without 'new'`);
}

const newInstance = this;

_invokingSuperConstructor = false;
let _$superCalled = false;
const $super = new Proxy(ParentClass, {
construct(target, argumentsList, newTarget){
//target = ParentClass
//newTarget = $super

//disallow use of the 'new' keyword when calling '$super'
throw new ReferenceError("unexpected use of 'new' keyword");
},
apply(target, thisArg, argumentsList){
//target = ParentClass

if(_$superCalled)
throw new ReferenceError("super constructor may be called only once during execution of derived constructor");
_$superCalled = true;

_invokingSuperConstructor = true;
target.apply(newInstance, argumentsList);

return newInstance[protectedMembersSymbol];
},
deleteProperty(target, property){
//target = ParentClass

//disallow deletion of static members of a parent class
throw new ReferenceError("invalid delete involving super constructor");
}
});

//I don't believe there's a way to trap access to `this` itself, but we can at least trap access to its properties:
function proxyThisMethod(methodName, argumentsList){
if(!_$superCalled)
throw new ReferenceError("must call super constructor before accessing 'this'");
return Reflect[methodName](...argumentsList);
}
let proxyForKeywordThis = new Proxy(newInstance, {
defineProperty(){ return proxyThisMethod("defineProperty", arguments); },
deleteProperty(){ return proxyThisMethod("deleteProperty", arguments); },
get(){ return proxyThisMethod("get", arguments); },
getOwnPropertyDescriptor(){ return proxyThisMethod("getOwnPropertyDescriptor", arguments); },
getPrototypeOf(){ return proxyThisMethod("getPrototypeOf", arguments); },
has(){ return proxyThisMethod("has", arguments); },
isExtensible(){ return proxyThisMethod("isExtensible", arguments); },
ownKeys(){ return proxyThisMethod("ownKeys", arguments); },
preventExtensions(){ return proxyThisMethod("preventExtensions", arguments); },
set(){ return proxyThisMethod("set", arguments); },
setPrototypeOf(){ return proxyThisMethod("setPrototypeOf", arguments); }
});

init.apply(proxyForKeywordThis, [$super, ...argumentsList]);

if(!_$superCalled) throw new ReferenceError("must call super constructor before returning from derived constructor");

return newInstance;
}

//*** for the prototype ***

//create the prototype (an instance of the parent class)
ChildClass.prototype = Object.create(ParentClass.prototype);
//rename it so Object.prototype.toString() will use the new class's name
defineNonEnumerableProperty(ChildClass.prototype, Symbol.toStringTag, className, true);
//set its constructor to be that of the new class
defineNonEnumerableProperty(ChildClass.prototype, "constructor", ChildClass);

//*** for the constructor ***

//rename it to be that of the initializer
defineNonEnumerableProperty(ChildClass, "name", className, true);
//override .toString() to only output the initializer function
defineNonEnumerableProperty(ChildClass, "toString", function toString(){ return init.toString(); });

//make extend() a static method of the new class
defineNonEnumerableProperty(ChildClass, "extend", extend);

return ChildClass;
}



/* helper functions */

function defineNonEnumerableProperty(object, property, value, readonly){
Object.defineProperty(object, property, {
writable: !readonly, enumerable: false, configurable: true,
value: value
});
}
Loading

0 comments on commit e056f3d

Please sign in to comment.