【前端面试小册】JS-第15节:函数形参与实参
一、核心概念
1.1 定义
函数的参数都是按值传递的:
- 基本类型:复制值传递
- 复杂类型:复制指针传递
类比理解:参数传递就像复印文件。基本类型就像复印文字内容(复制值),复杂类型就像复印文件地址(复制指针),你修改复印件不会影响原件,但通过地址找到的文件是同一个。
1.2 关键理解
JavaScript 中没有引用传递,只有按值传递。即使是对象,传递的也是对象引用的值(指针),而不是对象本身。
二、按值传递(基本类型)
2.1 基本示例
const name = '成都巴菲特';
function demo(myName) {
myName = '巴菲特';
console.log(myName); // 巴菲特
}
demo(name);
console.log(name); // 成都巴菲特(原值未改变)
2.2 原理分析
传递过程:
graph TD
A[全局变量 name = 成都巴菲特] --> B[调用 demo name]
B --> C[复制 name 的值]
C --> D[传递给形参 myName]
D --> E[myName = 巴菲特]
E --> F[只修改了 myName,不影响原值]
F --> G[name 仍然是 成都巴菲特]
关键点:
- 调用函数
demo时,把全局name的值会拷贝一份,然后传递给myName - 所以函数内部修改
myName的值是不会影响全局的name的值 myName是函数内部作用域的变量
2.3 内存模型
// 全局作用域
name: '成都巴菲特' // 内存地址 A
// 函数调用时
myName: '成都巴菲特' // 内存地址 B(新创建,复制了值)
// 函数内部修改
myName: '巴菲特' // 内存地址 B(修改了副本)
// 全局变量不变
name: '成都巴菲特' // 内存地址 A(原值未变)
三、按值传递(复杂类型 - 复制指针)
3.1 基本示例
let obj = {
name: '成都巴菲特'
};
function demo(myObj) {
myObj.name = '巴菲特';
console.log(myObj.name); // '巴菲特'
}
demo(obj);
console.log(obj.name); // '巴菲特'(原对象被修改)
3.2 原理分析
传递过程:
graph TD
A[全局变量 obj 指向对象A] --> B[调用 demo obj]
B --> C[复制 obj 的指针值]
C --> D[传递给形参 myObj]
D --> E[myObj 和 obj 指向同一对象]
E --> F[修改 myObj.name]
F --> G[实际修改的是同一个对象]
G --> H[obj.name 也被修改]
关键理解:
- 复杂类型传值的时候,是复制指针进行传递
- 所以这个时候
myObj和obj都是指向堆内存的同一个对象 - 所以
myObj.name = '巴菲特'修改的就是obj指向的那个对象,所以obj.name值也修改
3.3 内存模型
// 全局作用域
obj: 0x001 → [对象A: { name: '成都巴菲特' }] // 堆内存
// 函数调用时
myObj: 0x001 → [对象A: { name: '成都巴菲特' }] // 复制了指针,指向同一对象
// 函数内部修改
myObj: 0x001 → [对象A: { name: '巴菲特' }] // 修改了对象属性
// 全局变量也受影响
obj: 0x001 → [对象A: { name: '巴菲特' }] // 因为是同一个对象
3.4 重新赋值的情况
let obj = {
name: '成都巴菲特'
};
function demo(myObj) {
myObj.name = '巴菲特'; // 修改对象属性
myObj = {}; // 重新赋值,指向新对象
myObj.name = '成都'; // 修改新对象的属性
console.log(myObj.name); // '成都'
}
demo(obj);
console.log(obj.name); // '巴菲特'(原对象属性被修改,但对象本身未变)
详细分析:
graph TD
A[obj 指向对象A] --> B[myObj 也指向对象A]
B --> C[修改 myObj.name = 巴菲特]
C --> D[对象A的name被修改]
D --> E[myObj = 新对象B]
E --> F[myObj 现在指向对象B]
F --> G[obj 仍然指向对象A]
G --> H[修改 myObj.name = 成都]
H --> I[只影响对象B,不影响对象A]
步骤说明:
-
myObj.name = '巴菲特':myObj和obj还是指向堆中同一个对象(对象 A),所以name会被修改
-
myObj = {}:- 这一步会让
myObj重新指向堆中的一个新对象(对象 B) - 这一步后
myObj和obj指向堆中的对象不一样了 - 而
myObj是函数的局部变量,在函数执行完毕就会被销毁
- 这一步会让
-
myObj.name = '成都':- 因为
myObj重新指向了一个局部对象,所以修改的值不会影响obj指向的对象
- 因为
-
结果:
console.log(myObj.name)输出对象 B 的name:成都console.log(obj.name)输出对象 A 的name:巴菲特
四、面试题
4.1 题目 1:未声明的变量
let name = '成都巴菲特';
function demo() {
name = '巴菲特'; // 没有声明,直接赋值
console.log(name); // 巴菲特
}
demo();
console.log(name); // 巴菲特(全局变量被修改)
分析:
- 这里是因为
name = xxx这种形式,相当于定义的全局变量 - 所以全局变量
name被修改 - 这块内容在函数作用域讲过,可以去复习
关键点:
- 函数内部未声明的变量赋值,会创建全局变量(非严格模式)
- 严格模式下会报错:
ReferenceError
4.2 题目 2:重新赋值对象
let obj = {
name: '成都巴菲特'
};
function demo(myObj) {
myObj.name = '巴菲特'; // 修改对象属性
myObj = {}; // 重新赋值
myObj.name = '成都'; // 修改新对象属性
console.log(myObj.name); // 成都
}
demo(obj);
console.log(obj.name); // '巴菲特'
详细分析:
// 步骤 1:myObj 和 obj 还是指向堆中同一个对象(对象 A),所以 name 会被修改
myObj.name = '巴菲特';
// 步骤 2:这一步会让 myObj 重新指向堆中的一个新对象(对象 B)
// 这一步后 myObj 和 obj 指向堆中的对象不一样了
// 而 myObj 是函数的局部变量,在函数执行完毕就会被销毁
myObj = {}
// 步骤 3:因为 myObj 重新指向了一个局部对象,所以修改的值不会影响 obj 指向的对象
myObj.name = '成都'
// 步骤 4:所以这里输出对象 B 的 name
console.log(myObj.name) // 成都
// 步骤 5:输出对象 A 的 name
console.log(obj.name) // 巴菲特
内存变化图:
graph TD
A[初始: obj → 对象A name=成都巴菲特] --> B[调用: myObj → 对象A]
B --> C[修改: 对象A name=巴菲特]
C --> D[重新赋值: myObj → 对象B]
D --> E[修改: 对象B name=成都]
E --> F[结果: obj → 对象A name=巴菲特]
E --> G[结果: myObj → 对象B name=成都]
五、参数传递的本质
5.1 为什么是值传递而不是引用传递?
引用传递的特点(JavaScript 不支持):
- 如果支持引用传递,形参和实参应该是同一个变量
- 修改形参应该直接修改实参本身
值传递的特点(JavaScript 支持):
- 形参是实参的副本
- 基本类型:复制值
- 复杂类型:复制指针值
5.2 证明是值传递
let obj = { name: 'A' };
function demo(myObj) {
myObj = { name: 'B' }; // 重新赋值
console.log(myObj.name); // B
}
demo(obj);
console.log(obj.name); // A(原对象未变)
分析:
- 如果是引用传递,
myObj = { name: 'B' }应该直接修改obj - 但实际
obj未变,说明是值传递(复制了指针值)
5.3 为什么对象会被修改?
let obj = { name: 'A' };
function demo(myObj) {
myObj.name = 'B'; // 修改对象属性
console.log(myObj.name); // B
}
demo(obj);
console.log(obj.name); // B(对象被修改)
分析:
- 虽然传递的是指针值的副本,但两个指针指向同一个对象
- 修改对象属性时,通过指针找到的是同一个对象
- 所以原对象会被修改
类比:
- 就像两个人各有一把钥匙(指针副本),但打开的是同一扇门(对象)
- 其中一个人修改了门上的装饰(对象属性),另一个人看到的门也会变化
六、实际应用场景
6.1 函数参数保护
// ✅ 好的做法:不修改原对象
function processData(data) {
// 创建副本,避免修改原对象
const processed = { ...data };
processed.status = 'processed';
return processed;
}
// ❌ 不好的做法:直接修改原对象
function processData(data) {
data.status = 'processed'; // 修改了原对象
return data;
}
6.2 深拷贝 vs 浅拷贝
// 浅拷贝:只复制第一层
function shallowCopy(obj) {
return { ...obj };
}
// 深拷贝:递归复制所有层级
function deepCopy(obj) {
return JSON.parse(JSON.stringify(obj));
}
// 使用示例
const original = {
name: 'A',
nested: { value: 1 }
};
const shallow = shallowCopy(original);
shallow.nested.value = 2;
console.log(original.nested.value); // 2(被修改了)
const deep = deepCopy(original);
deep.nested.value = 3;
console.log(original.nested.value); // 2(未被修改)
七、思考
函数传入复杂类型,复制指针而不是整个值,是为了性能考虑。
原因:
- 如果复制整个对象,对于大对象会消耗大量内存和时间
- 复制指针只需要复制一个地址值(通常 8 字节),非常高效
- 这是 JavaScript 设计上的权衡
八、面试要点总结
核心知识点
- 参数传递方式:JavaScript 中所有参数都是按值传递
- 基本类型:复制值传递,修改不影响原值
- 复杂类型:复制指针传递,修改对象属性会影响原对象,但重新赋值不会
- 性能考虑:复制指针而不是整个对象,提高性能
常见面试题
Q1: JavaScript 的参数传递是按值传递还是按引用传递?
答:按值传递。基本类型复制值,复杂类型复制指针值。JavaScript 没有真正的引用传递。
Q2: 为什么修改对象属性会影响原对象?
答:虽然传递的是指针值的副本,但两个指针指向同一个对象。修改对象属性时,通过指针找到的是同一个对象,所以原对象会被修改。
Q3: 如何避免函数修改原对象?
答:
- 使用展开运算符创建浅拷贝:
{ ...obj } - 使用
Object.assign创建浅拷贝 - 使用深拷贝(如
JSON.parse(JSON.stringify(obj))或structuredClone)
实战建议
- ✅ 理解参数传递的本质,避免意外修改
- ✅ 需要保护原对象时,使用拷贝
- ✅ 注意浅拷贝和深拷贝的区别
- ✅ 合理使用参数传递机制,提高性能
前端面试小册 文章被收录于专栏
每天更新3-4节,持续更新中... 目标:50天学完,上岸银行总行!
海康威视公司福利 1382人发布
查看1道真题和解析