X-Git-Url: https://git.ladys.computer/Pisces/blobdiff_plain/1f05716e1051f4f92528a755f1ec4cc8e8a62f78..50ab30fc3a257e46da6b8bdc0dad054f729e16e1:/object.js diff --git a/object.js b/object.js index d08f43a..aa3538d 100644 --- a/object.js +++ b/object.js @@ -1,25 +1,494 @@ // ♓🌟 Piscēs ∷ object.js // ==================================================================== // -// Copyright © 2022 Lady [@ Lady’s Computer]. +// Copyright © 2022–2023 Lady [@ Lady’s Computer]. // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at . -/** Returns whether the provided value is a constructor. */ -export const isConstructor = ($) => { - if (!isObject($)) { - // The provided value is not an object. +import { bind, call, createArrowFunction } from "./function.js"; +import { + IS_CONCAT_SPREADABLE, + ITERATOR, + SPECIES, + toFunctionName, + toLength, + toPrimitive, + type, +} from "./value.js"; + +/** + * An object whose properties are lazy‐loaded from the methods on the + * own properties of the provided object. + * + * This is useful when you are looking to reference properties on + * objects which, due to module dependency graphs, cannot be guaranteed + * to have been initialized yet. + * + * The resulting properties will have the same attributes (regarding + * configurability, enumerability, and writability) as the + * corresponding properties on the methods object. If a property is + * marked as writable, the method will never be called if it is set + * before it is gotten. By necessity, the resulting properties are all + * configurable before they are accessed for the first time. + * + * Methods will be called with the resulting object as their this + * value. + * + * `LazyLoader` objects have the same prototype as the passed methods + * object. + */ +export class LazyLoader extends null { + /** + * Constructs a new `LazyLoader` object. + * + * ☡ This function throws if the provided value is not an object. + */ + constructor(loadMethods) { + if (type(loadMethods) !== "object") { + // The provided value is not an object; throw an error. + throw new TypeError( + `Piscēs: Cannot construct LazyLoader: Provided argument is not an object: ${loadMethods}.`, + ); + } else { + // The provided value is an object; process it and build the + // result. + const result = objectCreate(getPrototype(loadMethods)); + const methodKeys = getOwnPropertyKeys(loadMethods); + for (let index = 0; index < methodKeys.length; ++index) { + // Iterate over the property keys of the provided object and + // define getters and setters appropriately on the result. + const methodKey = methodKeys[index]; + const { configurable, enumerable, writable } = + getOwnPropertyDescriptor(loadMethods, methodKey); + defineOwnProperty(result, methodKey, { + configurable: true, + enumerable, + get: defineOwnProperty( + () => { + const value = call(loadMethods[methodKey], result, []); + defineOwnProperty(result, methodKey, { + configurable, + enumerable, + value, + writable, + }); + return value; + }, + "name", + { value: toFunctionName(methodKey, "get") }, + ), + set: writable + ? defineOwnProperty( + ($) => + defineOwnProperty(result, methodKey, { + configurable, + enumerable, + value: $, + writable, + }), + "name", + { value: toFunctionName(methodKey, "set") }, + ) + : void {}, + }); + } + return result; + } + } +} + +/** + * Defines an own property on the provided object on the provided + * property key using the provided property descriptor. + * + * ※ This is effectively an alias for `Object.defineProperty`. + */ +export const defineOwnProperty = createArrowFunction( + Object.defineProperty, + { name: "defineOwnProperty" }, +); + +export const { + /** + * Defines own properties on the provided object using the + * descriptors on the enumerable own properties of the provided + * additional objects. + * + * ※ This differs from `Object.defineProperties` in that it can take + * multiple source objects. + */ + defineOwnProperties, + + /** + * Returns a new frozen shallow copy of the enumerable own properties + * of the provided object, according to the following rules :— + * + * - For data properties, create a nonconfigurable, nonwritable + * property with the same value. + * + * - For accessor properties, create a nonconfigurable accessor + * property with the same getter *and* setter. + * + * The prototype for the resulting object will be taken from the + * `.prototype` property of the provided constructor, or the + * `.prototype` of the `.constructor` of the provided object if the + * provided constructor is undefined. If the used constructor has a + * nonnullish `.[Symbol.species]`, that will be used instead. If the + * used constructor or species is nullish or does not have a + * `.prototype` property, the prototype is set to null. + * + * ※ The prototype of the provided object itself is ignored. + */ + frozenCopy, + + /** + * Returns whether the provided object is frozen. + * + * ※ This function returns false for nonobjects. + * + * ※ This is effectively an alias for `!Object.isFrozen`. + */ + isUnfrozenObject, + + /** + * Returns whether the provided object is sealed. + * + * ※ This function returns false for nonobjects. + * + * ※ This is effectively an alias for `!Object.isSealed`. + */ + isUnsealedObject, + + /** + * Sets the prototype of the provided object to the provided value + * and returns the object. + * + * ※ This is effectively an alias for `Object.setPrototypeOf`. + */ + setPrototype, + + /** + * Returns the provided value converted to an object. + * + * Existing objects are returned with no modification. + * + * ☡ This function throws if its argument is null or undefined. + */ + toObject, +} = (() => { + const createObject = Object; + const { + create, + defineProperties, + getPrototypeOf, + isFrozen, + isSealed, + setPrototypeOf, + } = Object; + const { + next: generatorIteratorNext, + } = getPrototypeOf(function* () {}.prototype); + const propertyDescriptorEntryIterablePrototype = { + [ITERATOR]() { + return { + next: bind(generatorIteratorNext, this.generator(), []), + }; + }, + }; + const propertyDescriptorEntryIterable = ($) => + create(propertyDescriptorEntryIterablePrototype, { + generator: { value: $ }, + }); + + return { + defineOwnProperties: (O, ...sources) => { + const { length } = sources; + for (let index = 0; index < length; ++index) { + defineProperties(O, sources[index]); + } + return O; + }, + frozenCopy: (O, constructor = O?.constructor) => { + if (O == null) { + // O is null or undefined. + throw new TypeError( + "Piscēs: Cannot copy properties of null or undefined.", + ); + } else { + // O is not null or undefined. + // + // (If not provided, the constructor will be the value of + // getting the `.constructor` property of O.) + const species = constructor?.[SPECIES] ?? constructor; + return preventExtensions( + objectCreate( + species == null || !("prototype" in species) + ? null + : species.prototype, + objectFromEntries( + propertyDescriptorEntryIterable(function* () { + const ownPropertyKeys = getOwnPropertyKeys(O); + for ( + let i = 0; + i < ownPropertyKeys.length; + ++i + ) { + const P = ownPropertyKeys[i]; + const Desc = getOwnPropertyDescriptor(O, P); + if (Desc.enumerable) { + // P is an enumerable property. + yield [ + P, + "get" in Desc || "set" in Desc + ? { + configurable: false, + enumerable: true, + get: Desc.get, + set: Desc.set, + } + : { + configurable: false, + enumerable: true, + value: Desc.value, + writable: false, + }, + ]; + } else { + // P is not an enumerable property. + /* do nothing */ + } + } + }), + ), + ), + ); + } + }, + isUnfrozenObject: (O) => !isFrozen(O), + isUnsealedObject: (O) => !isSealed(O), + setPrototype: (O, proto) => { + const obj = toObject(O); + if (O === obj) { + // The provided value is an object; set its prototype normally. + return setPrototypeOf(O, proto); + } else { + // The provided value is not an object; attempt to set the + // prototype on a coerced version with extensions prevented, + // then return the provided value. + // + // This will throw if the given prototype does not match the + // existing one on the coerced object. + setPrototypeOf(preventExtensions(obj), proto); + return O; + } + }, + toObject: ($) => { + if ($ == null) { + // The provided value is nullish; this is an error. + throw new TypeError( + `Piscēs: Cannot convert ${$} into an object.`, + ); + } else { + // The provided value is not nullish; coerce it to an object. + return createObject($); + } + }, + }; +})(); + +export const { + /** + * Removes the provided property key from the provided object and + * returns the object. + * + * ※ This function differs from `Reflect.deleteProperty` and the + * `delete` operator in that it throws if the deletion is + * unsuccessful. + * + * ☡ This function throws if the first argument is not an object. + */ + deleteOwnProperty, + + /** + * Returns an array of property keys on the provided object. + * + * ※ This is effectively an alias for `Reflect.ownKeys`, except that + * it does not require that the argument be an object. + */ + getOwnPropertyKeys, + + /** + * Returns the value of the provided property key on the provided + * object. + * + * ※ This is effectively an alias for `Reflect.get`, except that it + * does not require that the argument be an object. + */ + getPropertyValue, + + /** + * Returns whether the provided property key exists on the provided + * object. + * + * ※ This is effectively an alias for `Reflect.has`, except that it + * does not require that the argument be an object. + * + * ※ This includes properties present on the prototype chain. + */ + hasProperty, + + /** + * Sets the provided property key to the provided value on the + * provided object and returns the object. + * + * ※ This function differs from `Reflect.set` in that it throws if + * the setting is unsuccessful. + * + * ☡ This function throws if the first argument is not an object. + */ + setPropertyValue, +} = (() => { + const { deleteProperty, get, has, ownKeys, set } = Reflect; + + return { + deleteOwnProperty: (O, P) => { + if (type(O) !== "object") { + throw new TypeError( + `Piscēs: Tried to set property but provided value was not an object: ${V}`, + ); + } else if (!deleteProperty(O, P)) { + throw new TypeError( + `Piscēs: Tried to delete property from object but [[Delete]] returned false: ${P}`, + ); + } else { + return O; + } + }, + getOwnPropertyKeys: (O) => ownKeys(toObject(O)), + getPropertyValue: (O, P, Receiver = O) => + get(toObject(O), P, Receiver), + hasProperty: (O, P) => has(toObject(O), P), + setPropertyValue: (O, P, V, Receiver = O) => { + if (type(O) !== "object") { + throw new TypeError( + `Piscēs: Tried to set property but provided value was not an object: ${V}`, + ); + } else if (!set(O, P, V, Receiver)) { + throw new TypeError( + `Piscēs: Tried to set property on object but [[Set]] returned false: ${P}`, + ); + } else { + return O; + } + }, + }; +})(); + +/** + * Marks the provided object as non·extensible and marks all its + * properties as nonconfigurable and (if data properties) nonwritable, + * and returns the object. + * + * ※ This is effectively an alias for `Object.freeze`. + */ +export const freeze = createArrowFunction(Object.freeze); + +/** + * Returns the function on the provided value at the provided property + * key. + * + * ☡ This function throws if the provided property key does not have an + * associated value which is callable. + */ +export const getMethod = (V, P) => { + const func = getPropertyValue(V, P); + if (func == null) { + return undefined; + } else if (typeof func !== "function") { + throw new TypeError(`Piscēs: Method not callable: ${P}`); + } else { + return func; + } +}; + +/** + * Returns the property descriptor for the own property with the + * provided property key on the provided object, or null if none + * exists. + * + * ※ This is effectively an alias for + * `Object.getOwnPropertyDescriptor`. + */ +export const getOwnPropertyDescriptor = createArrowFunction( + Object.getOwnPropertyDescriptor, +); + +/** + * Returns the property descriptors for the own properties on the + * provided object. + * + * ※ This is effectively an alias for + * `Object.getOwnPropertyDescriptors`. + */ +export const getOwnPropertyDescriptors = createArrowFunction( + Object.getOwnPropertyDescriptors, +); + +/** + * Returns an array of string‐valued own property keys on the + * provided object. + * + * ☡ This includes both enumerable and non·enumerable properties. + * + * ※ This is effectively an alias for `Object.getOwnPropertyNames`. + */ +export const getOwnPropertyStrings = createArrowFunction( + Object.getOwnPropertyNames, + { name: "getOwnPropertyStrings" }, +); + +/** + * Returns an array of symbol‐valued own property keys on the + * provided object. + * + * ☡ This includes both enumerable and non·enumerable properties. + * + * ※ This is effectively an alias for + * `Object.getOwnPropertySymbols`. + */ +export const getOwnPropertySymbols = createArrowFunction( + Object.getOwnPropertySymbols, +); + +/** + * Returns the prototype of the provided object. + * + * ※ This is effectively an alias for `Object.getPrototypeOf`. + */ +export const getPrototype = createArrowFunction( + Object.getPrototypeOf, + { name: "getPrototype" }, +); + +/** + * Returns whether the provided object has an own property with the + * provided property key. + * + * ※ This is effectively an alias for `Object.hasOwn`. + */ +export const hasOwnProperty = createArrowFunction(Object.hasOwn, { + name: "hasOwnProperty", +}); + +/** Returns whether the provided value is an arraylike object. */ +export const isArraylikeObject = ($) => { + if (type($) !== "object") { return false; } else { - // The provided value is an object. try { - Reflect.construct( - function () {}, - [], - $, - ); // will throw if $ is not a constructor + lengthOfArraylike($); // throws if not arraylike return true; } catch { return false; @@ -27,98 +496,131 @@ export const isConstructor = ($) => { } }; -/** Returns whether the provided value is an object. */ -export const isObject = ($) => { - return $ !== null && - (typeof $ == "function" || typeof $ == "object"); -}; +export const { + /** + * Returns whether the provided value is spreadable during array + * concatenation. + * + * This is also used to determine which things should be treated as + * collections. + */ + isConcatSpreadableObject, +} = (() => { + const { isArray } = Array; + + return { + isConcatSpreadableObject: ($) => { + if (type($) !== "object") { + // The provided value is not an object. + return false; + } else { + // The provided value is an object. + const spreadable = $[IS_CONCAT_SPREADABLE]; + return spreadable !== undefined ? !!spreadable : isArray($); + } + }, + }; +})(); /** - * Returns whether the provided object inherits from the prototype of - * the provided function. + * Returns whether the provided object is extensible. + * + * ※ This function returns false for nonobjects. + * + * ※ This is effectively an alias for `Object.isExtensible`. */ -export const ordinaryHasInstance = Function.prototype.call.bind( - Function.prototype[Symbol.hasInstance], +export const isExtensibleObject = createArrowFunction( + Object.isExtensible, + { name: "isExtensibleObject" }, ); /** - * Returns the primitive value of the provided object per its - * `toString` and `valueOf` methods. - * - * If the provided hint is "string", then `toString` takes precedence; - * otherwise, `valueOf` does. - * - * Throws an error if both of these methods are not callable or do not - * return a primitive. - */ -export const ordinaryToPrimitive = (O, hint) => { - for ( - const name of hint == "string" - ? ["toString", "valueOf"] - : ["valueOf", "toString"] - ) { - const method = O[name]; - if (typeof method == "function") { - // Method is callable. - const result = method.call(O); - if (!isObject(result)) { - // Method returns a primitive. - return result; - } else { - // Method returns an object. - continue; - } - } else { - // Method is not callable. - continue; - } - } - throw new TypeError("Piscēs: Unable to convert object to primitive"); -}; + * Returns the length of the provided arraylike value. + * + * This can produce larger lengths than can actually be stored in + * arrays, because no such restrictions exist on arraylike methods. + * + * ☡ This function throws if the provided value is not arraylike. + */ +export const lengthOfArraylike = ({ length }) => toLength(length); /** - * Returns the provided value converted to a primitive, or throws if - * no such conversion is possible. + * Returns an array of key~value pairs for the enumerable, + * string‐valued property keys on the provided object. * - * The provided preferred type, if specified, should be "string", - * "number", or "default". If the provided input has a - * `Symbol.toPrimitive` method, this function will throw rather than - * calling that method with a preferred type other than one of the - * above. + * ※ This is effectively an alias for `Object.entries`. */ -export const toPrimitive = ($, preferredType) => { - if (isObject($)) { - // The provided value is an object. - const exoticToPrim = $[Symbol.toPrimitive] ?? undefined; - if (exoticToPrim !== undefined) { - // The provided value has an exotic primitive conversion method. - if (typeof exoticToPrim != "function") { - // The method is not callable. - throw new TypeError( - "Piscēs: Symbol.toPrimitive was neither nullish nor callable.", - ); - } else { - // The method is callable. - const hint = `${preferredType ?? "default"}`; - if (!["default", "string", "number"].includes(hint)) { - // An invalid preferred type was specified. - throw new TypeError( - `Piscēs: Invalid preferred type: ${preferredType}.`, - ); - } else { - // The resulting hint is either default, string, or number. - return exoticToPrim.call($, hint); - } - } - } else { - // Use the ordinary primitive conversion function. - ordinaryToPrimitive($, hint); - } - } else { - // The provided value is already a primitive. - return $; - } -}; +export const namedEntries = createArrowFunction(Object.entries, { + name: "namedEntries", +}); + +/** + * Returns an array of the enumerable, string‐valued property keys on + * the provided object. + * + * ※ This is effectively an alias for `Object.keys`. + */ +export const namedKeys = createArrowFunction(Object.keys, { + name: "namedKeys", +}); + +/** + * Returns an array of property values for the enumerable, + * string‐valued property keys on the provided object. + * + * ※ This is effectively an alias for `Object.values`. + */ +export const namedValues = createArrowFunction(Object.values, { + name: "namedValues", +}); + +/** + * Returns a new object with the provided prototype and property + * descriptors. + * + * ※ This is effectively an alias for `Object.create`. + */ +export const objectCreate = createArrowFunction(Object.create, { + name: "objectCreate", +}); + +/** + * Returns a new object with the provided property keys and values. + * + * ※ This is effectively an alias for `Object.fromEntries`. + */ +export const objectFromEntries = createArrowFunction( + Object.fromEntries, + { name: "objectFromEntries" }, +); + +/** + * Marks the provided object as non·extensible, and returns the + * object. + * + * ※ This is effectively an alias for `Object.preventExtensions`. + */ +export const preventExtensions = createArrowFunction( + Object.preventExtensions, +); + +/** + * Marks the provided object as non·extensible and marks all its + * properties as nonconfigurable, and returns the object. + * + * ※ This is effectively an alias for `Object.seal`. + */ +export const seal = createArrowFunction(Object.seal); + +/** + * Sets the values of the enumerable own properties of the provided + * additional objects on the provided object. + * + * ※ This is effectively an alias for `Object.assign`. + */ +export const setPropertyValues = createArrowFunction(Object.assign, { + name: "setPropertyValues", +}); /** * Returns the property key (symbol or string) corresponding to the @@ -126,5 +628,5 @@ export const toPrimitive = ($, preferredType) => { */ export const toPropertyKey = ($) => { const key = toPrimitive($, "string"); - return typeof key == "symbol" ? key : `${key}`; + return typeof key === "symbol" ? key : `${key}`; };