JavaScript Array.fill 的对象参数:只改第一格,为什么三格的计数一起变了

前天 3阅读

给三张任务卡初始化计数,用Array(3).fill({ count: 0 })看起来简洁。随后只把第一张卡的count改成七,打印时三张卡却全是七。fill收到的是已经创建好的一个对象,它不会替每个位置再执行一次对象构造。

本例使用Node.js标准库断言,已在Node.js v24.19.0实跑。把完整程序保存为demo.mjs,运行node demo.mjs。只创建三个元素的小数组,不访问文件或网络;现代浏览器也具备核心数组方法,但其中的node:assert导入属于Node环境。

JavaScript Array.fill 的对象参数:只改第一格,为什么三格的计数一起变了

AI模型生成概念图:左侧三个入口指向同一个物件,右侧每个入口拥有独立物件,用于解释初始化时的引用关系;不是内存地址截图。

完整实验程序

代码先修改共享对象内部的count,再替换数组第一格,最后使用Array.from逐次创建对象。三个观察刻意采用不同值,避免只看初始化输出时,把相等的内容误认成独立的对象。

完整可运行程序

import assert from 'node:assert/strict';

const shared = Array(3).fill({ count: 0 });
shared[0].count = 7;
assert.equal(shared[0], shared[1]);
assert.deepEqual(shared.map(item => item.count), [7, 7, 7]);
console.log('shared counts:', JSON.stringify(shared.map(item => item.count)));

shared[0] = { count: 9 };
assert.notEqual(shared[0], shared[1]);
assert.equal(shared[1], shared[2]);
console.log('after replacement:', JSON.stringify(shared.map(item => item.count)));

const independent = Array.from({ length: 3 }, () => ({ count: 0 }));
independent[0].count = 7;
assert.notEqual(independent[0], independent[1]);
assert.deepEqual(independent.map(item => item.count), [7, 0, 0]);
console.log('factory counts:', JSON.stringify(independent.map(item => item.count)));

let calls = 0;
const make = () => { calls += 1; return { count: 0 }; };
const functions = Array(3).fill(make);
assert.equal(calls, 0);
assert.equal(functions[0], make);
assert.equal(functions[1], make);
console.log('fill function:', 'calls=' + calls, 'type=' + typeof functions[0]);

本次实际输出

shared counts: [7,7,7]
after replacement: [9,7,7]
factory counts: [7,0,0]
fill function: calls=0 type=function

三个位置保存了同一个对象入口

第一行shared counts是[7,7,7]。shared[0]与shared[1]身份相等的断言解释了原因:读取任意一格,都会到达同一个对象。fill依次给索引赋相同的value,对象字面量在传给fill之前只求值了一次。

这不需要异步任务、闭包或框架响应式机制参与。排查时先把界面剥离,像示例一样直接检查两个元素是否严格相等。即使打印出来的是三份相同的对象文字,也不能据此判断它们占据三个独立的对象身份。

替换一格与修改对象内部不是同一个动作

第二行after replacement变为[9,7,7]。把shared[0]赋成新对象,只改了数组零号位置保存的引用;一号和二号位置仍指向原来的共享对象,所以它们继续保持七。这个反例能阻止另一种误解:fill并没有把三格永久绑定为必须一起赋值。

选择修复方式要看业务要求。如果各格只保存数字零,fill(0)通常很自然;数字没有可供原地修改的count字段。如果各格代表能独立追加标签、修改状态的对象,就应在初始化时建立独立对象,而不等第一次编辑时才补救。

让回调负责每次创建

第三行factory counts是[7,0,0]。Array.from的映射回调每次返回新的对象字面量,因此修改第一份不会改变其余两份。关键是创建动作位于回调内部;如果回调每次返回外部已经存在的同一个template,仍然会共享。

最后一行fill function显示calls=0、type=function。把make函数本身交给fill,并不会把它当作工厂调用,而是把同一个函数值放进三格。要调用它,应选择本来就接受回调的方法,并检查回调实际返回的对象是否独立。

独立到哪一层,需要自己定义

本例每次新建的对象只含一个数字,因此外层身份不同就足够说明count互不影响。若工厂返回的对象内部还引用同一个外部数组,外层虽然独立,内部数组仍可能共享。为真实模型补一条针对可修改路径的身份断言,比只检查最外层更可靠。

fill也会直接修改调用它的数组,并返回该数组本身;不要把返回值名称叫copy就以为得到了副本。本文没有测量性能,也不建议为了省一行初始化代码牺牲明确的所有权。先确定哪些状态应共同变化、哪些应独立,再决定创建次数。

参考资料

官方资料核验日期:2026年10月2日。上述输出来自本文完整程序的本地执行,断言全部通过,退出状态为零。

文章版权声明:除非注明,否则均为云鹊BLOG原创文章,转载或复制请以超链接形式并注明出处。