-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.js
409 lines (327 loc) · 15.5 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
const publicProtoToProtectedProto = new WeakMap
class WeakTwoWayMap {
constructor() { this.m = new WeakMap }
set( a, b ) { this.m.set( a, b ); this.m.set( b, a ) }
get( item ) { return this.m.get( item ) }
has( item ) { return this.m.has( item ) }
}
// A two-way map to associate public instances with protected instances.
// There is one protected instance per public instance
const publicToProtected = new WeakTwoWayMap
// used during construction of any class (always synchronous)
const newTargetStack = []
const defaultOptions = {
mode: 'es5' // es5 class inheritance is simple, nice, easy, and robust
//mode: 'Reflect.construct', // es6+ class inheritance is tricky, difficult, and restrictive
}
/**
* @param {string} definerFunction Function for defining a class...
*/
function Class(options, className, definerFunction) {
"use strict"
const { mode } = options
if (typeof className == 'function' && !definerFunction) {
definerFunction = className
className = definerFunction.name || ''
}
if (typeof className != 'string')
throw new TypeError(`If supplying two arguments, you must specify a string for the first 'className' argument. If supplying only one argument, it must be a named function.`)
if (typeof definerFunction != 'function')
throw new TypeError('If supplying two arguments, you must specify a function for the second `definerFunction` argument. If supplying only one argument, it must be a named function.')
const ParentClass = this || Object
const parentPublicPrototype = ParentClass.prototype
// A two-way map to associate public instances with private instances.
// Unlike publicToProtected, this is inside here because there is one
// private instance per Class scope per instance (or to say it another way,
// each instance has as many private instances as the number of classes
// that the given instance has in its inheritance chain, one private
// instance per class)
const publicToPrivate = new WeakTwoWayMap
// the class "scope" that we will bind to the helper functions
const scope = {
publicToPrivate,
parentPublicPrototype,
}
// create the super helper for this class scope
const supers = new WeakMap
const _super = superHelper.bind( null, supers, scope )
// bind this class' scope to the helper functions
const _getPublicMembers = getPublicMembers.bind( null, scope )
const _getProtectedMembers = getProtectedMembers.bind( null, scope )
const _getPrivateMembers = getPrivateMembers.bind( null, scope )
// pass the helper functions to the user's class definition function
const definition = definerFunction( _getPublicMembers, _getProtectedMembers, _getPrivateMembers, _super)
// the user has the option of returning an object that defines which
// properties are public/protected/private.
if (definition && typeof definition !== 'object') {
throw new TypeError('The return value of a class definer function, if any, should be an object.')
}
// extend the parent class
const publicPrototype = definition && definition.public || definition || {}
publicPrototype.__proto__ = parentPublicPrototype
// extend the parent protected prototype
const parentProtectedPrototype = publicProtoToProtectedProto.get(parentPublicPrototype) || {}
const protectedPrototype = definition && definition.protected || {}
protectedPrototype.__proto__ = parentProtectedPrototype
publicProtoToProtectedProto.set(publicPrototype, protectedPrototype)
// private prototype does not inherit from parent, each private instance is
// private only for the class of this scope
const privatePrototype = definition && definition.private || {}
scope.publicPrototype = publicPrototype
scope.privatePrototype = privatePrototype
scope.protectedPrototype = protectedPrototype
scope.parentProtectedPrototype = parentProtectedPrototype
// the user has the option of assigning methods and properties to the
// helpers that we passed in, to let us know which methods and properties are
// public/protected/private so we can assign them onto the respective
// prototypes.
copyDescriptors(_getPublicMembers, publicPrototype)
copyDescriptors(_getProtectedMembers, protectedPrototype)
copyDescriptors(_getPrivateMembers, privatePrototype)
// if a `public` object was also supplied, also copy the definition props
// to the `public` prototype
//
// TODO For now we prioritize the "public" object returned from the
// definer, and copy from the definition to the publicPrototype, but this
// won't work with `super`. Maybe later, we can use a Proxy to read props
// from both the root object and the public object, so that `super` works
// from both.
if (definition && definition !== publicPrototype) {
// delete these so we don't copy them
delete definition.public
delete definition.protected
delete definition.private
// copy whatever remains
copyDescriptors(definition, publicPrototype)
}
let NewClass = null
// ES5 version (which seems to be so much better)
if ( mode === 'es5' ) {
console.log('es5 mode')
const userConstructor = publicPrototype.constructor
// Create the constructor for the class of this scope.
// We create the constructor inside of this immediately-invoked function (IIFE)
// just so that we can give it a `className`.
// We pass whatever we need from the outer scope into the IIFE.
NewClass = new Function(`
userConstructor,
publicPrototype,
protectedPrototype,
privatePrototype,
ParentClass,
publicToProtected,
publicToPrivate
`, `
return function ${className}() {
// make a protected instance if it doesn't exist already. Only the
// child-most class constructor will create the protected instance,
// because the publicToProtected map is shared among them all.
let protectedInstance = publicToProtected.get( this )
if ( !protectedInstance ) {
protectedInstance = Object.create( protectedPrototype )
publicToProtected.set( this, protectedInstance )
}
// make a private instance. Each class constructor will create one
// for a given instance because each constructor accesses the
// publicToPrivate map from its class scope (it isn't shared like
// publicToProtected is)
privateInstance = Object.create( privatePrototype )
publicToPrivate.set( this, privateInstance )
if (userConstructor) userConstructor.apply(this, arguments)
else ParentClass.apply(this, arguments)
}
`)(
userConstructor,
publicPrototype,
protectedPrototype,
privatePrototype,
ParentClass,
publicToProtected,
publicToPrivate
)
// standard ES5 class definition
NewClass.__proto__ = ParentClass // static inheritance
NewClass.prototype = publicPrototype
NewClass.prototype.constructor = NewClass // TODO: make non-writable and non-configurable like ES6+
debugger
}
// ES6+ version (which seems to be dumb)
else if ( mode === 'Reflect.construct' ) {
console.log('es6 mode')
const userConstructor = publicPrototype.hasOwnProperty('constructor') ? publicPrototype.constructor : false
if (userConstructor) {
console.log('userConstructor:', userConstructor)
console.log('parent class:', ParentClass)
userConstructor.__proto__ = ParentClass // static inheritance
userConstructor.prototype = publicPrototype
//userConstructor.prototype.constructor = userConstructor // already the case
}
// Create the constructor for the class of this scope.
// We create the constructor inside of this immediately-invoked function (IIFE)
// just so that we can give it a `className`.
// We pass whatever we need from the outer scope into the IIFE.
NewClass = new Function(`
userConstructor,
publicPrototype,
protectedPrototype,
privatePrototype,
ParentClass,
publicToProtected,
publicToPrivate,
newTargetStack
`, `
//return class ${className} extends ParentClass
return function ${className}() {
let self = null
console.log(" ------------- userConstructor?")
console.log(userConstructor && userConstructor.toString())
let newTarget = new.target
if ( newTarget ) newTargetStack.push( newTarget )
else newTarget = newTargetStack[ newTargetStack.length - 1 ]
if (userConstructor) {
console.log('??????????', userConstructor, arguments, newTarget)
self = Reflect.construct( userConstructor, arguments, newTarget )
}
else self = Reflect.construct( ParentClass, arguments, newTarget )
newTargetStack.pop()
// make a protected instance if it doesn't exist already. Only the
// child-most class constructor will create the protected instance,
// because the publicToProtected map is shared among them all.
let protectedInstance = publicToProtected.get( self )
if ( !protectedInstance ) {
protectedInstance = Object.create( protectedPrototype )
publicToProtected.set( self, protectedInstance )
}
// make a private instance. Each class constructor will create one
// for a given instance because each constructor accesses the
// publicToPrivate map from its class scope (it isn't shared like
// publicToProtected is)
privateInstance = Object.create( privatePrototype )
publicToPrivate.set( self, privateInstance )
return self
}
`)(
userConstructor,
publicPrototype,
protectedPrototype,
privatePrototype,
ParentClass,
publicToProtected,
publicToPrivate,
newTargetStack
)
// static inheritance
if (userConstructor) NewClass.__proto__ = userConstructor
else NewClass.__proto__ = ParentClass
NewClass.prototype = Object.create( publicPrototype )
NewClass.prototype.constructor = NewClass // TODO: make non-writable and non-configurable like ES6+
}
else {
throw new TypeError('The lowclass mode option should be one of: "es5", "Reflect.construct".')
}
// allow users to make subclasses. This defines what `this` is in the above
// definition of ParentClass.
NewClass.subclass = _Class.configure( options )
return NewClass
}
function getPublicMembers( scope, instance ) {
// check only for the private instance of this class scope
if ( instance.__proto__ === scope.privatePrototype )
return scope.publicToPrivate.get( instance )
// check for an instance of the class (or its subclasses) of this scope
else if ( hasPrototype( instance, scope.protectedPrototype ) )
return publicToProtected.get( instance )
// otherwise just return whatever was passed in, it's public already!
else return instance
}
class InvalidAccessError extends Error {}
function getProtectedMembers( scope, instance ) {
// check for an instance of the class (or its subclasses) of this scope
// This allows for example an instance of an Animal base class to access
// protected members of an instance of a Dog child class.
if ( hasPrototype( instance, scope.publicPrototype ) )
return publicToProtected.get( instance )
// check only for the private instance of this class scope
else if ( instance.__proto__ === scope.privatePrototype )
return publicToProtected.get( scope.publicToPrivate.get( instance ) )
// return the protected instance if it was passed in
else if ( hasPrototype( instance, scope.protectedPrototype ) )
return instance
throw new InvalidAccessError('invalid access of protected member')
}
function getPrivateMembers( scope, instance ) {
// check for a public instance that is or inherits from this class
if ( hasPrototype( instance, scope.publicPrototype ) )
return scope.publicToPrivate.get( instance )
// check for a protected instance that is or inherits from this class' protectedPrototype
else if ( hasPrototype( instance, scope.protectedPrototype ) )
return scope.publicToPrivate.get( publicToProtected.get( instance ) )
// return the private instance if it was passed in
else if ( instance.__proto__ === scope.privatePrototype )
return instance
throw new InvalidAccessError('invalid access of private member')
}
// check if an object has the given prototype in its chain
function hasPrototype( obj, proto ) {
let currentProto = obj.__proto__
do {
if ( proto === currentProto ) return true
currentProto = currentProto.__proto__
} while ( currentProto )
return false
}
// copy all properties (as descriptors) from source to destination
function copyDescriptors(source, destination, mod) {
const props = Object.keys(source)
let i = props.length
while (i--) {
const prop = props[i]
const descriptor = Object.getOwnPropertyDescriptor(source, prop)
if (mod) mod(descriptor)
Object.defineProperty(destination, prop, descriptor)
}
}
class InvalidSuperAccessError extends Error {}
function superHelper( supers, scope, instance ) {
const {
publicPrototype,
protectedPrototype,
parentPublicPrototype,
parentProtectedPrototype,
} = scope
if ( hasPrototype( instance, publicPrototype ) )
return getSuperHelperObject( instance, parentPublicPrototype, supers )
if ( hasPrototype( instance, protectedPrototype ) )
return getSuperHelperObject( instance, parentProtectedPrototype, supers )
// TODO: does it make sense to add _super support for private members
// here? Let's add it when/if we need it.
throw new InvalidSuperAccessError('invalid super access')
}
function getSuperHelperObject( instance, parentPrototype, supers ) {
let _super = supers.get( instance )
if ( !_super ) {
supers.set( instance, _super = {} )
copyDescriptors( parentPrototype, _super, (descriptor) => {
if ( descriptor.value && typeof descriptor.value === 'function' ) {
console.log('WTF?', descriptor.value, typeof descriptor.value, global === descriptor.value)
debugger
descriptor.value = descriptor.value.bind( instance )
}
//else { TODO how to handle get/set
//descriptor.get = descriptor.get.bind( instance )
//descriptor.set = descriptor.set.bind( instance )
//}
})
}
return _super
}
function _Class( ...args ) {
return Class( defaultOptions, ...args )
}
_Class.configure = function( options ) {
return function( ...args ) {
return Class.call( this, options, ...args )
}
}
module.exports = _Class
module.exports.InvalidAccessError = InvalidAccessError