@lumjs/core 1.2.1 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,6 +6,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [1.3.0] - 2022-09-11
10
+ ### Changed
11
+ - Overhauled the `obj.clone()` method.
12
+ - Added *named options* for all of the behaviours that the `CLONE` *modes* affect.
13
+ While this technically could make the *modes* obsolete, I'm leaving them in as
14
+ simple sets of default options that can be used instead of manually choosing all
15
+ of the individual options.
16
+ - Added a new `CLONE.N` *mode* which has **no** options turned on.
17
+ It will always remain first in the *enum* list so it's value will always be `0`.
18
+ - Added the ability to clone properties using their descriptors.
19
+ It is enabled by default on most of the *modes* now, as it simply makes sense.
20
+ - Added the ability to set the `prototype` on the cloned object.
21
+ - The `opts` parameter of `clone()` may be a `CLONE.*` enum value.
22
+ It's a shortcut for `{mode: CLONE.MODE}` for convenience.
23
+ - A small tweak to `obj.copyProps()`, not important, just a cleanup.
24
+ ### Fixed
25
+ - A reference in the DocBlock for `obj.getProperty()`.
26
+
9
27
  ## [1.2.1] - 2022-09-05
10
28
  ### Added
11
29
  - `core.InternalObjectId#untag()`
@@ -46,7 +64,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
46
64
  - See [1.0-beta.md](1.0-beta.md) for the beta versions of `1.0`
