1.1.3
[squeep-lazy-property] / index.js
1 'use strict';
2
3 /**
4 * Lazy Properties
5 */
6
7
8 /**
9 * Shorthand helper for building descriptors up from a prototype chain.
10 * @param {object} proto prototype object
11 * @param {object[]} objs objects to assign to derived object, in order
12 * @returns {object} new object
13 */
14 function createAssign(proto, ...objs) {
15 return Object.assign(Object.create(proto), ...objs);
16 }
17
18 /**
19 * Defaults common to both data and accessor descriptors.
20 * Null prototype.
21 */
22 const defaultCommonDescriptor = createAssign(null, {
23 configurable: true,
24 enumerable: true,
25 });
26
27 /**
28 * Defaults for an eventually-initialized property.
29 * Same as if a property was set normally.
30 */
31 const defaultDataDescriptor = createAssign(defaultCommonDescriptor, {
32 writable: true,
33 value: undefined,
34 });
35
36 /**
37 * Defaults for an interim accessor property.
38 */
39 const defaultAccessorDescriptor = createAssign(defaultCommonDescriptor, {
40 get: undefined,
41 set: undefined,
42 });
43
44 /**
45 * @typedef {object} PropertyDescriptor
46 * @property {boolean=} configurable some things can be changed
47 * @property {boolean=} enumerable shows up in places
48 * @property {boolean=} writable value can be changed
49 * @property {any=} value value
50 * @property {Function=} get getter
51 * @property {Function=} set setter
52 */
53
54 /**
55 * Defer calling initializer to set value for name until name is read.
56 * If a lazy property is defined on an object which is used as a prototype,
57 * the objectBound flag determines whether the lazy initializer will be
58 * invoked just once and set the property value on the original prototype
59 * object, or for any inherited object and set the property on that object.
60 * The objectBound flag also controls the 'this' object of the initializer
61 * when called.
62 * @param {object} obj object
63 * @param {string} name property name
64 * @param {Function} initializer function which returns value to set on property upon first access
65 * @param {PropertyDescriptor=} descriptor optional descriptor
66 * @param {boolean=} objectBound bind initializer to obj
67 * @returns {object} obj with lazy property
68 */
69 function lazy(obj, name, initializer, descriptor, objectBound = true) {
70 if (typeof initializer !== 'function') {
71 throw new TypeError('initializer is not callable');
72 }
73
74 // The descriptor for the eventually-initialized property.
75 const finalDescriptor = createAssign(defaultDataDescriptor, descriptor);
76
77 // The descriptor defining the interim accessor property.
78 const lazyDescriptor = createAssign(defaultAccessorDescriptor, {
79 // The accessor replaces itself with the value returned by invoking the initializer.
80 get: function () {
81 const targetObject = objectBound ? obj : this;
82 finalDescriptor.value = initializer.apply(targetObject);
83 Object.defineProperty(targetObject, name, finalDescriptor);
84 return finalDescriptor.value;
85 },
86
87 /**
88 * Unless a non-writable descriptor was specified, an explicit set will overwrite
89 * the lazy descriptor with a default data property,
90 */
91 ...(finalDescriptor.writable && {
92 set: function (value) {
93 Object.defineProperty(this, name, createAssign(defaultDataDescriptor, {
94 value,
95 }));
96 },
97 }),
98
99 configurable: true, // Ensure we can replace ourself.
100 enumerable: finalDescriptor.enumerable, // Use requested visibility.
101 });
102
103 return Object.defineProperty(obj, name, lazyDescriptor);
104 }
105
106 module.exports = {
107 lazy,
108 };