深入理解 JavaScript 函数参数传递:从值传递到纯函数实践

深入理解 JavaScript 函数参数传递:从值传递到纯函数实践

一、问题引入:一段代码引发的思考

先看以下代码示例,你能准确预测两次 console.log 输出结果吗?

javascript

ini 复制代码
// 案例1:修改对象参数的属性
const a = { b: 1 };
const fn = (x) => { x.b = 2; };

console.log('修改前 a :>> ', a); // 输出:{ b: 1 }
fn(a);
console.log('修改后 a :>> ', a); // 输出:{ b: 2 }

// 案例2:深拷贝后修改参数
const c1 = { c1: 'c1' };
const c = { a: 1, c: c1 };
const fn2 = (x) => {
  // 深拷贝:切断与原对象的引用关联
  x = JSON.parse(JSON.stringify(x)); 
  x.a = 2;
  x.c.c1 = 'c1-1';
  return x;
};

console.log('修改前 c :>> ', c); // 输出:{ a: 1, c: { c1: 'c1' } }
fn2(c);
console.log('修改后 c :>> ', c); // 输出:{ a: 1, c: { c1: 'c1' } }

若对结果存疑或想探究背后原理,本文将从内存存储机制切入,彻底解析 JS 函数参数传递规则,并演示如何通过纯函数避免副作用。

二、核心原理:JS 函数只有 "值传递"

许多开发者易混淆 "值传递" 与 "引用传递",但JavaScript 函数参数传递规则仅有 "值传递" ------ 调用函数时,会将实参的值复制一份赋值给形参。

形参和实参本质是独立变量,初始值相同。而 "修改形参是否影响实参" 的关键,在于不同数据类型的 "值" 在内存中的存储方式差异

2.1 基本数据类型:直接存储 "具体值"

基本数据类型(numberstringbooleanundefinednullsymbolbigint)的 "值" 直接存储于栈内存

传递基本数据类型时,函数复制 "具体值" 给形参,形参修改仅影响自身,不波及实参:

javascript

ini 复制代码
const fn = (num) => {
  num = 10; // 仅修改形参
  console.log('形参 num :>> ', num); // 输出:10
};

let a = 5;
fn(a);
console.log('实参 a :>> ', a); // 输出:5(未被修改)

2.2 引用数据类型:存储 "内存地址"

引用数据类型(objectarrayfunction 等)的 "值" 分两部分存储:

  • 栈内存:存储指向堆内存的 "引用地址"(类似文件快捷方式);

  • 堆内存:存储对象具体数据(类似文件实际内容)。

传递引用数据类型时,函数复制 "栈内存中的引用地址",形参和实参指向同一块堆内存数据

  • 若通过形参修改堆内存数据(如修改对象属性),实参会同步变化;

  • 若直接修改形参的引用地址(如 x = { new: 'obj' }),则不影响实参。

示例代码:

javascript

ini 复制代码
const fn = (obj) => {
  // 情况1:修改堆内存数据(影响实参)
  obj.b = 2; 
  // 情况2:修改形参引用地址(不影响实参)
  obj = { b: 3 }; 
  console.log('形参 obj :>> ', obj); // 输出:{ b: 3 }
};

const a = { b: 1 };
fn(a);
console.log('实参 a :>> ', a); // 输出:{ b: 2 }(仅堆数据被修改)

三、实践方案:如何避免修改参数影响外部?

开发中常需在函数内处理数据但不影响外部变量(避免 "副作用"),纯函数是最佳解决方案。

3.1 什么是纯函数?

纯函数需满足两个核心条件:

  1. 无副作用:不修改函数外部数据(全局变量、外部对象属性),也不修改引用类型形参指向的堆内存数据;

  2. 输入决定输出:相同输入必定返回相同输出,不依赖函数外部状态。

纯函数优势:逻辑可预测、易于测试、无意外副作用。

3.2 实现纯函数:切断引用关联

实现纯函数的关键是切断形参与实参的引用关联------ 对引用类型参数进行 "拷贝",在新对象上修改数据。根据需求,拷贝分为 "浅拷贝" 和 "深拷贝"。