47
65
  - See [lum.js](https://github.com/supernovus/lum.js) for the original library this is replacing.
48
66
 
49
- [Unreleased]: https://github.com/supernovus/lum.core.js/compare/v1.2.1...HEAD
67
+ [Unreleased]: https://github.com/supernovus/lum.core.js/compare/v1.3.0...HEAD
68
+ [1.3.0]: https://github.com/supernovus/lum.core.js/compare/v1.2.1...v1.3.0
50
69
  [1.2.1]: https://github.com/supernovus/lum.core.js/compare/v1.2.0...v1.2.1
51
70
  [1.2.0]: https://github.com/supernovus/lum.core.js/compare/v1.1.1...v1.2.0
52
71
  [1.1.1]: https://github.com/supernovus/lum.core.js/compare/v1.1.0...v1.1.1
package/lib/obj/clone.js CHANGED
@@ -1,27 +1,38 @@
1
1
  // Import *most* required bits here.
2
- const {B,N,F, isObj, isComplex, def} = require('../types');
2
+ const {B,N,F, isObj, isComplex, def, isArray} = require('../types');
3
3
  const Enum = require('../enum');
4
4
  const copyProps = require('./copyprops');
5
+ const getProp = require('./getproperty');
5
6
 
6
7
  /**
7
- * An enum of supported modes for the `clone` method.
8
+ * An `enum` of supported *modes* for the `clone()` method.
8
9
  *
9
- * - **P** = All properties. If unchecked, enumerable properties only.
10
- * - **A** = Uses `Array.slice()` shortcut for shallow Array cloning.
11
- * - **R** = Recursive (deep) cloning of nested objects.
10
+ * - **P** All properties. If unchecked, *enumerable* properties only.
11
+ * - **A** Uses `Array.slice()` shortcut for shallow `Array` cloning.
12
+ * - **R** Recursive (deep) cloning of nested objects.
13
+ * - **D** → Uses *descriptor* cloning instead of direct assignment.
14
+ * - **T** → Sets the `prototype` of the clone as well.
12
15
  *
13
- * | Mode | P | A | R | Notes |
14
- * | ---- | - | - | - | ----- |
15
- * | `CLONE.DEF` | × | | × | Default mode for cloning functions. |
16
- * | `CLONE.DEEP` | × | × | | |
17
- * | `CLONE.FULL` | ✓ | ✓ | × | |
18
- * | `CLONE.ALL` | ✓ | × | × | |
19
- * | `CLONE.ENTIRE` | ✓ | × | ✓ | |
20
- * | `CLONE.JSON` | × | × | ✓ | Uses JSON, so no `function` or `symbol` support. |
16
+ * | Mode | P | A | R | D | T | Notes |
17
+ * | ---- | - | - | - | - | - | ----- |
18
+ * | `CLONE.N` | × | × | × | × | × | Can be used to manually specify options. |
19
+ * | `CLONE.DEF` | × | ✓ | × | × | × | Default mode for cloning functions. |
20
+ * | `CLONE.DEEP` | × | × | ✓ | ✓ | × | |
21
+ * | `CLONE.FULL` | ✓ | ✓ | × | ✓ | × | |
22
+ * | `CLONE.ALL` | ✓ | × | × | ✓ | × | |
23
+ * | `CLONE.ENTIRE` | | × | ✓ | | | |
24
+ * | `CLONE.JSON` | - | - | ✓ | - | × | Uses JSON, so no `function` or `symbol` support. |
25
+ *
26
+ * The `✓` and `×` marks signify the *default settings* in the mode.
27
+ * In *most* cases there are options that can override the
28
+ * defaults.
29
+ *
30
+ * Any feature in the `CLONE.JSON` row marked with `-` are
31
+ * incompatible with that mode and cannot be enabled at all.
21
32
  *
22
33
  * @alias module:@lumjs/core/obj.CLONE
23
34
  */
24
- const CLONE = Enum(['DEF','FULL','ALL','DEEP','ENTIRE','JSON']);
35
+ const CLONE = Enum(['N','DEF','FULL','ALL','DEEP','ENTIRE','JSON']);
25
36
 
26
37
  exports.CLONE = CLONE;
27
38
 
@@ -29,7 +40,7 @@ const copyProps = require('./copyprops');
29
40
  const SLICE_ARRAYS = [CLONE.DEF, CLONE.FULL];
30
41
 
31
42
  // A list of modes that should get *all* properties.
32
- const ALL_PROPS = [CLONE.FULL, CLONE.ALL, CLONE.DEEP];
43
+ const ALL_PROPS = [CLONE.FULL, CLONE.ALL, CLONE.ENTIRE];
33
44
 
34
45
  // A list of modes that should do recursive cloning of objects.
35
46
  const RECURSIVE = [CLONE.DEEP, CLONE.ENTIRE];
@@ -37,28 +48,103 @@ const copyProps = require('./copyprops');
37
48
  /**
38
49
  * Clone an object or function.
39
50
  *
40
- * @param {object|function} obj - The object we want to clone.
41
- * @param {object} [opts={}] - Options for the cloning process.
51
+ * @param {object} obj - The object we want to clone.
52
+ *
53
+ * @param {(object|number)} [opts={}] Options for the cloning process.
54
+ *
55
+ * If this is a `number` then it's assumed to be the `opts.mode` parameter.
56
+ *
57
+ * @param {number} [opts.mode=CLONE.DEF] One of the `CLONE.*` enum values.
58
+ *
59
+ * When the `clone()` method was written to replace some similar methods
60
+ * from earlier libraries, I for some reason decided to simply have a bunch
61
+ * of different cloning modes. I have since added a full set of options that
62
+ * allows overriding the options of any mode (except `CLONE.JSON`).
42
63
  *
43
- * @param {number} [opts.mode=CLONE.DEF] - One of the `CLONE.*` enum values.
64
+ * The `CLONE` enum is also aliased as `clone.MODE` as an alternative.
44
65
  *
45
- * Note: The `CLONE` enum is also aliased as `clone.MODE` as an alternative.
66
+ * @param {boolean} [opts.all] Clone **all** of the object's properties?
67
+ *
68
+ * If `false` only *enumerable* properties will be cloned.
69
+ *
70
+ * The default value depends on the `opts.mode` used.
71
+ *
72
+ * This is not used if `opts.mode` was `CLONE.JSON`.
46
73
  *
47
- * @param {boolean} [opts.addClone=false] - Call `addClone()` on the cloned object.
74
+ * @param {boolean} [opts.slice] Use the `Array.slice()` shortcut?
75
+ *
76
+ * If `true` then when cloning `Array` objects, a shallow clone will be
77
+ * created using the `Array.slice()` method.
78
+ *
79
+ * The default value depends on the `opts.mode` used.
80
+ *
81
+ * This is not used if `opts.mode` was `CLONE.JSON`.
82
+ *
83
+ * @param {boolean} [opts.recursive] Clone nested objects recursively?
84
+ *
85
+ * The default value depends on the `opts.mode` used.
86
+ * If `opts.slice` is also `true` then `Array` objects will
87
+ * be *shallow clones* while any other kind of object will be recursive.
88
+ *
89
+ * This is not used if `opts.mode` was `CLONE.JSON`.
90
+ *
91
+ * @param {boolean} [opts.descriptors] Clone using the property descriptors?
92
+ *
93
+ * If `true` we will get the property descriptors from the original object,
94
+ * and assign them to the clone.
95
+ * This is the only way to clone *accessor* type properties properly.
96
+ *
97
+ * If `false` we will directly assign the *property value* from the original
98
+ * object into the clone. This means the *current value* returned from an
99
+ * *accessor* type property will be assigned statically to the clone.
100
+ *
101
+ * The default value will be `true` if `opts.all` **OR** `opts.recursive`
102
+ * are `true`. It will be `false` otherwise.
103
+ *
104
+ * This is not used if `opts.mode` was `CLONE.JSON`.
105
+ *
106
+ * @param {boolean} [opts.prototype] Set the clone's `prototype`?
107
+ *
108
+ * If `true`, once the cloning is complete, we will call
109
+ * `Object.getPrototypeOf()` on the original `obj`, and then call
110
+ * `Object.setProrotypeOf()` on the clone.
111
+ *
112
+ * If `false` then the clone with have a prototype of `Object` or
113
+ * `Array` depending on whether the original object was an `Array`
114
+ * or not. No further prototype handling will be done.
115
+ *
116
+ * The default value will be `true` if `opts.all` **AND** `opts.recursive`
117
+ * are *both* `true`. Otherwise the default value is `false`.
118
+ * So for *modes*, only `CLONE.ENTIRE` uses this by default.
119
+ *
120
+ * @param {(object|boolean)} [opts.addClone=false]
121
+ * Call `addClone()` on the cloned object.
48
122
  *
49
- * The options sent to this function will be used as the defaults in
50
- * the `clone()` method added to the object.
123
+ * If `opts.addClone` is an `object` then it will be used as the options
124
+ * passed to the `addClone()` method. If it is `true` then the `opts`
125
+ * will be passed as is.
51
126
  *
52
- * @param {boolean} [opts.addLock=false] - Call `addLock()` on the cloned object.
127
+ * @param {(object|boolean)} [opts.addLock=false]
128
+ * Call `addLock()` on the cloned object.
53
129
  *
54
- * No further options for this, just add a `lock()` method to the clone.
130
+ * If `opts.addLock` is an `object` then it will be used as the options
131
+ * passed to the `addLock()` method. If it is `true` then the `opts`
132
+ * will be passed as is.
55
133
  *
56
- * @param {?object} [opts.copy] Call `copyProps()` on the cloned object.
134
+ * @param {(object|boolean)} [opts.copy=false]
135
+ * Call `copyProps()` on the cloned object.
57
136
  *
58
- * Will pass the original `obj` as the source to copy from.
59
- * Will pass `opts.copy` as the options.
137
+ * This is called *after* the normal cloning process, so only properties
138
+ * that weren't copied during the cloning process will be copied here.
139
+ * This is a leftover from when `CLONE.JSON` was the default cloning mode,
140
+ * and this was the only way to restore `function` properties.
141
+ *
142
+ * If `opts.copy` is an `object` then it will be used as the options
143
+ * passed to the `copyProps()` method. If it is `true` then the `opts`
144
+ * will be passed as is.
60
145
  *
61
- * @return {object} - The clone of the object.
146
+ * @return {object} The clone of the object.
147
+ *
62
148
  * @alias module:@lumjs/core/obj.clone
63
149
  */
64
150
  function clone(obj, opts={})
@@ -71,14 +157,38 @@ function clone(obj, opts={})
71
157
  return obj;
72
158
  }
