从 Java、Python 转到 JavaScript,先看清对象到底被谁改了
任务服务里有一份默认配置:通知默认开启,标签默认为空。创建任务时展开一次,拿到“新对象”,再按用户要求修改它。代码看起来很稳妥,下一条请求却继承了上一条的设置。
迁移到 JavaScript 时,这类问题比语法错误更值得先看。Java 的 final 引用、Python 的可变对象,都能帮助理解它;关键是继续追问:新建的究竟是哪一层?
const 固定绑定,属性仍然可以变化
下面的例子保存为 references.mjs,运行 node references.mjs。.mjs 按 ES module 处理,独立示例不需要先建工程。
import assert from 'node:assert/strict';
const defaults = {
retries: 3,
notification: { enabled: true, channel: 'inbox' },
tags: [],
};
const first = { ...defaults };
assert.notEqual(first, defaults);
assert.equal(first.notification, defaults.notification);
first.notification.enabled = false;
assert.equal(defaults.notification.enabled, false);
const second = {
...defaults,
notification: { ...defaults.notification },
tags: [...defaults.tags],
};
second.notification.channel = 'email';
second.tags.push('meeting');
assert.equal(defaults.notification.channel, 'inbox');
assert.deepEqual(defaults.tags, []);
const tasks = [{ id: 't1', status: 'pending' }];
const selected = tasks.filter(task => task.status === 'pending');
assert.notEqual(selected, tasks);
assert.equal(selected[0], tasks[0]);
assert.notEqual({ id: 't1' }, { id: 't1' });
console.log('references: passed');
first 确实是新对象,notification 却仍指向同一份数据。const first 只限制重新给 first 赋值,管不到 first.notification.enabled。
flowchart TD
A[defaults] --> C[同一个 notification 对象]
B[first 浅拷贝] --> C
D[second 显式复制嵌套层] --> E[独立 notification 对象]
C --> F[修改 enabled 会被两个引用看见]
新数组不等于新元素
filter 返回新数组,元素还是原来的引用。map(task => task) 也一样。只有 map(task => ({ id: task.id })) 这样的显式构造,才会创建新的元素对象;其中若又放入原来的嵌套对象,引用仍会共享。
另一个迁移差异是比较:Python 的列表、字典常用 == 比较内容,JavaScript 对象的 === 比较身份。两个字段完全一样的对象,也可以不相等。测试里用 assert.deepEqual 比较内容,业务里则应决定按任务 ID、版本号还是其他字段比较。
数组方法在这里立即执行。不要把它们理解成 Java Stream 那样等待终止操作的惰性管道。
复制策略要跟着业务契约走
一律深拷贝没有回答“怎么合并”。用户提交 notification: { enabled: false },是保留默认 channel,还是整个替换通知配置?tags 是覆盖还是拼接?这些都应先写下来。
这个项目采用:通知逐字段归一化;tags 整体替换并复制;未知字段不进入结果。这样每次请求都得到独立的已知可变结构,不需要一个替所有场景做决定的 deepMerge。
Object.freeze 也只作用于当前层。冻结 defaults 后,若没有单独冻结 notification,仍可通过它修改 enabled。把默认配置当只读输入、返回时显式构造所需字段,会让边界更清楚。
可以做一个小实验:把 tags 改成对象数组,再执行 [...defaults.tags],修改其中一个对象的属性。解释为什么原数组也能看见变化。能说明引用关系,才算理解了浅拷贝。
本篇依据第 1 课讲义中的对象与展开部分改写。下一篇继续处理默认配置中更隐蔽的情况:用户明确传来的 0 和 false,为什么会丢掉?
- 点赞
- 收藏
- 关注作者
评论(0)