3.2.1 浅拷贝:适用于单层对象

若对象属性均为基本数据类型,浅拷贝可切断引用。常用浅拷贝方式:

  • 扩展运算符(...);

  • Object.assign()

  • 数组 slice()concat() 方法(针对数组)。

示例代码:

javascript

ini 复制代码
const fn = (obj) => {
  // 浅拷贝:生成新对象,与原对象无引用关联
  const newObj = { ...obj }; 
  newObj.b = 2; // 修改新对象,不影响原对象
  return newObj;
};

const a = { b: 1 };
const newA = fn(a);
console.log('原对象 a :>> ', a); // 输出:{ b: 1 }(未修改)
console.log('新对象 newA :>> ', newA); // 输出:{ b: 2 }

⚠️ 注意:浅拷贝仅拷贝对象 "第一层属性"。若对象包含嵌套引用类型(如 { a: 1, b: { c: 2 } }),嵌套对象仍共享引用,修改嵌套属性会影响原对象。

3.2.2 深拷贝:适用于嵌套对象

若对象存在多层嵌套(如案例 2 中的 c 对象),需用深拷贝 ------ 完全复制所有层级数据,生成独立新对象。

常用深拷贝方式:

  1. JSON.parse(JSON.stringify(x)) :简单高效,适用于多数场景,但不支持 bigintsymbol、函数、循环引用;

  2. 递归实现深拷贝:自定义递归逻辑,处理所有数据类型(复杂场景推荐);

  3. 第三方库 :如 Lodash 的 _.cloneDeep()(成熟稳定,适合生产环境)。

示例代码(案例 2 解析):

javascript

ini 复制代码
const c1 = { c1: 'c1' };
const c = { a: 1, c: c1 }; // 嵌套对象

const fn2 = (x) => {
  // 深拷贝:完全切断与原对象的所有引用
  const newX = JSON.parse(JSON.stringify(x)); 
  newX.a = 2; // 修改新对象属性
  newX.c.c1 = 'c1-1'; // 修改新对象嵌套属性
  return newX;
};

const newC = fn2(c);
console.log('原对象 c :>> ', c); // 输出:{ a: 1, c: { c1: 'c1' } }(未修改)
console.log('新对象 newC :>> ', newC); // 输出:{ a: 2, c: { c1: 'c1-1' } }

四、总结与避坑指南

4.1 核心结论

  1. JS 函数参数传递只有 "值传递" :形参是实参的 "值拷贝",二者独立;

  2. 引用类型的 "值" 是内存地址:修改形参的堆数据会影响实参,修改形参引用地址则不会;

  3. 纯函数是避免副作用的关键:通过拷贝(浅拷贝 / 深拷贝)切断引用关联,确保逻辑可预测。

4.2 避坑指南

  • 不直接修改函数的引用类型参数(如 function (obj) { obj.key = value }),避免产生副作用;
  • 按需选择浅拷贝 / 深拷贝:根据对象嵌套层级选择,避免过度拷贝影响性能;
  • 慎用 JSON.parse(JSON.stringify()):注意其不支持的数据类型,复杂场景推荐 _.cloneDeep() 或自定义递归拷贝。
相关推荐
六月的可乐2 小时前
探索AI在线前端html编辑器IDE
前端·html·ai编程
今禾2 小时前
深入解析CSS Grid布局:从入门到精通
前端·css·面试
复苏季风3 小时前
前端接口请求中,GET 和 POST 对于传参的string/number区别
前端·javascript
jump6803 小时前
从零开始起项目 0.0.
前端
工会代表3 小时前
前端项目自动化部署改造方案
前端·nginx
Soulkey3 小时前
Grid布局
前端·css
springfe01013 小时前
react useCallback应用
前端
毛骗导演3 小时前
从零构建现代化 CLI 应用:Claude CI 技术实现详解
前端·javascript
CUGGZ3 小时前
前端开发的物理外挂来了,爽到飞起!
前端·后端·程序员