The manual chapter on objects covers the value type, key handling and every builtin in detail. The examples below collect recipes for frequently needed operations that are not part of the core library.
Merging
Spread expressions merge objects shallowly, later keys winning:
let defaults = { theme: "light", fontSize: 12, notifications: true };
let user = { theme: "dark" };
let merged = { ...defaults, ...user };
print(merged, "\n");
// { "theme": "dark", "fontSize": 12, "notifications": true }
For nested objects a recursive version is needed; note the forward declaration, as ucode does not hoist function declarations:
function deepMergeArray;
function deepMerge(target, ...sources) {
for (let source in sources) {
if (type(source) != "object")
continue;
for (let key in keys(source)) {
if (type(source[key]) == "object" && type(target[key]) == "object")
target[key] = deepMerge({ ...target[key] }, source[key]);
else if (type(source[key]) == "array" && type(target[key]) == "array")
target[key] = deepMergeArray(target[key], source[key]);
else
target[key] = source[key];
}
}
return target;
}
function deepMergeArray(target, source) {
for (let i = 0; i < length(source); i++) {
if (type(source[i]) == "object" && type(target[i]) == "object")
target[i] = deepMerge({ ...target[i] }, source[i]);
else if (type(source[i]) == "array" && type(target[i]) == "array")
target[i] = deepMergeArray(target[i], source[i]);
else
target[i] = source[i];
}
return target;
}
let profile = {
name: "Alice",
preferences: { theme: "light", sidebar: { visible: true, width: 250 } }
};
let updates = { preferences: { theme: "dark", sidebar: { width: 300 } } };
deepMerge({}, profile, updates);
/*
{
"name": "Alice",
"preferences": {
"theme": "dark",
"sidebar": { "visible": true, "width": 300 }
}
}
*/
Filtering and mapping
function filterObject(obj, filterFn) {
let result = {};
for (let key in keys(obj))
if (filterFn(key, obj[key]))
result[key] = obj[key];
return result;
}
let mixed = { a: 1, b: "string", c: 3, d: true, e: 4.5 };
filterObject(mixed, (key, value) =>
type(value) == "int" || type(value) == "double");
// { "a": 1, "c": 3, "e": 4.5 }
function mapObject(obj, mapFn) {
let result = {};
for (let key in keys(obj))
result[key] = mapFn(key, obj[key]);
return result;
}
mapObject({ apple: 1.25, banana: 0.75, cherry: 2.50 },
(fruit, price) => price * 0.8);
// { "apple": 1, "banana": 0.6, "cherry": 2 }
Caches and lookup tables
Objects make natural memoization caches, captured by closure:
let fibonacci = (function () {
let cache = {};
return function fib(n) {
if (exists(cache, n))
return cache[n];
let result = (n <= 1) ? n : fib(n - 1) + fib(n - 2);
cache[n] = result;
return result;
};
})();
fibonacci(40); // 102334155
Lookup tables replace chains of conditionals, with ?? supplying the fallback:
let statusMessages = { "200": "OK", "404": "Not Found", "500": "Server Error" };
let getStatus = (code) => statusMessages[code] ?? "Unknown";
getStatus(404); // "Not Found"
getStatus(999); // "Unknown"
Deep cloning
A deep clone re-uses the array and object halves from the arrays page; the object half, adapted to start from an object:
function deepCloneArray;
function deepClone(obj) {
if (type(obj) != "object")
return obj;
let clone = {};
for (let key in keys(obj)) {
if (type(obj[key]) == "object")
clone[key] = deepClone(obj[key]);
else if (type(obj[key]) == "array")
clone[key] = deepCloneArray(obj[key]);
else
clone[key] = obj[key];
}
return clone;
}
Comparing by value
Equality operators test reference identity; structural comparison is a recipe, again forward-declared to break the mutual recursion:
function arrayEquals;
function objectEquals(obj1, obj2) {
if (type(obj1) != "object" || type(obj2) != "object")
return obj1 === obj2;
let keys1 = keys(obj1);
let keys2 = keys(obj2);
if (length(keys1) != length(keys2))
return false;
for (let key in keys1) {
if (!exists(obj2, key))
return false;
if (type(obj1[key]) == "object" && type(obj2[key]) == "object") {
if (!objectEquals(obj1[key], obj2[key]))
return false;
} else if (type(obj1[key]) == "array" && type(obj2[key]) == "array") {
if (!arrayEquals(obj1[key], obj2[key]))
return false;
} else if (obj1[key] !== obj2[key]) {
return false;
}
}
return true;
}
function arrayEquals(arr1, arr2) {
if (length(arr1) != length(arr2))
return false;
for (let i = 0; i < length(arr1); i++) {
if (type(arr1[i]) == "object" && type(arr2[i]) == "object") {
if (!objectEquals(arr1[i], arr2[i]))
return false;
} else if (type(arr1[i]) == "array" && type(arr2[i]) == "array") {
if (!arrayEquals(arr1[i], arr2[i]))
return false;
} else if (arr1[i] !== arr2[i]) {
return false;
}
}
return true;
}
Iterating and sorting
The two-variable for ... in form destructures key and value:
let inventory = { apples: 50, oranges: 25, bananas: 30 };
for (let fruit, quantity in inventory)
printf("We have %d %s in stock\n", quantity, fruit);
Objects keep their keys in insertion order, and sort() reorders them in place; the compare function receives both keys and both values:
let stock = { apples: 45, bananas: 25, oranges: 30, grapes: 60 };
sort(stock, (k1, k2, v1, v2) => v2 - v1);
for (let fruit, quantity in stock)
printf("%s: %d\n", fruit, quantity);
// grapes: 60
// apples: 45
// oranges: 30
// bananas: 25