73
159
 
74
- if (!isObj(opts))
160
+ if (typeof opts === N)
161
+ { // The 'mode' option.
162
+ opts = {mode: opts};
163
+ }
164
+ else if (!isObj(opts))
75
165
  { // Opts has to be a valid object.
76
166
  opts = {};
77
167
  }
78
168
 
79
- const mode = typeof opts.mode === N ? opts.mode : CLONE.DEF;
80
- const reclone = typeof opts.addClone === B ? opts.addClone : false;
81
- const relock = typeof opts.addLock === B ? opts.addLock : false;
169
+ // The mode is the base option.
170
+ const mode = typeof opts.mode === N ? opts.mode : CLONE.DEF;
171
+
172
+ // Options with defaults based on the mode.
173
+ const allProps = opts.all ?? ALL_PROPS.includes(mode);
174
+ const useSlice = opts.slice ?? SLICE_ARRAYS.includes(mode);
175
+ const recursive = opts.recursive ?? RECURSIVE.includes(mode);
176
+
177
+ // Some that depend on the values of the above options.
178
+ const descriptors = opts.descriptors ?? (allProps || recursive);
179
+ const withProto = opts.prototype ?? (allProps && recursive);
180
+
181
+ // Finally, a few that can be boolean or objects.
182
+ const subOpts = opt =>
183
+ {
184
+ if (isObj(opts[opt]))
185
+ return opts[opt];
186
+ else if (opts[opt] === true)
187
+ return opts;
188
+ }
189
+ const reclone = subOpts('addClone');
190
+ const relock = subOpts('addLock');
191
+ const cpProps = subOpts('copy');
82
192
 
