PHP Sort Multidimensional Array by Value (usort, Tested)
usort($rows, fn($a, $b) => $a['grade'] <=> $b['grade']) sorts by one column. Descending, several columns, UK dates, case-insensitive names, keeping keys and array_multisort.
To deep copy an object in JavaScript, use const copy = structuredClone(original). It's built into every modern browser and into Node.js 17 and later, so you don't need a library. It copies nested objects and arrays all the way down, keeps Date, Map, Set and RegExp values as the right types, and even copies circular references. The spread operator ({ ...obj }) and Object.assign() only make a shallow copy, so nested objects are still shared with the original. The old JSON.parse(JSON.stringify(obj)) trick does copy deeply, but it quietly turns dates into strings, drops undefined, empties Maps and Sets, and crashes on circular data. The one thing structuredClone() can't copy is functions, and it doesn't keep class instances as their class.
In this guide I show what "copy" really means for objects, test every common method side by side on the same data, and cover the edge cases people hit in real projects: arrays of objects, classes, functions, frozen objects and speed. Every example ran on Node.js 20.19.2 on 7 October 2026, and the output is pasted from the terminal. structuredClone() behaves the same way in Chrome, Firefox, Safari and Edge.
Objects in JavaScript are stored by reference. When you write const b = a, you don't get a second object; you get a second name for the same one. A shallow copy makes a new outer object, but its properties still point at the same nested objects. A deep copy makes new objects at every level.
const original = { name: "Amira", address: { city: "Leeds" }, tags: ["admin"] };
const sameRef = original;
const shallow = { ...original };
const deep = structuredClone(original);
shallow.address.city = "York"; // nested object is shared
deep.tags.push("editor"); // deep copy is independent
console.log(sameRef === original); // true
console.log(shallow === original); // false
console.log(shallow.address === original.address); // true
console.log(original.address.city); // York
console.log(original.tags, deep.tags);
true
false
true
York
[ 'admin' ] [ 'admin', 'editor' ]
Line by line:
sameRef === original is true. Plain assignment never copies anything.shallow === original is false, so the spread operator did make a new outer object.shallow.address === original.address is true. The nested address object is shared, so changing shallow.address.city to "York" also changed the original. This is the bug that sends most people searching for a deep copy.deep.tags.push("editor") didn't touch original.tags, because structuredClone() created a new array.Whether you need a deep copy depends on your data. If every property is a primitive (strings, numbers, booleans), a shallow copy is a full copy and spread is perfect. As soon as there's an object or array inside, you need to think about it. The same rule applies to the arrays you build with map, filter and reduce, which I cover in my guide to JavaScript map, filter and reduce: they create new arrays, but the objects inside are the same ones.
Some array methods don't even create a new array. sort(), reverse() and splice() change the original in place, so a list you thought was safe can be reordered under your feet. Newer methods such as toSorted() and toReversed() return a new array instead, but it's still a shallow copy: the objects inside are shared. My guide on how to sort an array of objects by property shows toSorted() in action and explains when the original order matters.
JSON.parse(JSON.stringify(obj)) was the standard deep-copy trick for years, and plenty of tutorials still recommend it. It works for plain JSON-shaped data, but here's what happens to a realistic object with a date, a Map, a Set, an undefined value, NaN and a regular expression.
const order = {
id: 42,
placed: new Date("2026-10-07T09:30:00Z"),
items: new Map([["sku-1", 2]]),
coupons: new Set(["AUTUMN10"]),
note: undefined,
total: NaN,
pattern: /^GB\d+$/i,
};
const viaJson = JSON.parse(JSON.stringify(order));
const viaClone = structuredClone(order);
console.log("JSON :", viaJson);
console.log("clone :", viaClone);
console.log("placed is Date?", viaJson.placed instanceof Date, viaClone.placed instanceof Date);
console.log("has note key?", "note" in viaJson, "note" in viaClone);
JSON : {
id: 42,
placed: '2026-10-07T09:30:00.000Z',
items: {},
coupons: {},
total: null,
pattern: {}
}
clone : {
id: 42,
placed: 2026-10-07T09:30:00.000Z,
items: Map(1) { 'sku-1' => 2 },
coupons: Set(1) { 'AUTUMN10' },
note: undefined,
total: NaN,
pattern: /^GB\d+$/i
}
placed is Date? false true
has note key? false true
The JSON version damaged five of the seven properties without a single error:
placed became the string '2026-10-07T09:30:00.000Z'. Calling copy.placed.getFullYear() would now throw, and comparing dates would compare strings instead.items (a Map) and coupons (a Set) became empty objects {}. The data is simply gone.note: undefined disappeared completely, so "note" in copy is false. Code that checks for the key will behave differently.total: NaN became null.pattern became {}.structuredClone() kept every one of them, with the right types: instanceof Date is true, the Map and Set still hold their entries, and undefined and NaN survive. If you're sending data to a server, JSON is still the right format, and my guide to sending JSON with fetch covers that. Just don't use a JSON round trip as a copying tool for data that isn't plain JSON.
A circular reference is an object that points back to itself, directly or through a chain. Trees with parent links, graph data and some framework objects have them.
const team = { name: "Platform" };
team.self = team;
try {
JSON.parse(JSON.stringify(team));
} catch (e) {
console.log("JSON:", e.name, "-", e.message.split("\n")[0]);
}
const copy = structuredClone(team);
console.log("clone keeps the loop:", copy.self === copy, "and is new:", copy !== team);
JSON: TypeError - Converting circular structure to JSON
clone keeps the loop: true and is new: true
JSON.stringify() throws a TypeError. structuredClone() copies the loop correctly: the copy's self property points at the copy, not the original, which is exactly what you want.
structuredClone() uses the structured clone algorithm, the same one browsers use to pass data to Web Workers and store it in IndexedDB. It's designed for data, not behaviour, and that shows in two places.
class Basket {
constructor() { this.items = []; }
add(x) { this.items.push(x); return this; }
get count() { return this.items.length; }
}
try {
structuredClone({ greet() { return "hi"; } });
} catch (e) {
console.log(e.name + ":", e.message);
}
const b = new Basket().add("tea");
const c = structuredClone(b);
console.log("instanceof Basket:", c instanceof Basket);
console.log("count getter:", c.count, "| own keys:", Object.keys(c));
console.log("add is", typeof c.add);
DataCloneError: greet() { return "hi"; } could not be cloned.
instanceof Basket: false
count getter: undefined | own keys: [ 'items' ]
add is undefined
DataCloneError. An object with a method, such as { greet() {} }, can't be cloned at all. The error message names the function, which helps you find it. DOM elements and some host objects throw the same error.Basket has its own data (items), but instanceof Basket is false, the count getter is gone, and the add method is undefined. The prototype chain isn't copied, and neither are getters, setters or property descriptors.The cleanest fix is to give the class its own clone() method that builds a new instance and deep-copies only the data.
class Basket {
constructor(items = []) { this.items = items; }
add(x) { this.items.push(x); return this; }
clone() { return new Basket(structuredClone(this.items)); }
}
const a = new Basket(["tea"]);
const b = a.clone().add("scones");
console.log(a.items, b.items, b instanceof Basket);
[ 'tea' ] [ 'tea', 'scones' ] true
The original basket still has only tea, the copy has tea and scones, and the copy is a real Basket with its methods. This is more honest than a generic deep-copy function too, because the class decides which fields are data and which are things like database connections that should never be copied.
If you really need a generic copy that keeps prototypes and functions, you can write one. This version handles circular references with a WeakMap, copies arrays and dates, and keeps the prototype of every object.
function deepCopy(value, seen = new WeakMap()) {
if (value === null || typeof value !== "object") return value;
if (seen.has(value)) return seen.get(value);
if (value instanceof Date) return new Date(value);
if (Array.isArray(value)) {
const out = [];
seen.set(value, out);
for (const item of value) out.push(deepCopy(item, seen));
return out;
}
const out = Object.create(Object.getPrototypeOf(value));
seen.set(value, out);
for (const key of Reflect.ownKeys(value)) out[key] = deepCopy(value[key], seen);
return out;
}
const user = { name: "Tom", born: new Date("1999-02-01"), roles: ["a"], hello() { return "hi " + this.name; } };
user.me = user;
const copy = deepCopy(user);
copy.roles.push("b");
console.log(user.roles, copy.roles);
console.log(copy.hello(), copy.me === copy, copy.born instanceof Date);
[ 'a' ] [ 'a', 'b' ]
hi Tom true true
It's about 20 lines and it works for everyday objects, but notice what it doesn't do: Maps, Sets, typed arrays and regular expressions would need their own branches. That's why I treat this as a last resort. In order of preference, I use structuredClone(), then a clone() method on the class, then Lodash's cloneDeep() if the project already includes Lodash, and only then a hand-written function.
Arrays behave like objects, so spreading an array copies only the outer array. This is the version of the bug I see most in React and Vue state updates.
const rows = [{ id: 1, qty: 2 }, { id: 2, qty: 5 }];
const shallow = [...rows];
shallow[0].qty = 99;
console.log("after spread edit, original qty:", rows[0].qty);
rows[0].qty = 2;
const deep = structuredClone(rows);
deep[0].qty = 99;
console.log("after structuredClone edit, original qty:", rows[0].qty);
const mapped = rows.map(r => ({ ...r })); // one level deep is enough here
mapped[1].qty = 0;
console.log("after map+spread edit, original qty:", rows[1].qty);
after spread edit, original qty: 99
after structuredClone edit, original qty: 2
after map+spread edit, original qty: 5
[...rows] made a new array, but shallow[0] is the same object as rows[0], so setting qty to 99 changed the original.structuredClone(rows) copied every object, so the original stayed at 2.rows.map(r => ({ ...r })) copies one level inside the array. It's enough here because each row only holds numbers, and it's often what you want when you're updating one item in a list. If the rows had nested objects of their own, you'd be back to sharing them.The rule of thumb: copy as deep as your data goes, and no deeper. For a list of flat records, map plus spread is clear and fast. For anything nested or unknown, use structuredClone(). If the copy is going to be saved, for example to localStorage in a small app like my habit tracker built with JavaScript and localStorage, remember that storage only holds strings, so you'll serialise to JSON there anyway, and dates will need converting back when you load them.
const defaults = { theme: "dark", layout: { sidebar: true } };
const a = Object.assign({}, defaults);
a.layout.sidebar = false;
console.log("Object.assign nested change leaks:", defaults.layout.sidebar);
const frozen = Object.freeze({ inner: { n: 1 } });
const copy = structuredClone(frozen);
copy.inner.n = 2;
console.log("clone of frozen is frozen?", Object.isFrozen(copy), copy.inner.n, frozen.inner.n);
Object.assign nested change leaks: false
clone of frozen is frozen? false 2 1
Object.assign({}, defaults) behaves exactly like spread: the nested layout object was shared, so setting sidebar to false on the copy changed the defaults too. This is a nasty bug in settings code, because the next user to load the defaults gets the previous user's choices. The second test shows another detail: cloning a frozen object gives you an unfrozen copy. Object.isFrozen(copy) is false, and changing copy.inner.n didn't affect the original. That's usually what you want from a copy, but if you rely on freezing to protect data, freeze the copy again.
I timed both on an object holding 20,000 rows, averaging 20 runs after a warm-up.
const big = { rows: Array.from({ length: 20000 }, (_, i) => ({ id: i, name: "row " + i, tags: ["a", "b"], meta: { ok: true } })) };
function time(label, fn) {
fn(); const t = performance.now();
for (let i = 0; i < 20; i++) fn();
return [label, (performance.now() - t) / 20];
}
const r = [time("JSON", () => JSON.parse(JSON.stringify(big))), time("structuredClone", () => structuredClone(big))];
const [a, b] = r;
console.log("both finished; faster on this run:", a[1] < b[1] ? a[0] : b[0]);
both finished; faster on this run: JSON
On this run and this machine, the JSON round trip was faster for this plain data, which isn't surprising: the web.dev article on structuredClone() notes that V8 heavily optimises JSON.parse() for exactly this pattern. The exact milliseconds change from run to run, so I'm not quoting them, but across three runs JSON was roughly one and a half to two times faster, and each copy of all 20,000 rows took tens of milliseconds either way. For the small objects in a typical form or settings panel, both are effectively instant. Pick the method that gives the correct result first. Only if profiling shows that copying is a real bottleneck should you reach for JSON, and only when you know the data is plain JSON. Often the better fix is to copy less: update the one nested object you're changing, rather than cloning the whole state on every keystroke. A debounce function also helps when copies happen on every input event.
After testing all of them on the same data, this is the order I'd pick them in for everyday work:
{ ...obj } or [...arr]. A shallow copy is already a full copy here, and spread is the clearest syntax.rows.map(r => ({ ...r })). It copies exactly one level inside the array, which is all the data has.structuredClone(obj). It's built in, handles dates, Maps, Sets and loops, and fails loudly instead of silently when it meets a function.clone() method on the class, so the copy keeps its prototype and the class decides what counts as data.localStorage, a file): JSON.stringify(). That isn't copying, it's serialising, and you'll convert dates back when you read it.cloneDeep() is fine and copies function references instead of throwing. I wouldn't add Lodash just for this any more, because structuredClone() now covers almost every case.In React, Vue, Svelte and similar libraries, the reason you copy is usually so that the framework notices a change. Those libraries compare references, so a brand-new outer object is what triggers a re-render. That means you rarely need a full deep copy of the whole state. Instead, copy the path you're changing: setUser({ ...user, address: { ...user.address, city: "York" } }) creates a new user and a new address but reuses everything else. Cloning the whole tree with structuredClone() works too, but it gives every nested object a new identity, which can make memoised components re-render for no reason.
structuredClone() takes a second argument, { transfer: [buffer] }. Instead of copying a large ArrayBuffer, it moves it into the copy, and the original buffer becomes unusable. You'll only need this when you're handling big binary data, such as image pixels or audio, and want to avoid holding two copies in memory at once. Here's what that looks like with an 8 MB buffer:
const pixels = new ArrayBuffer(8 * 1024 * 1024); // 8 MB
const job = { name: "thumbnail", pixels };
const moved = structuredClone(job, { transfer: [pixels] });
console.log("copy has", moved.pixels.byteLength, "bytes");
console.log("original now has", job.pixels.byteLength, "bytes");
copy has 8388608 bytes
original now has 0 bytes
The copy received all 8,388,608 bytes, and the original buffer was detached: its length dropped to zero, so any code still holding the original would now read nothing. That's the whole point of transferring, but it's also a trap if another part of your program still expects the original to work. For ordinary objects, leave the second argument out, and you'll never see this behaviour.
{ ...obj } and [...arr] copy one level only. Anything nested is still shared.JSON.parse(JSON.stringify()) on real data. It silently converts dates to strings, drops undefined and functions, empties Maps and Sets, turns NaN into null and throws on circular data.structuredClone() throws DataCloneError. Remove the functions first, clone only the data, or give the class a clone() method.structuredClone is not defined means you're on Node.js 16 or older, an old browser or a test runner with an outdated environment. Upgrade the runtime or the test environment rather than adding a polyfill if you can.Object.assign() to merge settings. Nested defaults get shared and mutated. Clone the defaults first, or merge level by level.Use structuredClone(obj). It returns a brand new object with new copies of every nested object and array, so changing the copy never affects the original. If the object only has primitive values (no nested objects or arrays), { ...obj } is enough. The full list of types it supports is on MDN's structuredClone() page.
For copying, yes. In my test, structuredClone() kept dates, Maps, Sets, undefined, NaN, regular expressions and circular references, while the JSON trick lost or changed five of seven properties without any warning. JSON was faster on plain data in my test (roughly one and a half to two times on 20,000 rows), but for everyday objects both are effectively instant. Use JSON for sending and saving data, and structuredClone() for copying it.
No. The spread operator makes a shallow copy: the top-level object or array is new, but nested objects and arrays are shared with the original. In my example, changing shallow.address.city also changed original.address.city. Object.assign() behaves the same way.
structuredClone() throws a DataCloneError for functions. The best option is a clone() method on your class that creates a new instance and deep-copies the data, so the methods come from the class as normal. Alternatively, write a small recursive copy that keeps prototypes (shown above) or use Lodash's cloneDeep(), which copies function references rather than throwing.
Yes, from Node.js 17 onwards it's a global function, so you call it without importing anything. I ran every example here on Node.js 20.19.2. If you see "structuredClone is not defined", check node -v, and check your test runner's environment too, because some older Jest set-ups didn't expose it.
// note
This is a learning note from studying the web. It is one small topic, written so I can remember it. It is not a course and not a claim that I have finished the subject.
If a sentence is wrong, say so from the contact page and name this title. Drafts never appear here. Related notes, when they exist, are other published posts, and the same sample rule applies to each of them.
usort($rows, fn($a, $b) => $a['grade'] <=> $b['grade']) sorts by one column. Descending, several columns, UK dates, case-insensitive names, keeping keys and array_multisort.
Two HTML elements give you a keyboard-friendly accordion with no JavaScript. Open one at a time with name, swap the arrow for +/−, animate it, and fix the screen reader trap.
Find duplicates with GROUP BY and HAVING, preview them with ROW_NUMBER(), then delete all but one with a self-join. Error 1093, NULLs, case and spaces, and a UNIQUE key.