@feugene/mu
Version:
Helpful TS utilities without dependencies
90 lines • 3.96 kB
JavaScript
import clone from '../core/clone.mjs';
import isObject from '../is/isObject.mjs';
// Узкое определение plain object без опоры на constructor
function isPlainObject(val) {
if (!isObject(val))
return false;
const proto = Object.getPrototypeOf(val);
return proto === Object.prototype || proto === null;
}
const FORBIDDEN_KEYS = new Set(['__proto__', 'prototype', 'constructor']);
function isForbiddenKey(key) {
return typeof key === 'string' ? FORBIDDEN_KEYS.has(key) : false;
}
function ownEnumerableKeys(obj) {
const keys = Object.keys(obj);
const symbols = Object.getOwnPropertySymbols(obj).filter(sym => {
const desc = Object.getOwnPropertyDescriptor(obj, sym);
return !!desc?.enumerable;
});
return keys.concat(symbols);
}
function shallowCopyWithSymbols(obj) {
const res = { ...obj };
for (const sym of Object.getOwnPropertySymbols(obj)) {
const desc = Object.getOwnPropertyDescriptor(obj, sym);
if (desc?.enumerable) {
res[sym] = obj[sym];
}
}
return res;
}
/**
* Apply default values from one or more source objects to an origin object without mutation.
*
* Semantics (v5, ESM-only, Node 22+):
* - Immutability: returns a new object; `origin` and `sources` are not mutated.
* - Keys: only own enumerable string and symbol keys from sources are considered; inherited/non-enumerable are ignored.
* - Guards: forbidden keys `"__proto__"`, `"prototype"`, `"constructor"` are skipped (proto-pollution safe).
* - Deep behavior: if both destination and source values are plain objects, defaults are applied recursively.
* - Arrays: when setting a missing key from a source array, the array is cloned (not referenced). Arrays are not deep-merged.
* - Presence rule: a key is considered present in destination if it exists as an own property, even if its value is `undefined` or `null` — such keys are not overwritten.
*
* @example
* defaults({ a: { b: 2 } }, { a: { b: 1, c: 3 } })
* // => { a: { b: 2, c: 3 } }
*
* @param origin The base object to apply defaults onto (not mutated).
* @param sources One or more source objects providing default values (left-to-right).
* @returns A new object with defaults applied.
*/
export default function defaults(origin, ...sources) {
// Иммутабельность: не мутируем origin
const result = isPlainObject(origin) ? shallowCopyWithSymbols(origin) : origin;
for (const source of sources) {
if (!isObject(source))
continue;
for (const key of ownEnumerableKeys(source)) {
if (isForbiddenKey(key))
continue;
const srcVal = source[key];
const hasOwn = Object.hasOwn(result, key);
const dstVal = hasOwn ? result[key] : undefined;
// Если ключ уже существует в результате
if (hasOwn) {
// Глубокая установка defaults для plain-objects
if (isPlainObject(dstVal) && isPlainObject(srcVal)) {
;
result[key] = defaults(dstVal, srcVal);
}
// Во всех остальных случаях — ничего не делаем (не переопределяем, даже если undefined)
continue;
}
// Ключа нет — можно задать значение по умолчанию из источника
if (isPlainObject(srcVal)) {
;
result[key] = defaults({}, srcVal);
continue;
}
if (Array.isArray(srcVal)) {
;
result[key] = clone(srcVal);
continue;
}
;
result[key] = srcVal;
}
}
return result;
}
//# sourceMappingURL=defaults.mjs.map