'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 {object} proto prototype object
+ * @param {object[]} objs objects to assign to derived object, in order
+ * @returns {object} new 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}
+ * @typedef {object} PropertyDescriptor
+ * @property {boolean=} configurable some things can be changed
+ * @property {boolean=} enumerable shows up in places
+ * @property {boolean=} writable value can be changed
+ * @property {any=} value value
+ * @property {Function=} get getter
+ * @property {Function=} set setter
*/
-function descriptorFromDefault(defaultDescriptor, ...objs) {
- const newObject = Object.create(defaultDataDescriptor);
- Object.assign(newObject, ...objs);
- return Object.assign(Object.create(defaultDescriptor), ...objs);
-}
/**
* Defer calling initializer to set value for name until name is read.
- * @param {Object} obj
- * @param {String} name
- * @param {() => {*}} initializer
- * @param {Object=} descriptor
- * @param {Boolean} descriptor.configurable
- * @param {Boolean} descriptor.enumerable
- * @param {Boolean} descriptor.writable
+ * 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 object
+ * @param {string} name property name
+ * @param {Function} initializer function which returns value to set on property upon first access
+ * @param {PropertyDescriptor=} descriptor optional descriptor
+ * @param {boolean=} objectBound bind initializer to obj
+ * @returns {object} obj with lazy property
*/
-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 = {