从 Java、Python 转到 JavaScript,先看清对象到底被谁改了

举报
Wetter 发表于 2026/11/04 09:18:12 2026/11/04
【摘要】 const 不能阻止对象被修改,展开、filter 和 map 也不会自动复制嵌套对象。本文用任务默认配置说明绑定、引用与浅拷贝的区别,并给出避免跨请求污染的写法。

任务服务里有一份默认配置:通知默认开启,标签默认为空。创建任务时展开一次,拿到“新对象”,再按用户要求修改它。代码看起来很稳妥,下一条请求却继承了上一条的设置。

迁移到 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,为什么会丢掉?

【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。