JavaScript Array.fill Shares Objects: Create Independent Items
Array(3).fill({ count: 0 }) creates three entries pointing to one object. Changing items[0].count therefore changes the value observed through every entry. To initialize independent records, construct each object inside a callback: Array.from({ length: 3 }, () => ({ count: 0 })).
The location of object creation matters more than the method name. Returning an existing template from Array.from still shares it, and spreading a template creates independent outer records while retaining its nested arrays. For a matrix, create each row separately and, when cells are mutable objects, create each cell separately too.
Diagnose identity before changing the initializer
The ECMAScript fill algorithm assigns the supplied value to each selected index. It does not call a factory or clone the value. In fill({ count: 0 }), the object literal is evaluated once before the method receives it.
Compare items[0] === items[1]. A true result for object entries identifies shared identity. Comparing JSON output alone cannot distinguish two independent objects from two references to the same object: both can initially print matching fields.
Property mutation and element replacement are different operations. items[0].count = 7 edits the shared object. Assigning items[0] = { count: 7 } replaces only the first array entry; it does not detach the remaining entries from each other. Choose the initializer according to whether each entry needs its own mutable state.
Reproduce the failure and verify independent items
Save this fixture as array-items.mjs and run node array-items.mjs. It was executed during article preparation on October 22, 2026, with Node.js 24.13.0 on macOS. It uses ordinary arrays and plain data objects, without packages or framework state.
import assert from 'node:assert/strict';
const shared = Array(3).fill({ count: 0 });assert.equal(shared[0], shared[1]);shared[0].count = 7;assert.deepEqual(shared.map(item => item.count), [7, 7, 7]);console.log('fill object: 7,7,7');
let calls = 0;const holes = Array(3).map(() => { calls++; return {}; });assert.equal(calls, 0);assert.equal(holes.length, 3);assert.equal(Object.hasOwn(holes, 0), false);console.log('map over holes: 0 calls, length 3');
const template = { count: 0, labels: [] };const reused = Array.from({ length: 3 }, () => template);assert.equal(reused[0], reused[1]);const shallow = Array.from({ length: 3 }, () => ({ ...template }));assert.notEqual(shallow[0], shallow[1]);assert.equal(shallow[0].labels, shallow[1].labels);shallow[0].labels.push('shared');assert.deepEqual(shallow[2].labels, ['shared']);assert.deepEqual(template.labels, ['shared']);console.log('shallow copy: new records, shared nested labels');
const items = Array.from({ length: 3 }, (_, id) => ({ id, count: 0, labels: [] }));assert.equal(new Set(items).size, 3);assert.equal(new Set(items.map(item => item.labels)).size, 3);items[0].count = 7;items[0].labels.push('local');assert.deepEqual(items.map(item => item.count), [7, 0, 0]);assert.deepEqual(items[1].labels, []);assert.deepEqual(items[2].labels, []);console.log('factory: 7,0,0; nested labels independent');
const badGrid = Array(2).fill(Array(3).fill(0));assert.equal(badGrid[0], badGrid[1]);badGrid[0][0] = 7;assert.equal(badGrid[1][0], 7);const grid = Array.from({ length: 2 }, () => Array.from({ length: 3 }, () => ({ count: 0 })));assert.notEqual(grid[0], grid[1]);assert.equal(new Set(grid.flat()).size, 6);grid[0][0].count = 7;assert.deepEqual(grid.map(row => row.map(cell => cell.count)), [[7, 0, 0], [0, 0, 0]]);console.log('matrix factory: [[7,0,0],[0,0,0]]');
const zeros = Array(3).fill(0);zeros[0] = 7;assert.deepEqual(zeros, [7, 0, 0]);assert.deepEqual(Array.from({ length: 0 }, () => ({})), []);console.log('primitive fill and empty factory passed');console.log('all assertions passed');Expected output:
fill object: 7,7,7map over holes: 0 calls, length 3shallow copy: new records, shared nested labelsfactory: 7,0,0; nested labels independentmatrix factory: [[7,0,0],[0,0,0]]primitive fill and empty factory passedall assertions passedThe fixture checks identity and then mutates one entry. For the independent factory, both the outer records and their labels arrays have three distinct identities. The mutation assertions verify the promised isolation rather than assuming that different outer objects imply different children.
Why replacing fill with map can leave holes
Array(3) has length three but no own indexed entries. On an ordinary array with no inherited indexed properties, Array(3).map(() => ({})) never calls the callback. The fixture checks the callback count, length, and absence of index zero explicitly.
The ECMAScript map algorithm checks whether each indexed property exists before invoking the callback. By comparison, the Array.from algorithm supports an array-like input with a length and invokes its mapper for each position in this example. Its second callback argument supplies the index, used above as id.
Array(3).fill(null).map(() => ({})) also works: filling first creates the entries, and the callback constructs a fresh object on every call. Array.from({ length: 3 }, factory) expresses the initialization directly without first filling placeholders. Neither form makes a callback that returns the same existing object produce independent objects.
Allocate mutable children at the required depth
The shallow example creates three records with { ...template }, but each record retains template.labels. Pushing into one labels array therefore appears in the other records and in the template. For a known data shape, build the mutable child inside the factory, as the fixture does with labels: []. See the separate nested object spread explanation for merging existing settings; initialization here creates new state rather than combining patches.
A two-dimensional array has two allocation boundaries. Array(2).fill(Array(3).fill(0)) creates one row and reuses it. The fixture proves that editing one row’s first value also changes the other row’s first value. A factory at the outer level gives each row its own array. A factory at the inner level gives each object cell its own record.
Use the following choices for the ordinary data shapes exercised in the fixture:
| Required initial state | Construction | Verified boundary |
|---|---|---|
| Repeated numeric default | Array(3).fill(0) | Replacing one element leaves the others at zero |
| Independent flat records | Array.from({ length: 3 }, () => ({ count: 0 })) | A new object is constructed per call |
| Records with independent lists | Factory returning { count: 0, labels: [] } | Both record and child list are allocated per call |
| Independent matrix rows and object cells | Nested Array.from factories | Each row and each cell has distinct identity |
| Deliberately shared object | Array(3).fill(existingObject) | Every entry retains that one object |
Passing a function to fill stores that function as the value; it does not invoke it. If a factory returns an existing child object, that child remains shared even though the factory ran multiple times. Independence comes from allocation at every mutable level your application intends to isolate.
These checks cover small fixed dimensions, ordinary arrays, and plain records in the stated Node.js environment. They do not establish browser framework behavior, performance, arbitrary deep-cloning semantics, or handling of proxies and indexed prototype properties. Before using an initializer for mutable state, assert distinct identities at the levels you will edit, then change one item and confirm its neighbors remain unchanged.
References: