cleanup and rearrangement
[squeep-lazy-property] / index.js
index 672e684b562d3ef8bb2d9574ca08dff18f4cac22..e30ed5b4c2edeed3f00d4f8f9de6b2f46d5a3912 100644 (file)
--- 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 = {