创见博客
JavaScript 对象传参原理:为什么改属性生效,重新赋值无效?
七崽爱吃小饼干2026/10/19阅读 0

先看题目:

js
const objA = new Object();
objA.atr = 'valueA';

const fn = (o) => {
  o.atr = 'valueB';
  o = new Object();
  o.atr = 'valueC';
};

fn(objA);
console.log(objA.atr); // Output: valueB

问题是:为什么最后输出的是 valueB,而不是 valueC?

答案的关键只有一句话:JavaScript 只有值传递。对象作为参数时,传递的是对象引用的副本。

先拆解执行过程

第一步:创建对象

js
const objA = new Object();
objA.atr = 'valueA';

此时 objA 保存的是一个引用,指向堆上的对象:

text
objA ───> { atr: 'valueA' }

new Object() 等价于 {},这里只是创建一个普通对象并添加了一个属性。

第二步:调用函数,复制引用

js
fn(objA);

进入 fn 时,参数 o 接收的是 objA 的副本。

注意,复制的是引用本身,不是对象内容:

text
objA ─┐
      ├──> { atr: 'valueA' }
o ────┘

这时 objA 和 o 指向同一个对象。通过任何一个引用修改对象,另一个引用都能看到变化。

第三步:修改属性

js
o.atr = 'valueB';

这条语句没有让 o 指向新对象,而是沿着引用找到了那个共享对象,把它的 atr 属性改成 valueB。

因为 objA 也指向这个对象,所以外部看到的 objA.atr 也变成了 valueB:

text
objA ─┐
      ├──> { atr: 'valueB' }
o ────┘

第四步:重新赋值参数

js
o = new Object();

这一步很容易被误解。

o = new Object() 并没有修改原来的对象,而是让局部变量 o 指向了一个全新的对象:

text
objA ───> { atr: 'valueB' }

o ──────> { }   // 新对象

从这一刻开始,o 和 objA 已经指向两个不同的对象。它们之间的“连接”断了。

第五步:修改新对象

js
o.atr = 'valueC';

这行代码修改的是新对象:

text
objA ───> { atr: 'valueB' }

o ──────> { atr: 'valueC' }

函数执行结束后,局部变量 o 被销毁,但 objA 仍然指向第一个对象。所以:

js
console.log(objA.atr); // valueB

如果函数内部在最后打印 o.atr,会得到 valueC。两个结果并不矛盾,因为它们看的根本不是同一个对象。

核心:值传递,还是引用传递?

这是这道题最容易答错的点。

很多人会说:“对象是引用传递,所以函数内部修改会影响外部。”

这个说法不准确。

JavaScript 的参数传递规则是:

  • 原始类型:复制值。
  • 引用类型:复制引用值。

也就是说,对象参数传的是“指向对象的地址”的副本。函数拿到的是同一把钥匙,但不是同一个变量槽位。

可以把它理解为:

text
调用者变量 objA  ──保存──> 地址 0x100
函数参数 o        ──保存──> 地址 0x100 的副本

因此:

  • 通过 o 修改对象内容,会影响 objA,因为两者指向同一个对象。
  • 让 o 指向新对象,不会影响 objA,因为只是修改了函数内部的局部变量。

更准确的说法是 call by sharing,也叫“共享传递”或“对象引用按值传递”。它不是传统意义上的引用传递。

和原始类型对比

原始类型更直观:

js
let count = 1;

function changeCount(n) {
  n = 2;
}

changeCount(count);
console.log(count); // 1

n 只是 count 的副本,修改 n 不会影响外部的 count。

对象的不同之处在于:副本不是整个对象,而是引用。所以你能通过这个引用修改对象内部,但不能通过重新赋值参数来改变外部变量指向的对象。

再做一个对比实验

js
const obj = { value: 1 };

function mutate(o) {
  o.value = 2;
}

function reassign(o) {
  o = { value: 3 };
}

mutate(obj);
console.log(obj.value); // 2

reassign(obj);
console.log(obj.value); // 2

mutate 修改的是共享对象,所以外部能看到变化。

reassign 只是让参数 o 指向新对象,外部的 obj 完全不受影响。

这两个函数放在一起,基本就把“修改对象”和“重新赋值参数”的区别讲清楚了。

如果想让外部变成 valueC,怎么办?

有两种常见方式。

方式一:直接修改原对象

js
const objA = { atr: 'valueA' };

function fn(o) {
  o.atr = 'valueC';
}

fn(objA);
console.log(objA.atr); // valueC

这里没有重新赋值 o,而是一直沿着原引用修改。

也可以用 Object.assign:

js
Object.assign(o, { atr: 'valueC' });

方式二:返回新对象,由调用者重新赋值

js
let objA = { atr: 'valueA' };

function fn(o) {
  return { ...o, atr: 'valueC' };
}

objA = fn(objA);
console.log(objA.atr); // valueC

注意这里 objA 必须是 let。如果使用 const,可以修改对象属性,但不能把 objA 重新赋值为另一个对象。

js
const obj = { value: 1 };
obj.value = 2;        // 可以
obj = { value: 3 };   // 报错

const 限制的是变量绑定,不是对象内容。这一点也经常和参数传递一起被问到。

面试时怎么回答

可以按这个顺序说:

  1. 输出是 valueB。
  2. JavaScript 是值传递。对象参数传递的是引用的副本,不是对象本身。
  3. o.atr = 'valueB' 通过共享引用修改了原对象,所以 objA 能看到变化。
  4. o = new Object() 让局部参数指向新对象,切断了和 objA 的关联。
  5. 后续 o.atr = 'valueC' 只修改新对象,函数结束后局部变量 o 被销毁,objA 仍然指向原对象。

如果再补一句“这通常叫 call by sharing,而不是引用传递”,基本就能把这道题答完整。

最后

这道题不难,但它把两个概念放在了一起:

  • 对象属性可以被修改,因为引用指向的是同一块对象。
  • 参数重新赋值不会影响外部,因为函数拿到的是引用的副本。

记住一句就够:能改对象,不能换对象。 通过参数可以修改共享对象的内容,但重新给参数赋值,只会改变函数内部的局部变量。

评论
0/100