]> Lady’s Gitweb - Pisces/blob - object.js
aa94bc571c469f3125b4d701dc07bacd6220ccdb
[Pisces] / object.js
1 // ♓🌟 Piscēs ∷ object.js
2 // ====================================================================
3 //
4 // Copyright © 2022–2023 Lady [@ Lady’s Computer].
5 //
6 // This Source Code Form is subject to the terms of the Mozilla Public
7 // License, v. 2.0. If a copy of the MPL was not distributed with this
8 // file, You can obtain one at <https://mozilla.org/MPL/2.0/>.
9
10 import {
11 IS_CONCAT_SPREADABLE,
12 isAccessorDescriptor,
13 SPECIES,
14 toFunctionName,
15 toLength,
16 toPrimitive,
17 toPropertyDescriptor,
18 type,
19 UNDEFINED,
20 } from "./value.js";
21
22 const { isArray } = Array;
23 const object = Object;
24 const {
25 assign,
26 create,
27 defineProperty,
28 defineProperties,
29 entries,
30 freeze: objectFreeze,
31 fromEntries,
32 getOwnPropertyDescriptor: objectGetOwnPropertyDescriptor,
33 getOwnPropertyNames,
34 getOwnPropertySymbols: objectGetOwnPropertySymbols,
35 getPrototypeOf,
36 hasOwn,
37 isExtensible,
38 isFrozen,
39 isSealed,
40 keys,
41 preventExtensions: objectPreventExtensions,
42 seal: objectSeal,
43 setPrototypeOf,
44 values,
45 } = Object;
46 const {
47 apply: call,
48 deleteProperty,
49 get,
50 getOwnPropertyDescriptor: reflectGetOwnPropertyDescriptor,
51 has,
52 ownKeys,
53 set,
54 } = Reflect;
55
56 /**
57 * An object whose properties are lazy‐loaded from the methods on the
58 * own properties of the provided object.
59 *
60 * This is useful when you are looking to reference properties on
61 * objects which, due to module dependency graphs, cannot be guaranteed
62 * to have been initialized yet.
63 *
64 * The resulting properties will have the same attributes (regarding
65 * configurability, enumerability, and writability) as the
66 * corresponding properties on the methods object. If a property is
67 * marked as writable, the method will never be called if it is set
68 * before it is gotten. By necessity, the resulting properties are all
69 * configurable before they are accessed for the first time.
70 *
71 * Methods will be called with the resulting object as their this
72 * value.
73 *
74 * `LazyLoader` objects have the same prototype as the passed methods
75 * object.
76 */
77 export class LazyLoader extends null {
78 /**
79 * Constructs a new `LazyLoader` object.
80 *
81 * ☡ This function throws if the provided value is not an object.
82 */
83 constructor(loadMethods) {
84 if (type(loadMethods) !== "object") {
85 // The provided value is not an object; throw an error.
86 throw new TypeError(
87 `Piscēs: Cannot construct LazyLoader: Provided argument is not an object: ${loadMethods}.`,
88 );
89 } else {
90 // The provided value is an object; process it and build the
91 // result.
92 const result = create(getPrototypeOf(loadMethods));
93 const methodKeys = ownKeys(loadMethods);
94 for (let index = 0; index < methodKeys.length; ++index) {
95 // Iterate over the property keys of the provided object and
96 // define getters and setters appropriately on the result.
97 const methodKey = methodKeys[index];
98 const { configurable, enumerable, writable } =
99 getOwnPropertyDescriptor(loadMethods, methodKey);
100 defineProperty(
101 result,
102 methodKey,
103 assign(create(null), {
104 configurable: true,
105 enumerable,
106 get: defineProperty(
107 () => {
108 const value = call(loadMethods[methodKey], result, []);
109 defineProperty(
110 result,
111 methodKey,
112 assign(create(null), {
113 configurable,
114 enumerable,
115 value,
116 writable,
117 }),
118 );
119 return value;
120 },
121 "name",
122 assign(create(null), {
123 value: toFunctionName(methodKey, "get"),
124 }),
125 ),
126 set: writable
127 ? defineProperty(
128 ($) =>
129 defineProperty(
130 result,
131 methodKey,
132 assign(create(null), {
133 configurable,
134 enumerable,
135 value: $,
136 writable,
137 }),
138 ),
139 "name",
140 assign(create(null), {
141 value: toFunctionName(methodKey, "set"),
142 }),
143 )
144 : UNDEFINED,
145 }),
146 );
147 }
148 return result;
149 }
150 }
151 }
152
153 /**
154 * Defines an own enumerable data property on the provided object with
155 * the provided property key and value.
156 */
157 export const defineOwnDataProperty = (O, P, V) =>
158 defineProperty(
159 O,
160 P,
161 assign(create(null), {
162 configurable: true,
163 enumerable: true,
164 value: V,
165 writable: true,
166 }),
167 );
168
169 /**
170 * Defines an own nonenumerable data property on the provided object
171 * with the provided property key and value.
172 */
173 export const defineOwnNonenumerableDataProperty = (O, P, V) =>
174 defineProperty(
175 O,
176 P,
177 assign(create(null), {
178 configurable: true,
179 enumerable: false,
180 value: V,
181 writable: true,
182 }),
183 );
184
185 /**
186 * Defines an own property on the provided object on the provided
187 * property key using the provided property descriptor.
188 *
189 * ※ This is effectively an alias for `Object.defineProperty`.
190 */
191 export const defineOwnProperty = (O, P, Desc) =>
192 defineProperty(O, P, Desc);
193
194 /**
195 * Defines own properties on the provided object using the descriptors
196 * on the enumerable own properties of the provided additional objects.
197 *
198 * ※ This differs from `Object.defineProperties` in that it can take
199 * multiple source objects.
200 */
201 export const defineOwnProperties = (O, ...sources) => {
202 const { length } = sources;
203 for (let k = 0; k < length; ++k) {
204 defineProperties(O, sources[k]);
205 }
206 return O;
207 };
208
209 /**
210 * Removes the provided property key from the provided object and
211 * returns the object.
212 *
213 * ※ This function differs from `Reflect.deleteProperty` and the
214 * `delete` operator in that it throws if the deletion is
215 * unsuccessful.
216 *
217 * ☡ This function throws if the first argument is not an object.
218 */
219 export const deleteOwnProperty = (O, P) => {
220 if (type(O) !== "object") {
221 throw new TypeError(
222 `Piscēs: Tried to set property but provided value was not an object: ${V}`,
223 );
224 } else if (!deleteProperty(O, P)) {
225 throw new TypeError(
226 `Piscēs: Tried to delete property from object but [[Delete]] returned false: ${P}`,
227 );
228 } else {
229 return O;
230 }
231 };
232
233 /**
234 * Marks the provided object as non·extensible and marks all its
235 * properties as nonconfigurable and (if data properties) nonwritable,
236 * and returns the object.
237 *
238 * ※ This is effectively an alias for `Object.freeze`.
239 */
240 export const freeze = (O) => objectFreeze(O);
241
242 /**
243 * Returns a new frozen shallow copy of the enumerable own properties
244 * of the provided object, according to the following rules :—
245 *
246 * - For data properties, create a nonconfigurable, nonwritable
247 * property with the same value.
248 *
249 * - For accessor properties, create a nonconfigurable accessor
250 * property with the same getter *and* setter.
251 *
252 * The prototype for the resulting object will be taken from the
253 * `.prototype` property of the provided constructor, or the
254 * `.prototype` of the `.constructor` of the provided object if the
255 * provided constructor is undefined. If the used constructor has a
256 * nonnullish `.[Symbol.species]`, that will be used instead. If the
257 * used constructor or species is nullish or does not have a
258 * `.prototype` property, the prototype is set to null.
259 *
260 * ※ The prototype of the provided object itself is ignored.
261 */
262 export const frozenCopy = (O, constructor = O?.constructor) => {
263 if (O == null) {
264 // O is null or undefined.
265 throw new TypeError(
266 "Piscēs: Cannot copy properties of null or undefined.",
267 );
268 } else {
269 // O is not null or undefined.
270 //
271 // (If not provided, the constructor will be the value of getting
272 // the `.constructor` property of O.)
273 const species = constructor?.[SPECIES] ?? constructor;
274 const copy = create(
275 species == null || !("prototype" in species)
276 ? null
277 : species.prototype,
278 );
279 const keys = ownKeys(O);
280 for (let k = 0; k < keys.length; ++k) {
281 const P = keys[k];
282 const Desc = getOwnPropertyDescriptor(O, P);
283 if (Desc.enumerable) {
284 // P is an enumerable property.
285 defineProperty(
286 copy,
287 P,
288 assign(
289 create(null),
290 isAccessorDescriptor(Desc)
291 ? {
292 configurable: false,
293 enumerable: true,
294 get: Desc.get,
295 set: Desc.set,
296 }
297 : {
298 configurable: false,
299 enumerable: true,
300 value: Desc.value,
301 writable: false,
302 },
303 ),
304 );
305 } else {
306 // P is not an enumerable property.
307 /* do nothing */
308 }
309 }
310 return objectPreventExtensions(copy);
311 }
312 };
313
314 /**
315 * Returns the function on the provided value at the provided property
316 * key.
317 *
318 * ☡ This function throws if the provided property key does not have an
319 * associated value which is callable.
320 */
321 export const getMethod = (V, P) => {
322 const func = V[P];
323 if (func == null) {
324 return undefined;
325 } else if (typeof func !== "function") {
326 throw new TypeError(`Piscēs: Method not callable: ${P}`);
327 } else {
328 return func;
329 }
330 };
331
332 /**
333 * Returns the property descriptor record for the own property with the
334 * provided property key on the provided value, or null if none exists.
335 *
336 * ※ This is effectively an alias for
337 * `Object.getOwnPropertyDescriptor`, but the return value is a proxied
338 * object with null prototype.
339 */
340 export const getOwnPropertyDescriptor = (O, P) => {
341 const desc = objectGetOwnPropertyDescriptor(O, P);
342 return desc === UNDEFINED ? UNDEFINED : toPropertyDescriptor(desc);
343 };
344
345 /**
346 * Returns the property descriptors for the own properties on the
347 * provided value.
348 *
349 * ※ This is effectively an alias for
350 * `Object.getOwnPropertyDescriptors`, but the values on the resulting
351 * object are proxied objects with null prototypes.
352 */
353 export const getOwnPropertyDescriptors = (O) => {
354 const obj = toObject(O);
355 const keys = ownKeys(obj);
356 const descriptors = {};
357 for (let k = 0; k < keys.length; ++k) {
358 const key = keys[k];
359 defineOwnDataProperty(
360 descriptors,
361 key,
362 getOwnPropertyDescriptor(O, key),
363 );
364 }
365 return descriptors;
366 };
367
368 /**
369 * Returns an array of property keys on the provided value.
370 *
371 * ※ This is effectively an alias for `Reflect.ownKeys`, except that
372 * it does not require that the argument be an object.
373 */
374 export const getOwnPropertyKeys = (O) => ownKeys(toObject(O));
375
376 /**
377 * Returns an array of string‐valued own property keys on the
378 * provided value.
379 *
380 * ☡ This includes both enumerable and non·enumerable properties.
381 *
382 * ※ This is effectively an alias for `Object.getOwnPropertyNames`.
383 */
384 export const getOwnPropertyStrings = (O) => getOwnPropertyNames(O);
385
386 /**
387 * Returns an array of symbol‐valued own property keys on the
388 * provided value.
389 *
390 * ☡ This includes both enumerable and non·enumerable properties.
391 *
392 * ※ This is effectively an alias for
393 * `Object.getOwnPropertySymbols`.
394 */
395 export const getOwnPropertySymbols = (O) =>
396 objectGetOwnPropertySymbols(O);
397
398 /**
399 * Returns the value of the provided property key on the provided
400 * value.
401 *
402 * ※ This is effectively an alias for `Reflect.get`, except that it
403 * does not require that the argument be an object.
404 */
405 export const getPropertyValue = (O, P, Receiver = O) =>
406 get(toObject(O), P, Receiver);
407
408 /**
409 * Returns the prototype of the provided value.
410 *
411 * ※ This is effectively an alias for `Object.getPrototypeOf`.
412 */
413 export const getPrototype = (O) => getPrototypeOf(O);
414
415 /**
416 * Returns whether the provided value has an own property with the
417 * provided property key.
418 *
419 * ※ This is effectively an alias for `Object.hasOwn`.
420 */
421 export const hasOwnProperty = (O, P) => hasOwn(O, P);
422
423 /**
424 * Returns whether the provided property key exists on the provided
425 * value.
426 *
427 * ※ This is effectively an alias for `Reflect.has`, except that it
428 * does not require that the argument be an object.
429 *
430 * ※ This includes properties present on the prototype chain.
431 */
432 export const hasProperty = (O, P) => has(toObject(O), P);
433
434 /** Returns whether the provided value is an arraylike object. */
435 export const isArraylikeObject = ($) => {
436 if (type($) !== "object") {
437 return false;
438 } else {
439 try {
440 lengthOfArraylike($); // throws if not arraylike
441 return true;
442 } catch {
443 return false;
444 }
445 }
446 };
447
448 /**
449 * Returns whether the provided value is spreadable during array
450 * concatenation.
451 *
452 * This is also used to determine which things should be treated as
453 * collections.
454 */
455 export const isConcatSpreadableObject = ($) => {
456 if (type($) !== "object") {
457 // The provided value is not an object.
458 return false;
459 } else {
460 // The provided value is an object.
461 const spreadable = $[IS_CONCAT_SPREADABLE];
462 return spreadable !== UNDEFINED ? !!spreadable : isArray($);
463 }
464 };
465
466 /**
467 * Returns whether the provided value is an extensible object.
468 *
469 * ※ This function returns false for nonobjects.
470 *
471 * ※ This is effectively an alias for `Object.isExtensible`.
472 */
473 export const isExtensibleObject = (O) => isExtensible(O);
474
475 /**
476 * Returns whether the provided value is an unfrozen object.
477 *
478 * ※ This function returns false for nonobjects.
479 *
480 * ※ This is effectively an alias for `!Object.isFrozen`.
481 */
482 export const isUnfrozenObject = (O) => !isFrozen(O);
483
484 /**
485 * Returns whether the provided value is an unsealed object.
486 *
487 * ※ This function returns false for nonobjects.
488 *
489 * ※ This is effectively an alias for `!Object.isSealed`.
490 */
491 export const isUnsealedObject = (O) => !isSealed(O);
492
493 /**
494 * Returns the length of the provided arraylike value.
495 *
496 * This can produce larger lengths than can actually be stored in
497 * arrays, because no such restrictions exist on arraylike methods.
498 *
499 * ☡ This function throws if the provided value is not arraylike.
500 */
501 export const lengthOfArraylike = ({ length }) => toLength(length);
502
503 /**
504 * Returns an array of key~value pairs for the enumerable,
505 * string‐valued property keys on the provided value.
506 *
507 * ※ This is effectively an alias for `Object.entries`.
508 */
509 export const namedEntries = (O) => entries(O);
510
511 /**
512 * Returns an array of the enumerable, string‐valued property keys on
513 * the provided value.
514 *
515 * ※ This is effectively an alias for `Object.keys`.
516 */
517 export const namedKeys = (O) => keys(O);
518
519 /**
520 * Returns an array of property values for the enumerable,
521 * string‐valued property keys on the provided value.
522 *
523 * ※ This is effectively an alias for `Object.values`.
524 */
525 export const namedValues = (O) => values(O);
526
527 /**
528 * Returns a new object with the provided prototype and property
529 * descriptors.
530 *
531 * ※ This is effectively an alias for `Object.create`.
532 */
533 export const objectCreate = (O, Properties) => create(O, Properties);
534
535 /**
536 * Returns a new object with property keys and values from the provided
537 * iterable value.
538 *
539 * ※ This is effectively an alias for `Object.fromEntries`.
540 */
541 export const objectFromEntries = (iterable) => fromEntries(iterable);
542
543 /**
544 * Marks the provided object as non·extensible, and returns the
545 * object.
546 *
547 * ※ This is effectively an alias for `Object.preventExtensions`.
548 */
549 export const preventExtensions = (O) => objectPreventExtensions(O);
550
551 /**
552 * Marks the provided object as non·extensible and marks all its
553 * properties as nonconfigurable, and returns the object.
554 *
555 * ※ This is effectively an alias for `Object.seal`.
556 */
557 export const seal = (O) => objectSeal(O);
558
559 /**
560 * Sets the provided property key to the provided value on the provided
561 * object and returns the object.
562 *
563 * ※ This function differs from `Reflect.set` in that it throws if the
564 * setting is unsuccessful.
565 *
566 * ☡ This function throws if the first argument is not an object.
567 */
568 export const setPropertyValue = (O, P, V, Receiver = O) => {
569 if (type(O) !== "object") {
570 throw new TypeError(
571 `Piscēs: Tried to set property but provided value was not an object: ${V}`,
572 );
573 } else if (!set(O, P, V, Receiver)) {
574 throw new TypeError(
575 `Piscēs: Tried to set property on object but [[Set]] returned false: ${P}`,
576 );
577 } else {
578 return O;
579 }
580 };
581
582 /**
583 * Sets the values of the enumerable own properties of the provided
584 * additional objects on the provided values.
585 *
586 * ※ This is effectively an alias for `Object.assign`.
587 */
588 export const setPropertyValues = (target, source, ...sources) => {
589 const to = toObject(target);
590 for (let i = -1; i < sources.length; ++i) {
591 // Iterate over each source and set the appropriate property
592 // values.
593 const nextSource = i === -1 ? source : sources[i];
594 if (nextSource != null) {
595 // The current source is not nullish; handle its own properties.
596 const from = toObject(nextSource);
597 const keys = ownKeys(from);
598 for (let k = 0; k < keys.length; ++k) {
599 // Iterate over each key in the current source and set it in
600 // the target object if it is enumerable.
601 const nextKey = keys[k];
602 const desc = reflectGetOwnPropertyDescriptor(from, nextKey);
603 if (desc !== UNDEFINED && desc.enumerable) {
604 // The current key is present and enumerable; set it to its
605 // corresponding value.
606 const propValue = from[nextKey];
607 to[nextKey] = propValue;
608 } else {
609 // The current key is not present or not enumerable.
610 /* do nothing */
611 }
612 }
613 } else {
614 // The current source is nullish.
615 /* do nothing */
616 }
617 }
618 return to;
619 };
620
621 /**
622 * Sets the prototype of the provided object to the provided value and
623 * returns the object.
624 *
625 * ※ This is effectively an alias for `Object.setPrototypeOf`, but it
626 * won’t throw when setting the prototype of a primitive to its current
627 * value.
628 */
629 export const setPrototype = (O, proto) => {
630 const obj = toObject(O);
631 if (O === obj) {
632 // The provided value is an object; set its prototype normally.
633 return setPrototypeOf(O, proto);
634 } else {
635 // The provided value is not an object; attempt to set the
636 // prototype on a coerced version with extensions prevented, then
637 // return the provided value.
638 //
639 // This will throw if the given prototype does not match the
640 // existing one on the coerced object.
641 setPrototypeOf(objectPreventExtensions(obj), proto);
642 return O;
643 }
644 };
645
646 /**
647 * Returns the provided value converted to an object.
648 *
649 * Existing objects are returned with no modification.
650 *
651 * ☡ This function throws if its argument is null or undefined.
652 */
653 export const toObject = ($) => {
654 if ($ == null) {
655 // The provided value is nullish; this is an error.
656 throw new TypeError(
657 `Piscēs: Cannot convert ${$} into an object.`,
658 );
659 } else {
660 // The provided value is not nullish; coerce it to an object.
661 return object($);
662 }
663 };
664
665 /**
666 * Returns the property key (symbol or string) corresponding to the
667 * provided value.
668 */
669 export const toPropertyKey = ($) => {
670 const key = toPrimitive($, "string");
671 return typeof key === "symbol" ? key : `${key}`;
672 };
This page took 0.172561 seconds and 3 git commands to generate.