X-Git-Url: http://git.squeep.com/?p=squeep-lazy-property;a=blobdiff_plain;f=index.js;fp=index.js;h=e30ed5b4c2edeed3f00d4f8f9de6b2f46d5a3912;hp=672e684b562d3ef8bb2d9574ca08dff18f4cac22;hb=7e55eb8f04aaa7d0d6a6c3de90313153a40800af;hpb=cde5aaf728595e10c50041e8b6deafe53f6b3eda diff --git a/index.js b/index.js index 672e684..e30ed5b 100644 --- a/index.js +++ b/index.js @@ -1,78 +1,98 @@ 'use strict'; /** - * Establish some immutable descriptor Shapes with our preferred defaults. - * We want normally-behaving properties unless told otherwise. + * Lazy Properties */ -function defaultShape(proto, ...objs) { - const newObject = Object.create(proto); - Object.assign(newObject, ...objs); - return newObject; + + +/** + * Shorthand helper for building descriptors up from a prototype chain. + * @param {*} proto + * @param {...any} objs + * @returns {Object} + */ +function createAssign(proto, ...objs) { + return Object.assign(Object.create(proto), ...objs); } -const defaultDataDescriptor = defaultShape(null, { +/** + * Defaults common to both data and accessor descriptors. + * Null prototype. + */ +const defaultCommonDescriptor = createAssign(null, { configurable: true, enumerable: true, +}); + +/** + * Defaults for an eventually-initialized property. + * Same as if a property was set normally. + */ +const defaultDataDescriptor = createAssign(defaultCommonDescriptor, { writable: true, value: undefined, }); -const defaultAccessorDescriptor = defaultShape(null, { - configurable: true, - enumerable: true, +/** + * Defaults for an interim accessor property. + */ +const defaultAccessorDescriptor = createAssign(defaultCommonDescriptor, { get: undefined, set: undefined, }); -/** - * Create a new descriptor which inherits our preferred defaults. - * @param {Object} defaultDescriptor - * @param {...any} objs - * @returns {Object} - */ -function descriptorFromDefault(defaultDescriptor, ...objs) { - return Object.assign(Object.create(defaultDescriptor), ...objs); -} - /** * Defer calling initializer to set value for name until name is read. + * If a lazy property is defined on an object which is used as a prototype, + * the objectBound flag determines whether the lazy initializer will be + * invoked just once and set the property value on the original prototype + * object, or for any inherited object and set the property on that object. + * The objectBound flag also controls the 'this' object of the initializer + * when called. * @param {Object} obj * @param {String} name * @param {() => {*}} initializer * @param {Object=} descriptor - * @param {Boolean} descriptor.configurable - * @param {Boolean} descriptor.enumerable - * @param {Boolean} descriptor.writable + * @param {Boolean=} descriptor.configurable + * @param {Boolean=} descriptor.enumerable + * @param {Boolean=} descriptor.writable + * @param {Boolean=} objectBound */ -function lazy(obj, name, initializer, descriptor) { +function lazy(obj, name, initializer, descriptor, objectBound = true) { if (typeof initializer !== 'function') { - throw new TypeError('initialize is not callable'); + throw new TypeError('initializer is not callable'); } - // Establish the descriptor for the eventually-initialized property. - const finalDescriptor = descriptorFromDefault(defaultDataDescriptor, descriptor); + // The descriptor for the eventually-initialized property. + const finalDescriptor = createAssign(defaultDataDescriptor, descriptor); // The descriptor defining the interim accessor property. - const lazyDescriptor = descriptorFromDefault(defaultAccessorDescriptor, { + const lazyDescriptor = createAssign(defaultAccessorDescriptor, { // The accessor replaces itself with the value returned by invoking the initializer. get: function () { - finalDescriptor.value = initializer.apply(obj); - Object.defineProperty(obj, name, finalDescriptor); + const targetObject = objectBound ? obj : this; + finalDescriptor.value = initializer.apply(targetObject); + Object.defineProperty(targetObject, name, finalDescriptor); return finalDescriptor.value; }, - // An explicit set overrides the lazy descriptor with a default data property. - set: function (value) { - Object.defineProperty(obj, name, descriptorFromDefault(defaultDataDescriptor, { - value, - })) - }, + /** + * Unless a non-writable descriptor was specified, an explicit set will overwrite + * the lazy descriptor with a default data property, + */ + ...(finalDescriptor.writable && { + set: function (value) { + Object.defineProperty(this, name, createAssign(defaultDataDescriptor, { + value, + })) + }, + }), configurable: true, // Ensure we can replace ourself. enumerable: finalDescriptor.enumerable, // Use requested visibility. }); - Object.defineProperty(obj, name, lazyDescriptor); + return Object.defineProperty(obj, name, lazyDescriptor); } module.exports = {