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