|
1 | 1 | # JavaScript Class Implementation |
2 | 2 |
|
3 | | -This implementation allows for classes to be given protected access to items in a super-class. |
| 3 | +This implementation adds the capability for class instances to have [**protected members**](#readme-protected) that can be accessed by derivative class constructors. |
4 | 4 |
|
5 | | -This is a JavaScript module. |
| 5 | +This is a JavaScript module. It can be imported into your script like so: `import Class from "Class.mjs.js"` |
6 | 6 |
|
7 | | ---- |
| 7 | +# Class.extend() |
8 | 8 |
|
9 | | -## Creating subclasses |
| 9 | +Creates a child class. This is a static method of `Class` and its derivative classes. |
10 | 10 |
|
11 | | -**<samp>*Class*.extend([*options*])</samp>** |
| 11 | +## Syntax |
12 | 12 |
|
13 | | -Creates a new class that inherits from the parent class. |
| 13 | +```javascript |
| 14 | +Class.extend(initializer) |
| 15 | +Class.extend(initializer, applier) |
| 16 | +``` |
14 | 17 |
|
15 | | -Parameters: |
16 | | -- **<code>*options*</code>** {object} |
17 | | -This can include any of the following: |
18 | | - |
19 | | - - **<code>className</code>** {string} |
20 | | - 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. |
21 | | - |
22 | | - - **<code>constructorFn</code>** {function} |
23 | | - Initializes new instances of the class.<br><br> |
24 | | - **<samp>*options*.constructorFn(*Super*[, ...])</samp>**<br><br> |
25 | | - **<code>*Super*</code>** {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.<br><br> |
26 | | - Additionally, <code>*Super*</code> provides access to protected members (<a href="#protected-members">see below</a>). |
27 | | - |
28 | | - - **<code>returnFn</code>** {function} |
29 | | - Returns a value when the constructor is called without using the `new` keyword. |
| 18 | +### Parameters |
30 | 19 |
|
31 | | -#### Example |
| 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. |
32 | 22 |
|
33 | | -``` |
34 | | -let Rectangle = Class.extend({ |
35 | | - className:"Rectangle", |
36 | | - constructorFn:function (Super, width, height){ |
37 | | - Super(); |
38 | | - this.width = width||0; |
39 | | - this.height = height||0; |
40 | | - this.area = function (){ return Math.abs(this.width * this.height); }; |
41 | | - this.whatAmI = function (){ return "I am a rectangle."; }; |
42 | | - }, |
43 | | - returnFn:function (width, height){ |
44 | | - return Math.abs((width||0) * (height||0)); |
45 | | - } |
46 | | -}); |
| 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). |
47 | 25 |
|
48 | | -let Square = Rectangle.extend({ |
49 | | - className:"Square", |
50 | | - constructorFn:function (Super, width){ |
51 | | - Super(width, width); |
52 | | - Object.defineProperty(this, "height", { get:function (){ return this.width; }, set:function (val){ this.width = 1*val||0; }, enumerable:true, configurable:true }); |
53 | | - let iAm = [this.whatAmI(), "I am a square."].join(" "); |
54 | | - this.whatAmI = function (){ return iAm; }; |
55 | | - }, |
56 | | - returnFn:function (width){ |
57 | | - return Math.pow(width||0, 2); |
58 | | - } |
59 | | -}); |
| 26 | +### Return value |
60 | 27 |
|
61 | | -let s = new Square(3); |
| 28 | +The new class constructor. It has its own static copy of the `extend` method. |
62 | 29 |
|
63 | | -s.toString(); //[instance of Square] |
64 | | -s.area(); //9 |
65 | | -s.height = 4; |
66 | | -s.area(); //16 |
67 | | -s.whatAmI(); //I am a rectangle. I am a square. |
| 30 | +<a name="readme-initializer"></a> |
| 31 | +### Initializer |
68 | 32 |
|
| 33 | +The signature of the *<code>initializer</code>* function is expected to be: |
| 34 | +```javascript |
| 35 | +function MyClassName($super, ...args){ |
| 36 | + //code that does not include `this` |
| 37 | + const protectedMembers = $super(...args); |
| 38 | + //code that may include `this` |
| 39 | +} |
69 | 40 | ``` |
70 | 41 |
|
71 | | -### Protected members |
| 42 | +**<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`. |
72 | 44 |
|
73 | | -A class can give its descendants protected access to its private variables. Once <code>*Super*</code> is called within the constructor, the protected properties of its parent class are made available via **<code>*Super*.protected</code>**. This object will be available to child classes as well; any additions/deletions/overloads of its members that are made here in the constructor will be reflected in the class' descendants. |
| 45 | +<a name="readme-protected"></a> |
| 46 | +**<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. |
74 | 48 |
|
75 | | -#### Example |
| 49 | +## Examples |
76 | 50 |
|
77 | | -``` |
78 | | -let Alpha = Class.extend({ |
79 | | - className:"Alpha", |
80 | | - constructorFn:function (Super){ |
81 | | - Super(); |
82 | | - let randomInstanceID = Math.random(); |
83 | | - Super.protected.rando = randomInstanceID; |
84 | | - } |
85 | | -}); |
| 51 | +### Create a new class |
86 | 52 |
|
87 | | -let Bravo = Alpha.extend({ |
88 | | - className:"Bravo", |
89 | | - constructorFn:function (Super){ |
90 | | - Super(); |
91 | | - this.foo = "My ID is "+Super.protected.rando; |
92 | | - } |
| 53 | +```javascript |
| 54 | +const MyClass = Class.extend(function Rectangle($super, width, height){ |
| 55 | + $super(); |
| 56 | + this.dimensions = ()=>width+" x "+height; |
93 | 57 | }); |
94 | 58 |
|
95 | | -let b = new Bravo(); |
| 59 | +let r = new MyClass(2, 3); |
| 60 | + |
| 61 | +console.log(MyClass.name); // Rectangle |
| 62 | +console.log(r.toString()); // [object Rectangle] |
| 63 | +console.log(r.dimensions()); // 2 x 3 |
| 64 | +``` |
| 65 | + |
| 66 | +### Use an applier function |
96 | 67 |
|
97 | | -b.foo; //My ID is ... |
| 68 | +```javascript |
| 69 | +const Rectangle = Class.extend(function Rectangle($super, width, height){ |
| 70 | + $super(); |
| 71 | + this.dimensions = ()=>width+" x "+height; |
| 72 | + }, |
| 73 | + (width, height)=>"area = "+(width*height) //applier function for when Rectangle() is called without using `new` |
| 74 | +); |
98 | 75 |
|
| 76 | +console.log((new Rectangle(2, 3)).toString()); // [object Rectangle] |
| 77 | +console.log(Rectangle(2, 3)); // area = 6 |
99 | 78 | ``` |
100 | 79 |
|
| 80 | +### Inherit from a superclass |
101 | 81 |
|
102 | | ---- |
| 82 | +```javascript |
| 83 | +const Rectangle = Class.extend(function Rectangle($super, width, height){ |
| 84 | + $super(); |
| 85 | + this.dimensions = ()=>width+" x "+height; |
| 86 | +}); |
103 | 87 |
|
104 | | -### Private members |
| 88 | +const Square = Rectangle.extend(function Square($super, width){ |
| 89 | + $super(width, width); |
| 90 | + //this.dimensions() is inherited from Rectangle |
| 91 | +}); |
105 | 92 |
|
106 | | -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. |
| 93 | +let s = new Square(2); |
107 | 94 |
|
108 | | -#### Example using a WeakMap |
| 95 | +console.log(s.dimensions()); // 2 x 2 |
| 96 | +``` |
| 97 | + |
| 98 | +### Use static methods of the parent class |
| 99 | + |
| 100 | +```javascript |
| 101 | +const Rectangle = Class.extend(function Rectangle($super, width, height){ |
| 102 | + $super(); |
| 103 | + this.dimensions = ()=>width+" x "+height; |
| 104 | +}); |
| 105 | +Rectangle.area = function (width, height){ return width * height; }; |
109 | 106 |
|
| 107 | +const Square = Rectangle.extend(function Square($super, width){ |
| 108 | + $super(width, width); |
| 109 | + this.area = function (){ |
| 110 | + return $super.area(width, width); //here, using `$super` as an object is equivalent to using `Rectangle` |
| 111 | + }; |
| 112 | +}); |
| 113 | + |
| 114 | +let s = new Square(2); |
| 115 | + |
| 116 | +console.log(Rectangle.area(2, 2)); // 4 |
| 117 | +console.log(s.area()); // 4 |
110 | 118 | ``` |
111 | | -let Cuber = (function (){ |
112 | | - |
113 | | - const private = new WeakMap(); |
| 119 | + |
| 120 | +### Use protected members |
| 121 | + |
| 122 | +```javascript |
| 123 | +const Rectangle = Class.extend(function Rectangle($super, width, height){ |
| 124 | + const prot = $super(); |
114 | 125 |
|
115 | | - function cube(){ return Math.pow(private.get(this).val, 3); } |
| 126 | + prot.width = width; |
| 127 | + prot.height = height; |
116 | 128 |
|
117 | | - return Class.extend({ |
118 | | - constructorFn: function (Super, myVal){ |
119 | | - Super(); |
120 | | - private.set(this, { val: myVal }); |
121 | | - this.cube = cube; |
122 | | - } |
| 129 | + Object.defineProperty(this, "width", { |
| 130 | + enumerable: true, configurable: true, |
| 131 | + get(){ return prot.width; }, |
| 132 | + set(width){ return prot.width = width; } |
| 133 | + }); |
| 134 | + Object.defineProperty(this, "height", { |
| 135 | + enumerable: true, configurable: true, |
| 136 | + get(){ return prot.height; }, |
| 137 | + set(height){ return prot.height = height; } |
123 | 138 | }); |
124 | 139 |
|
125 | | -})(); |
| 140 | + this.dimensions = ()=>prot.width+" x "+prot.height; |
| 141 | +}); |
126 | 142 |
|
127 | | -let c = new Cuber(5); |
| 143 | +const Square = Rectangle.extend(function Square($super, width){ |
| 144 | + const prot = $super(width, width); |
| 145 | + |
| 146 | + Object.defineProperty(this, "width", { |
| 147 | + enumerable: true, configurable: true, |
| 148 | + get(){ return prot.width; }, |
| 149 | + set(width){ return prot.width = prot.height = width; } |
| 150 | + }); |
| 151 | + Object.defineProperty(this, "height", { |
| 152 | + enumerable: true, configurable: true, |
| 153 | + get(){ return prot.height; }, |
| 154 | + set(height){ return prot.height = prot.width = height; } |
| 155 | + }); |
| 156 | +}); |
128 | 157 |
|
129 | | -c.cube(); //125 |
| 158 | +let s = new Square(2); |
| 159 | +console.log(s.dimensions()); // 2 x 2 |
| 160 | +s.height = 3; |
| 161 | +console.log(s.dimensions()); // 3 x 3 |
130 | 162 | ``` |
0 commit comments