先记住这个答案
JavaScript 的参数都是按值传递的,调用时把实参的值复制一份给形参。原始值复制的是值本身;对象复制的是「指向这个对象的引用」,所以形参和外部变量指向同一个对象。在函数里改对象的属性,外面能看到;给形参重新赋一个新对象,只是形参换了指向,外面的变量不变。有人把这种行为叫 call by sharing(共享传递),本质上还是按值传递,只不过这个值是引用。
- 所有参数都按值传递,对象传的是引用的副本
- 函数里改属性,外面能看到
- 给形参重新赋值,外面不受影响
- 展开运算符
{...obj}只拷贝一层
一个函数里同时做两件事
先改传进来的对象的属性,再给形参赋一个新对象,看外面的变量变成了什么。
function update(profile) {
profile.name = 'Grace' // 改的是共享的那个对象
profile = { name: 'Lin' } // profile 指向新对象,和外面断开
console.log(profile.name)
}
const user = { name: 'Ada' }
update(user)
console.log(user.name)查看输出与解释
Lin
Grace打个比方:user 手里有一把房子的钥匙,调用时复制了一把给 profile。第一行 profile 用钥匙开门,把房子里的名字改成了 Grace,user 回家当然也看到 Grace。第二行 profile 把自己的钥匙换成了另一套房子的,这和 user 手里那把钥匙没有关系,所以外面还是 Grace,不是 Lin。
为什么说不是按引用传递
真正的按引用传递,是把变量本身交给函数,函数里给形参赋值,外面的变量也跟着变。C++ 的引用参数、C# 的 ref 就是这样。用 swap 函数一试就知道 JavaScript 不是。
swap 交换不了外面的变量JavaScriptfunction swap(a, b) {
const t = a
a = b // 只改了形参 a
b = t // 只改了形参 b
}
let x = { v: 1 }
let y = { v: 2 }
swap(x, y)
console.log(x.v, y.v) // 外面没变查看输出与解释
1 2如果是按引用传递,x 和 y 应该被交换。实际上 a 和 b 只是两份钥匙的拷贝,函数里怎么换都不影响外面的 x 和 y。所以说「对象按引用传递」是一种容易让人推错的简化说法,面试时最好讲准确:按值传递,对象的值是引用。
项目里怎么避免函数改掉传入的对象
函数悄悄改了传入的对象,是很多诡异 bug 的来源。比如 React 里直接改 state 对象,引用没变,组件就不会重新渲染。通用的做法是不改原对象,返回一个新对象。
function rename(user, name) {
return { ...user, name } // 顶层是新对象
}
const u = { name: 'Ada', tags: ['a'] }
const u2 = rename(u, 'Bob')
u2.tags.push('b') // tags 数组还是共享的
console.log(u.name, u.tags.length)查看输出与解释
Ada 2name 没有被改,因为 u2 是新对象。但 tags 被拷贝过来的只是数组的引用,u2.tags 和 u.tags 还是同一个数组,push 之后两边都变成了两个元素。所以做不可变更新时,改到哪一层,就要把那一层也复制一份,比如 { ...user, tags: [...user.tags, 'b'] }。
容易答错的地方
- 说「对象是按引用传递的」
- 这个说法会让人以为给形参重新赋值能改掉外面的变量,上面的
swap例子就会推错。准确的说法是:参数都按值传递,对象的值是一个引用。 - 以为
const声明的对象不能被改 const限制的是变量不能重新赋值,对象内部的属性照样可以改。传给函数之后,函数也能改它的属性。想让对象不可改,要用Object.freeze,而且它也只冻结一层。
面试官还会怎么问?
数组作为参数也一样吗?
一样。函数里 push、splice、直接改下标,外面的数组都会变;给形参赋一个新数组,外面不变。slice、map、filter 返回新数组,不改原数组,但里面的对象元素仍是共享的。
怎么深拷贝一个对象?
现在首选 structuredClone(obj),能处理嵌套对象、Date、Map、Set 和循环引用。但它不支持函数和 DOM 节点,遇到会抛错,类的原型也不会保留。JSON.parse(JSON.stringify(obj)) 会丢掉 undefined、函数,并把 Date 变成字符串,只适合纯数据。
字符串是原始值,为什么还能调用方法?
调用 'abc'.toUpperCase() 时,引擎临时把字符串包装成 String 对象,调用完就丢掉。字符串本身不可变,所有字符串方法都返回新字符串,所以传字符串进函数,函数里怎么操作都改不了外面的值。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。