83
193
  let copy;
84
194
 
@@ -89,7 +199,7 @@ function clone(obj, opts={})
89
199
  //console.debug("::clone using JSON cloning");
90
200
  copy = JSON.parse(JSON.stringify(obj));
91
201
  }
92
- else if (Array.isArray(obj) && SLICE_ARRAYS.includes(mode))
202
+ else if (Array.isArray(obj) && useSlice)
93
203
  { // Make a shallow copy using slice.
94
204
  //console.debug("::clone using Array.slice()");
95
205
  copy = obj.slice();
@@ -97,10 +207,10 @@ function clone(obj, opts={})
97
207
  else
98
208
  { // Build a clone using a simple loop.
99
209
  //console.debug("::clone using simple loop");
100
- copy = {};
210
+ copy = isArray(obj) ? [] : {};
101
211
 
102
212
  let props;
103
- if (ALL_PROPS.includes(mode))
213
+ if (allProps)
104
214
  { // All object properties.
105
215
  //console.debug("::clone getting all properties");
106
216
  props = Object.getOwnPropertyNames(obj);
@@ -112,31 +222,74 @@ function clone(obj, opts={})
112
222
  }
113
223
 
114
224
  //console.debug("::clone[props]", props);
225
+
226
+ let recOpts;
227
+ if (recursive)
228
+ { // Recursive opts; skips addClone, addLock, and copy.
229
+ recOpts =
230
+ {
231
+ mode, recursive, descriptors,
232
+ all: allProps,
233
+ slice: useSlice,
234
+ prototype: withProto,
235
+ }
236
+ }
115
237
 
116
238
  for (const prop of props)
117
239
  {
118
- let val = obj[prop];
119
- if (isObj(val) && RECURSIVE.includes(mode))
120
- { // Deep cloning.
121
- val = clone(val, {mode});
240
+ if (descriptors)
241
+ { // Use descriptor assignment.
242
+ const objDesc = getProp(obj, prop);
243
+ if (isObj(objDesc))
244
+ { // Make a fast, shallow clone of the descriptor.
245
+ const cloneDesc = clone(objDesc);
246
+ if (isObj(objDesc.value) && recursive)
247
+ { // Recursively clone the value.
248
+ cloneDesc.value = clone(objDesc.value, recOpts);
249
+ }
250
+ def(copy, prop, cloneDesc);
251
+ }
252
+ else
253
+ {
254
+ console.error("getProperty failed", {obj, prop, props, opts});
255
+ }
122
256
  }
123
- copy[prop] = val;
257
+ else
258
+ { // Use direct assignment.
259
+ let val = obj[prop];
260
+ if (isObj(val) && recursive)
261
+ { // Deep cloning.
262
+ val = clone(val, recOpts);
263
+ }
264
+ copy[prop] = val;
265
+ }
266
+ } // for prop
267
+
268
+ } // simple loop cloning algorithm
269
+
270
+ if (withProto)
271
+ { // Copy the prototype if it is different.
272
+ const objProto = Object.getPrototypeOf(obj);
273
+ const copyProto = Object.getPrototypeOf(copy);
274
+ if (objProto && objProto !== copyProto)
275
+ { // Update the clone's prototype to match the original.
276
+ Object.setPrototypeOf(copy, objProto);
124
277
  }
125
278
  }
126
279
 
127
- if (reclone)
280
+ if (isObj(reclone))
128
281
  { // Add the clone() method to the clone, with the passed opts as defaults.
129
- addClone(copy, opts);
282
+ addClone(copy, reclone);
130
283
  }
131
284
 
132
- if (opts.copy)
133
- { // Pass the clone through the copyProps() function as well.
134
- copyProps(obj, copy, opts.copy);
285
+ if (isObj(relock))
286
+ { // Add the lock() method to the clone.
287
+ addLock(copy, relock);
135
288
  }
136
289
 
137
- if (relock)
138
- { // Add the lock() method to the clone.
139
- addLock(copy, opts);
290
+ if (isObj(cpProps))
291
+ { // Pass the clone through the copyProps() function as well.
292
+ copyProps(obj, copy, cpProps);
140
293
  }
141
294
 
142
295
  return copy;
@@ -27,6 +27,7 @@ const {B,isObj,isComplex,isArray,def: defProp} = require('../types');
27
27
  */
28
28
  function copyProps(source, target, propOpts)
29
29
  {
30
+ //console.debug("copyProps", source, target, propOpts);
30
31
  if (!isComplex(source) || !isComplex(target))
31
32
  {
32
33
  throw new TypeError("source and target both need to be objects");
@@ -66,9 +67,9 @@ function copyProps(source, target, propOpts)
66
67
  }
67
68
 
68
69
  // For each propDef found, add it to the target.
69
- for (let p = 0; p < propDefs.length; p++)
70
+ for (const prop of propDefs)
70
71
  {
71
- const prop = propDefs[p];
72
+ //console.debug(" @prop:", prop);
72
73
  if (exclude && exclude.indexOf(prop) !== -1)
73
74
  continue; // Excluded property.
74
75
 
@@ -82,7 +83,8 @@ function copyProps(source, target, propOpts)
82
83
  overwrite = defOverwrite[prop];
83
84
  }
84
85
 
85
- const def = Object.getOwnPropertyDescriptor(source, prop)
86
+ const def = Object.getOwnPropertyDescriptor(source, prop);
87
+ //console.debug(" @desc:", def);
86
88
  if (def === undefined) continue; // Invalid property.
87
89
 
88
90
  if (isObj(defOverrides[prop]))
@@ -93,12 +95,15 @@ function copyProps(source, target, propOpts)
93
95
  def[key] = val;
94
96
  }
95
97
  }
98
+
96
99
  if (overwrite || target[prop] === undefined)
97
100
  { // Property doesn't already exist, let's add it.
98
101
  defProp(target, prop, def);
99
102
  }
100
103
  }
101
104
 
105
+ //console.debug("copyProps:done", target);
106
+
102
107
  return target;
103
108
  } // copyProps()
104
109
 
@@ -12,7 +12,8 @@ const {isComplex,isObj} = types;
12
12
  * @param {string} prop - Name of the property we want the descriptor of.
13
13
  * @param {mixed} [defval] The fallback value if no descriptor is found.
14
14
  *
15
- * @returns {mixed} - The descriptor if found, `defval` if not.
15
+ * @returns {mixed} The descriptor if found, `defval` if not.
16
+ * @alias module:@lumjs/core/obj.getProperty
16
17
  */
17
18
  function getProperty(obj, prop, defval)
18
19
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumjs/core",
3
- "version": "1.2.1",
3
+ "version": "1.3.0",
4
4
  "main": "lib/index.js",
5
5
  "exports":
6
6
  {