bump package version to 1.1.2
[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 {*} proto
11 * @param {...any} objs
12 * @returns {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 * Defer calling initializer to set value for name until name is read.
46 * If a lazy property is defined on an object which is used as a prototype,
47 * the objectBound flag determines whether the lazy initializer will be
48 * invoked just once and set the property value on the original prototype
49 * object, or for any inherited object and set the property on that object.
50 * The objectBound flag also controls the 'this' object of the initializer
51 * when called.
52 * @param {Object} obj
53 * @param {String} name
54 * @param {() => {*}} initializer
55 * @param {Object=} descriptor
56 * @param {Boolean=} descriptor.configurable
57 * @param {Boolean=} descriptor.enumerable
58 * @param {Boolean=} descriptor.writable
59 * @param {Boolean=} objectBound
60 */
61 function lazy(obj, name, initializer, descriptor, objectBound = true) {
62 if (typeof initializer !== 'function') {
63 throw new TypeError('initializer is not callable');
64 }
65
66 // The descriptor for the eventually-initialized property.
67 const finalDescriptor = createAssign(defaultDataDescriptor, descriptor);
68
69 // The descriptor defining the interim accessor property.
70 const lazyDescriptor = createAssign(defaultAccessorDescriptor, {
71 // The accessor replaces itself with the value returned by invoking the initializer.
72 get: function () {
73 const targetObject = objectBound ? obj : this;
74 finalDescriptor.value = initializer.apply(targetObject);
75 Object.defineProperty(targetObject, name, finalDescriptor);
76 return finalDescriptor.value;
77 },
78
79 /**
80 * Unless a non-writable descriptor was specified, an explicit set will overwrite
81 * the lazy descriptor with a default data property,
82 */
83 ...(finalDescriptor.writable && {
84 set: function (value) {
85 Object.defineProperty(this, name, createAssign(defaultDataDescriptor, {
86 value,
87 }))
88 },
89 }),
90
91 configurable: true, // Ensure we can replace ourself.
92 enumerable: finalDescriptor.enumerable, // Use requested visibility.
93 });
94
95 return Object.defineProperty(obj, name, lazyDescriptor);
96 }
97
98 module.exports = {
99 lazy,
100 };