/********************************************************************** * * object.js * * Repo and docs: * https://github.com/flynx/object.js * * * XXX should this extend Object??? * ...if yes then it would also be logical to move Object.run(..) * here... * **********************************************************************/ ((typeof define)[0]=='u'?function(f){module.exports=f(require)}:define) (function(require){ var module={} // make module AMD/node compatible... /*********************************************************************/ // Helpers... var TAB_SIZE = module.TAB_SIZE = 4 // Normalize indent... // // normalizeIndent(text) // -> text // // // This will remove common indent from each like of text, this is useful // for printing function code of functions that were defined at deep // levels of indent. // // NOTE: this will trim out both leading and trailing white-space. // // XXX is this the right place for this??? // ...when moving take care that ImageGrid's core.doc uses this... var normalizeIndent = module.normalizeIndent = function(text, tab_size){ tab_size = tab_size || TAB_SIZE text = tab_size > 0 ? text.replace(/\t/g, ' '.repeat(tab_size)) : text var lines = text.split(/\n/) var l = lines .reduce(function(l, e, i){ var indent = e.length - e.trimLeft().length return e.trim().length == 0 // ignore 0 indent of first line... || (i == 0 && indent == 0) ? l : l < 0 ? indent : Math.min(l, indent) }, -1) return lines .map(function(line, i){ return i == 0 ? line : line.slice(l) }) .join('\n') .trim() } //--------------------------------------------------------------------- // Prototype chain content access... // Get a list of source objects for a prop/attr name... // // sources(obj, name) // sources(obj, name, callback) // -> list // -> [] // // callback(obj) // -> true | 'stop' // -> .. // // // The callback(..) is called with each matching object. // // The callback(..) can be used to break/stop the search, returning // a partial list og matcges up untill and including the object // triggering the stop. // // // NOTE: this go up the prototype chain, not caring about any role ( // instance/class or instance/prototype) bounderies and depends // only on the object given as the starting point. // It is possible to start the search from this, thus checking // for any overloading in the instance, though this approach is // not very reusable.... // NOTE: this will not trigger any props... var sources = module.sources = function(obj, name, callback){ var stop var res = [] do { if(obj.hasOwnProperty(name)){ res.push(obj) // handle callback... stop = callback && callback(obj) // stop requested by callback... if(stop === true || stop == 'stop'){ return res } } obj = obj.__proto__ } while(obj !== null) return res } // Find the next parent attribute in the prototype chain. // // Get parent attribute value... // parent(proto, name) // -> value // -> undefined // // Get parent method... // parent(method, this) // -> meth // -> undefined // // // The two forms differ in: // - in parent(method, ..) a method's .name attr is used for name. // - in parent(method, ..) the containing prototype is inferred. // // NOTE: there are cases where method.name is not set (e.g. anonymous // function), so there a name should be passed explicitly... // NOTE: when passing a method it is recommended to pass an explicit // reference to it relative to the constructor, i.e.: // Constructor.prototype.method // this will avoid relative resolution loops, for example: // this.method // deep in a chain will resolve to the first .method value visible // from 'this', i.e. the top most value and not the value visible // from that particular level... // // // Example: // var X = object.Constructor('X', { // __proto__: Y.prototype, // // attr: 123, // // method: function(){ // // get attribute... // var a = object.parent(X.prototype, 'attr') // // // get method... // var ret = object.parent(X.prototype.method, this) // .call(this, ...arguments) // // // ... // } // }) // // // NOTE: in the general case this will get the value of the returned // property/attribute, the rest of the way passive to props. // The method case will get the value of every method from 'this' // and to the method after the match. // NOTE: this is super(..) replacement, usable in any context without // restriction -- super(..) is restricted to class methods only... var parent = module.parent = function(proto, name){ // special case: method... if(typeof(name) != typeof('str')){ that = name name = proto.name // get first matching source... proto = sources(that, name, function(obj){ return obj[name] === proto }) .pop() } // get first source... var res = sources(proto, name, function(obj){ return 'stop' }) .pop() return res ? // get next value... res.__proto__[name] : undefined } // Find the next parent property descriptor in the prototype chain... // // parentProperty(proto, name) // -> prop-descriptor // // // This is like parent(..) but will get a property descriptor... // var parentProperty = module.parentProperty = function(proto, name){ // get second source... var c = 0 var res = sources(proto, name, function(obj){ return c++ == 1 }) .pop() return res ? // get next value... Object.getOwnPropertyDescriptor(res, name) : undefined } // Find the next parent method and call it... // // parentCall(proto, name, this, ...) // parentCall(meth, this, ...) // -> res // -> undefined // // // This also gracefully handles the case when no higher level definition // is found, i.e. the corresponding parent(..) call will return undefined // or a non-callable. // // NOTE: this is just like parent(..) but will call the retrieved method, // essentially this is a shorthand to: // parent(proto, name).call(this, ...) // or: // parent(method, this).call(this, ...) // NOTE: for more docs see parent(..) var parentCall = module.parentCall = function(proto, name, that, ...args){ var meth = parent(proto, name) return meth instanceof Function ? meth.call(...( typeof(name) == typeof('str') ? [...arguments].slice(2) : [...arguments].slice(1) )) : undefined } //--------------------------------------------------------------------- // Mixin utils... // XXX should we add mixout(..) and friends ??? // Mix a set of methods/props/attrs into an object... // // mixinFlat(root, object, ...) // -> root // // // NOTE: essentially this is just like Object.assign(..) but copies // properties directly rather than copying property values... var mixinFlat = module.mixinFlat = function(root, ...objects){ return objects .reduce(function(root, cur){ Object.keys(cur) .map(function(k){ Object.defineProperty(root, k, Object.getOwnPropertyDescriptor(cur, k)) }) return root }, root) } // Mix sets of methods/props/attrs into an object as prototypes... // // mixin(root, object, ...) // -> root // // // This will create a new object per set of methods given and // mixinFlat(..) the method set into this object leaving the // original objects intact. // // root <-- object1_copy <-- .. <-- objectN_copy // var mixin = module.mixin = function(root, ...objects){ return objects .reduce(function(res, cur){ return module.mixinFlat(Object.create(res), cur) }, root) } //--------------------------------------------------------------------- // Constructor... // Make an uninitialized instance object... // // makeRawInstance(context, constructor, ...) // -> instance // // // This will: // - construct an object // - if .__new__(..) is defined // -> call and use its return value // - if prototype is a function or if .__call__(..) is defined // -> use a wrapper function // - else // -> use {} // - link the object into the prototype chain // // // This will not call .__init__(..) // // // NOTE: context is only used when passeding to .__new__(..) if defined, // and is ignored otherwise... // NOTE: as this is simply an extension to the base JavaScript protocol this // can be used to construct any object... // Example: // var O = function(){} // // new is optional... // var o = new makeRawInstance(null, O) // NOTE: .__new__(..) is intentionaly an instance method (contary to // Python) this is done because there are no classes in JS and // adding and instance constructor as a class method would create // unneccessary restrictions both on the "class" object and on the // instance... // // XXX Q: should the context (this) in .__new__(..) be _constructor or // .prototype??? // ... .prototype seems to be needed more often but through it we // can't reach the actual constructor... but on the other hand we // can (should?) always explicitly use it -- .__new__(..) is usually // in the same scope + this makes it more reusable for chaining // .__new__(..) calls... // ...currently it's .prototype... var makeRawInstance = module.makeRawInstance = function(context, constructor, ...args){ var _mirror_doc = function(func, target){ Object.defineProperty(func, 'toString', { value: function(...args){ return target.toString(...args) }, enumerable: false, }) return func } var obj = // prototype defines .__new__(..)... constructor.prototype.__new__ instanceof Function ? constructor.prototype.__new__(context, ...args) // callable instance -- prototype is a function... // NOTE: we need to isolate the .prototype from instances... : constructor.prototype instanceof Function ? _mirror_doc( function(){ return constructor.prototype .call(obj, this, ...arguments) }, constructor.prototype) // callable instance -- prototype defines .__call__(..)... // NOTE: we need to isolate the .__call__ from instances... : constructor.prototype.__call__ instanceof Function ? _mirror_doc( function(){ return constructor.prototype.__call__ .call(obj, this, ...arguments) }, constructor.prototype.__call__) // default object base... : {} // link to prototype chain... obj.__proto__ = constructor.prototype Object.defineProperty(obj, 'constructor', { value: constructor, enumerable: false, }) return obj } // Make an object constructor function... // // Make a constructor with an object prototype... // Constructor(name, proto) // -> constructor // // Make a constructor with a prototype (object/function) and a class // prototype... // Constructor(name, class-proto, proto) // -> constructor // NOTE: the defines a set of class methods and // attributes. // // // The resulting constructor can produce objects in one of these ways: // // Create instance... // constructor(..) // new constructor // new constructor(..) // -> instance // // Create raw/uninitialized instance... // constructor.__rawinstance__(..) // makeRawInstance(null, constructor, ..) // -> raw-instance // // // All produced objects are instances of the constructor // instance instanceof constructor // -> true // // // // Create and initialization protocol: // 1) raw instance is created: // a) constructor.__rawinstance__(..) / makeRawInstance(..) called: // - call .__new__(..) if defined and get return value as // instance, or // - if .__call__(..) defined or prototype is a function, wrap // it and use the wrapper function as instance, or // - create an empty object // b) instance linked to prototype chain // set .__proto__ to constructor.prototype // 2) instance is initialized: // call .__init__(..) if defined // // // // Special methods (constructor): // // Handle uninitialized instance construction // .__rawinstance__(context, ...) // -> instance // NOTE: This is a shorthand to makeRawInstance(..) see it for // details. // // // Special methods (.prototype): // // Create new instance object... // .__new__(context, ..) // -> object // // Handle instance call... // .__call__(context, ..) // -> .. // // Initialize instance object... // .__init__(..) // -> .. // // // NOTE: raw instance creation is defined by makeRawInstance(..) so see // it for more info. // NOTE: raw instance creation can be completely overloaded by defining // .__rawinstance__(..) on the constructor. // // // // Inheritance: // A simple way to build C -> B -> A chain would be: // // // NOTE: new is optional... // var A = new Constructor('A') // // // NOTE: in a prototype chain the prototypes are "inherited" // // NOTE: JS has no classes and the prototype is just another // // object, the only difference is that it's used by the // // constructor to link other objects i.e. "instances" to... // var B = Constructor('B', {__proto__: A.prototype}) // // var C = Constructor('C', Objec.create(B.prototype)) // // var c = C() // // c instanceof C // -> true // c instanceof B // -> true // c instanceof A // -> true // // A.prototype.x = 123 // // c.x // -> 123 // // // // Motivation: // The general motivation here is to standardise the constructor // protocol and make a single simple way to go with minimal variation. // This is due to the JavaScript base protocol though quite simple, // being too flexible making it very involved to produce objects in a // consistent manner by hand, especially in long running projects, // in turn spreading all the refactoring over multiple sites and styles. // // This removes part of the flexibility and in return gives us: // - single, well defined protocol // - one single spot where all the "magic" happens // - full support for existing JavaScript ways of doing things // - easy refactoring without touching the client code // // // NOTE: this sets the proto's .constructor attribute, thus rendering it // not reusable, to use the same prototype for multiple objects // clone it via. Object.create(..) or copy it... // NOTE: to disable .__rawinstance__(..) handling set it to false in the // class prototype... var Constructor = module.Constructor = // shorthand... module.C = function Constructor(name, a, b){ var proto = b == null ? a : b var cls_proto = b == null ? b : a proto = proto || {} // the actual constructor... var _constructor = function Constructor(){ // create raw instance... var obj = _constructor.__rawinstance__ ? _constructor.__rawinstance__(this, ...arguments) : makeRawInstance(this, _constructor, ...arguments) // initialize... obj.__init__ instanceof Function && obj.__init__(...arguments) return obj } _constructor.name = name // just in case the browser refuses to change the name, we'll make // it a different offer ;) _constructor.name == 'Constructor' // NOTE: this eval(..) should not be a risk as its inputs are // static and never infuenced by external inputs... && eval('_constructor = '+ _constructor .toString() .replace(/Constructor/g, name)) // set .toString(..)... // NOTE: do this only if .toString(..) is not defined by user... ;((cls_proto || {}).toString() == ({}).toString()) && Object.defineProperty(_constructor, 'toString', { value: function(){ var args = proto.__init__ ? proto.__init__ .toString() .split(/\n/)[0] .replace(/function\(([^)]*)\){.*/, '$1') : '' var code = proto.__init__ ? proto.__init__ .toString() .replace(/[^{]*{/, '{') : '{ .. }' return `${this.name}(${args})${normalizeIndent(code)}` }, enumerable: false, }) _constructor.__proto__ = cls_proto _constructor.prototype = proto // generic raw instance constructor... _constructor.__rawinstance__ instanceof Function || (_constructor.__rawinstance__ = function(context, ...args){ return makeRawInstance(context, this, ...args) }) // set .prototype.constructor Object.defineProperty(_constructor.prototype, 'constructor', { value: _constructor, enumerable: false, }) return _constructor } /********************************************************************** * vim:set ts=4 sw=4 : */ return module })