From: Lady Date: Fri, 24 Nov 2023 23:18:42 +0000 (-0500) Subject: Refactor object.js not to depend on function.js X-Git-Url: https://git.ladys.computer/Pisces/commitdiff_plain/f6b84c13c5c539dbac4f4b9b530eeec95d683475?ds=inline;hp=37155822c6ea043e92fe17fbb97086ad1785b89b Refactor object.js not to depend on function.js This unfortunately requires a literal re·implementation of `Object.assign`, but it’s otherwise worth it. This commit also adds `defineOwnDataProperty` and `defineOwnNonenumerableDataProperty` for convenience. --- diff --git a/object.js b/object.js index 9132e4b..aa94bc5 100644 --- a/object.js +++ b/object.js @@ -7,10 +7,9 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at . -import { bind, call, createArrowFunction } from "./function.js"; import { IS_CONCAT_SPREADABLE, - ITERATOR, + isAccessorDescriptor, SPECIES, toFunctionName, toLength, @@ -20,6 +19,40 @@ import { UNDEFINED, } from "./value.js"; +const { isArray } = Array; +const object = Object; +const { + assign, + create, + defineProperty, + defineProperties, + entries, + freeze: objectFreeze, + fromEntries, + getOwnPropertyDescriptor: objectGetOwnPropertyDescriptor, + getOwnPropertyNames, + getOwnPropertySymbols: objectGetOwnPropertySymbols, + getPrototypeOf, + hasOwn, + isExtensible, + isFrozen, + isSealed, + keys, + preventExtensions: objectPreventExtensions, + seal: objectSeal, + setPrototypeOf, + values, +} = Object; +const { + apply: call, + deleteProperty, + get, + getOwnPropertyDescriptor: reflectGetOwnPropertyDescriptor, + has, + ownKeys, + set, +} = Reflect; + /** * An object whose properties are lazy‐loaded from the methods on the * own properties of the provided object. @@ -56,380 +89,146 @@ export class LazyLoader extends null { } else { // The provided value is an object; process it and build the // result. - const result = objectCreate(getPrototype(loadMethods)); - const methodKeys = getOwnPropertyKeys(loadMethods); + const result = create(getPrototypeOf(loadMethods)); + const methodKeys = ownKeys(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, - }), + defineProperty( + result, + methodKey, + assign(create(null), { + configurable: true, + enumerable, + get: defineProperty( + () => { + const value = call(loadMethods[methodKey], result, []); + defineProperty( + result, + methodKey, + assign(create(null), { + configurable, + enumerable, + value, + writable, + }), + ); + return value; + }, "name", - { value: toFunctionName(methodKey, "set") }, - ) - : void {}, - }); + assign(create(null), { + value: toFunctionName(methodKey, "get"), + }), + ), + set: writable + ? defineProperty( + ($) => + defineProperty( + result, + methodKey, + assign(create(null), { + configurable, + enumerable, + value: $, + writable, + }), + ), + "name", + assign(create(null), { + value: toFunctionName(methodKey, "set"), + }), + ) + : UNDEFINED, + }), + ); } return result; } } } +/** + * Defines an own enumerable data property on the provided object with + * the provided property key and value. + */ +export const defineOwnDataProperty = (O, P, V) => + defineProperty( + O, + P, + assign(create(null), { + configurable: true, + enumerable: true, + value: V, + writable: true, + }), + ); + +/** + * Defines an own nonenumerable data property on the provided object + * with the provided property key and value. + */ +export const defineOwnNonenumerableDataProperty = (O, P, V) => + defineProperty( + O, + P, + assign(create(null), { + configurable: true, + enumerable: false, + value: V, + writable: true, + }), + ); + /** * 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 the property descriptor record 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`, but the return value is a - * proxied object with null prototype. - */ - getOwnPropertyDescriptor, - - /** - * Returns the property descriptors for the own properties on the - * provided object. - * - * ※ This is effectively an alias for - * `Object.getOwnPropertyDescriptors`, but the values on the - * resulting object are proxied objects with null prototypes. - */ - getOwnPropertyDescriptors, - - /** - * 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, +export const defineOwnProperty = (O, P, Desc) => + defineProperty(O, P, Desc); - /** - * 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, - getOwnPropertyDescriptor: objectGetOwnPropertyDescriptor, - 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 */ - } - } - }), - ), - ), - ); - } - }, - getOwnPropertyDescriptor: (O, P) => { - const desc = objectGetOwnPropertyDescriptor(O, P); - return desc === UNDEFINED - ? UNDEFINED - : toPropertyDescriptor(desc); - }, - getOwnPropertyDescriptors: (O) => { - const obj = toObject(O); - const ownKeys = getOwnPropertyKeys(obj); - const descriptors = {}; - for (let k = 0; k < ownKeys.length; ++k) { - const key = ownKeys[k]; - defineOwnProperty(descriptors, key, { - configurable: true, - enumerable: true, - value: getOwnPropertyDescriptor(O, key), - writable: true, - }); - } - return descriptors; - }, - 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, +/** + * 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. + */ +export const defineOwnProperties = (O, ...sources) => { + const { length } = sources; + for (let k = 0; k < length; ++k) { + defineProperties(O, sources[k]); + } + return O; +}; - /** - * 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; - } - }, - }; -})(); +/** + * 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. + */ +export const 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; + } +}; /** * Marks the provided object as non·extensible and marks all its @@ -438,7 +237,79 @@ export const { * * ※ This is effectively an alias for `Object.freeze`. */ -export const freeze = createArrowFunction(Object.freeze); +export const freeze = (O) => objectFreeze(O); + +/** + * 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. + */ +export const 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; + const copy = create( + species == null || !("prototype" in species) + ? null + : species.prototype, + ); + const keys = ownKeys(O); + for (let k = 0; k < keys.length; ++k) { + const P = keys[k]; + const Desc = getOwnPropertyDescriptor(O, P); + if (Desc.enumerable) { + // P is an enumerable property. + defineProperty( + copy, + P, + assign( + create(null), + isAccessorDescriptor(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 */ + } + } + return objectPreventExtensions(copy); + } +}; /** * Returns the function on the provided value at the provided property @@ -448,7 +319,7 @@ export const freeze = createArrowFunction(Object.freeze); * associated value which is callable. */ export const getMethod = (V, P) => { - const func = getPropertyValue(V, P); + const func = V[P]; if (func == null) { return undefined; } else if (typeof func !== "function") { @@ -458,51 +329,107 @@ export const getMethod = (V, P) => { } }; +/** + * Returns the property descriptor record for the own property with the + * provided property key on the provided value, or null if none exists. + * + * ※ This is effectively an alias for + * `Object.getOwnPropertyDescriptor`, but the return value is a proxied + * object with null prototype. + */ +export const getOwnPropertyDescriptor = (O, P) => { + const desc = objectGetOwnPropertyDescriptor(O, P); + return desc === UNDEFINED ? UNDEFINED : toPropertyDescriptor(desc); +}; + +/** + * Returns the property descriptors for the own properties on the + * provided value. + * + * ※ This is effectively an alias for + * `Object.getOwnPropertyDescriptors`, but the values on the resulting + * object are proxied objects with null prototypes. + */ +export const getOwnPropertyDescriptors = (O) => { + const obj = toObject(O); + const keys = ownKeys(obj); + const descriptors = {}; + for (let k = 0; k < keys.length; ++k) { + const key = keys[k]; + defineOwnDataProperty( + descriptors, + key, + getOwnPropertyDescriptor(O, key), + ); + } + return descriptors; +}; + +/** + * Returns an array of property keys on the provided value. + * + * ※ This is effectively an alias for `Reflect.ownKeys`, except that + * it does not require that the argument be an object. + */ +export const getOwnPropertyKeys = (O) => ownKeys(toObject(O)); + /** * Returns an array of string‐valued own property keys on the - * provided object. + * provided value. * * ☡ This includes both enumerable and non·enumerable properties. * * ※ This is effectively an alias for `Object.getOwnPropertyNames`. */ -export const getOwnPropertyStrings = createArrowFunction( - Object.getOwnPropertyNames, - { name: "getOwnPropertyStrings" }, -); +export const getOwnPropertyStrings = (O) => getOwnPropertyNames(O); /** * Returns an array of symbol‐valued own property keys on the - * provided object. + * provided value. * * ☡ This includes both enumerable and non·enumerable properties. * * ※ This is effectively an alias for * `Object.getOwnPropertySymbols`. */ -export const getOwnPropertySymbols = createArrowFunction( - Object.getOwnPropertySymbols, -); +export const getOwnPropertySymbols = (O) => + objectGetOwnPropertySymbols(O); + +/** + * Returns the value of the provided property key on the provided + * value. + * + * ※ This is effectively an alias for `Reflect.get`, except that it + * does not require that the argument be an object. + */ +export const getPropertyValue = (O, P, Receiver = O) => + get(toObject(O), P, Receiver); /** - * Returns the prototype of the provided object. + * Returns the prototype of the provided value. * * ※ This is effectively an alias for `Object.getPrototypeOf`. */ -export const getPrototype = createArrowFunction( - Object.getPrototypeOf, - { name: "getPrototype" }, -); +export const getPrototype = (O) => getPrototypeOf(O); /** - * Returns whether the provided object has an own property with the + * Returns whether the provided value 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", -}); +export const hasOwnProperty = (O, P) => hasOwn(O, P); + +/** + * Returns whether the provided property key exists on the provided + * value. + * + * ※ 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. + */ +export const hasProperty = (O, P) => has(toObject(O), P); /** Returns whether the provided value is an arraylike object. */ export const isArraylikeObject = ($) => { @@ -518,43 +445,50 @@ export const isArraylikeObject = ($) => { } }; -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 value is spreadable during array + * concatenation. + * + * This is also used to determine which things should be treated as + * collections. + */ +export const 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 is extensible. + * Returns whether the provided value is an extensible object. * * ※ This function returns false for nonobjects. * * ※ This is effectively an alias for `Object.isExtensible`. */ -export const isExtensibleObject = createArrowFunction( - Object.isExtensible, - { name: "isExtensibleObject" }, -); +export const isExtensibleObject = (O) => isExtensible(O); + +/** + * Returns whether the provided value is an unfrozen object. + * + * ※ This function returns false for nonobjects. + * + * ※ This is effectively an alias for `!Object.isFrozen`. + */ +export const isUnfrozenObject = (O) => !isFrozen(O); + +/** + * Returns whether the provided value is an unsealed object. + * + * ※ This function returns false for nonobjects. + * + * ※ This is effectively an alias for `!Object.isSealed`. + */ +export const isUnsealedObject = (O) => !isSealed(O); /** * Returns the length of the provided arraylike value. @@ -568,33 +502,27 @@ export const lengthOfArraylike = ({ length }) => toLength(length); /** * Returns an array of key~value pairs for the enumerable, - * string‐valued property keys on the provided object. + * string‐valued property keys on the provided value. * * ※ This is effectively an alias for `Object.entries`. */ -export const namedEntries = createArrowFunction(Object.entries, { - name: "namedEntries", -}); +export const namedEntries = (O) => entries(O); /** * Returns an array of the enumerable, string‐valued property keys on - * the provided object. + * the provided value. * * ※ This is effectively an alias for `Object.keys`. */ -export const namedKeys = createArrowFunction(Object.keys, { - name: "namedKeys", -}); +export const namedKeys = (O) => keys(O); /** * Returns an array of property values for the enumerable, - * string‐valued property keys on the provided object. + * string‐valued property keys on the provided value. * * ※ This is effectively an alias for `Object.values`. */ -export const namedValues = createArrowFunction(Object.values, { - name: "namedValues", -}); +export const namedValues = (O) => values(O); /** * Returns a new object with the provided prototype and property @@ -602,19 +530,15 @@ export const namedValues = createArrowFunction(Object.values, { * * ※ This is effectively an alias for `Object.create`. */ -export const objectCreate = createArrowFunction(Object.create, { - name: "objectCreate", -}); +export const objectCreate = (O, Properties) => create(O, Properties); /** - * Returns a new object with the provided property keys and values. + * Returns a new object with property keys and values from the provided + * iterable value. * * ※ This is effectively an alias for `Object.fromEntries`. */ -export const objectFromEntries = createArrowFunction( - Object.fromEntries, - { name: "objectFromEntries" }, -); +export const objectFromEntries = (iterable) => fromEntries(iterable); /** * Marks the provided object as non·extensible, and returns the @@ -622,9 +546,7 @@ export const objectFromEntries = createArrowFunction( * * ※ This is effectively an alias for `Object.preventExtensions`. */ -export const preventExtensions = createArrowFunction( - Object.preventExtensions, -); +export const preventExtensions = (O) => objectPreventExtensions(O); /** * Marks the provided object as non·extensible and marks all its @@ -632,17 +554,113 @@ export const preventExtensions = createArrowFunction( * * ※ This is effectively an alias for `Object.seal`. */ -export const seal = createArrowFunction(Object.seal); +export const seal = (O) => objectSeal(O); + +/** + * 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. + */ +export const 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; + } +}; /** * Sets the values of the enumerable own properties of the provided - * additional objects on the provided object. + * additional objects on the provided values. * * ※ This is effectively an alias for `Object.assign`. */ -export const setPropertyValues = createArrowFunction(Object.assign, { - name: "setPropertyValues", -}); +export const setPropertyValues = (target, source, ...sources) => { + const to = toObject(target); + for (let i = -1; i < sources.length; ++i) { + // Iterate over each source and set the appropriate property + // values. + const nextSource = i === -1 ? source : sources[i]; + if (nextSource != null) { + // The current source is not nullish; handle its own properties. + const from = toObject(nextSource); + const keys = ownKeys(from); + for (let k = 0; k < keys.length; ++k) { + // Iterate over each key in the current source and set it in + // the target object if it is enumerable. + const nextKey = keys[k]; + const desc = reflectGetOwnPropertyDescriptor(from, nextKey); + if (desc !== UNDEFINED && desc.enumerable) { + // The current key is present and enumerable; set it to its + // corresponding value. + const propValue = from[nextKey]; + to[nextKey] = propValue; + } else { + // The current key is not present or not enumerable. + /* do nothing */ + } + } + } else { + // The current source is nullish. + /* do nothing */ + } + } + return to; +}; + +/** + * Sets the prototype of the provided object to the provided value and + * returns the object. + * + * ※ This is effectively an alias for `Object.setPrototypeOf`, but it + * won’t throw when setting the prototype of a primitive to its current + * value. + */ +export const 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(objectPreventExtensions(obj), proto); + return O; + } +}; + +/** + * 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. + */ +export const 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 object($); + } +}; /** * Returns the property key (symbol or string) corresponding to the diff --git a/object.test.js b/object.test.js index 38d333f..3a0e1a7 100644 --- a/object.test.js +++ b/object.test.js @@ -19,6 +19,8 @@ import { spy, } from "./dev-deps.js"; import { + defineOwnDataProperty, + defineOwnNonenumerableDataProperty, defineOwnProperties, defineOwnProperty, deleteOwnProperty, @@ -309,11 +311,113 @@ describe("LazyLoader", () => { }); }); +describe("defineOwnDataProperty", () => { + it("[[Call]] defines the property", () => { + const obj = {}; + defineOwnDataProperty(obj, "etaoin", "success"); + assert(Object.hasOwn(obj, "etaoin")); + assertStrictEquals(obj.etaoin, "success"); + }); + + it("[[Call]] defines a configurable, enumerable, writable property", () => { + const obj = {}; + defineOwnDataProperty(obj, "etaoin", "success"); + assertEquals( + Object.getOwnPropertyDescriptor(obj, "etaoin"), + { + configurable: true, + enumerable: true, + value: "success", + writable: true, + }, + ); + }); + + it("[[Call]] returns the provided object", () => { + const obj = {}; + assertStrictEquals( + defineOwnDataProperty(obj, "etaoin", null), + obj, + ); + }); + + it("[[Construct]] throws an error", () => { + assertThrows(() => new defineOwnDataProperty(obj, "etaoin", null)); + }); + + describe(".length", () => { + it("[[Get]] returns the correct length", () => { + assertStrictEquals(defineOwnDataProperty.length, 3); + }); + }); + + describe(".name", () => { + it("[[Get]] returns the correct name", () => { + assertStrictEquals( + defineOwnDataProperty.name, + "defineOwnDataProperty", + ); + }); + }); +}); + +describe("defineOwnNonenumerableDataProperty", () => { + it("[[Call]] defines the property", () => { + const obj = {}; + defineOwnNonenumerableDataProperty(obj, "etaoin", "success"); + assert(Object.hasOwn(obj, "etaoin")); + assertStrictEquals(obj.etaoin, "success"); + }); + + it("[[Call]] defines a configurable, non·enumerable, writable property", () => { + const obj = {}; + defineOwnNonenumerableDataProperty(obj, "etaoin", "success"); + assertEquals( + Object.getOwnPropertyDescriptor(obj, "etaoin"), + { + configurable: true, + enumerable: false, + value: "success", + writable: true, + }, + ); + }); + + it("[[Call]] returns the provided object", () => { + const obj = {}; + assertStrictEquals( + defineOwnNonenumerableDataProperty(obj, "etaoin", null), + obj, + ); + }); + + it("[[Construct]] throws an error", () => { + assertThrows(() => + new defineOwnNonenumerableDataProperty(obj, "etaoin", null) + ); + }); + + describe(".length", () => { + it("[[Get]] returns the correct length", () => { + assertStrictEquals(defineOwnNonenumerableDataProperty.length, 3); + }); + }); + + describe(".name", () => { + it("[[Get]] returns the correct name", () => { + assertStrictEquals( + defineOwnNonenumerableDataProperty.name, + "defineOwnNonenumerableDataProperty", + ); + }); + }); +}); + describe("defineOwnProperty", () => { it("[[Call]] defines the property", () => { const obj = {}; defineOwnProperty(obj, "etaoin", {}); - assert("etaoin" in obj); + assert(Object.hasOwn(obj, "etaoin")); }); it("[[Call]] returns the provided object", () => { @@ -525,17 +629,45 @@ describe("frozenCopy", () => { ); }); + it("[[Call]] preserves data properties", () => { + const properties = { + implied: { + configurable: false, + enumerable: true, + }, + writable: { + configurable: false, + enumerable: true, + value: "etaoin", + writable: true, + }, + nonwritable: { + configurable: false, + enumerable: true, + value: "shrdlu", + writable: false, + }, + }; + assertEquals( + Object.getOwnPropertyDescriptors( + frozenCopy(Object.create(null, properties)), + ), + { + implied: { + ...properties.implied, + value: undefined, + writable: false, + }, + writable: { ...properties.writable, writable: false }, + nonwritable: properties.nonwritable, + }, + ); + }); + it("[[Call]] does not copy properties on the prototype", () => { assert( !("failure" in - frozenCopy(Object.create({ failure: undefined }), { - data: { - configurable: true, - value: undefined, - writable: true, - }, - accessor: { configurable: true, get: undefined }, - })), + frozenCopy(Object.create({ failure: undefined }))), ); });