深入理解 JavaScript 的按值传递 (Pass by Value) 和按引用传递 (Pass by Reference) 在基本类型和对象类型上的行为。

哈喽,各位未来的代码大师们,欢迎来到“JavaScript 传值奥秘大揭秘”讲座!今天咱们不搞虚的,直接上干货,一起扒一扒 JavaScript 里面“按值传递”和“按引用传递”这俩兄弟的底裤。

开场白:别怕,没那么玄乎!

很多小伙伴一听到“按值传递”和“按引用传递”,头皮就开始发麻,感觉像进了迷宫。其实呢,它们就像咱们平时买东西一样,一个是你给了别人一张钞票,另一个是你给了别人一张购物卡。钞票给了就没了,购物卡给了,里面的余额要是变了,你也知道。

JavaScript 里的传值也是这个道理,理解了这一点,就成功了一半。

第一幕:基本类型——老实的“按值传递”

咱先从基本类型说起,它们包括:

  • Number(数字)
  • String(字符串)
  • Boolean(布尔值)
  • Null(空值)
  • Undefined(未定义)
  • Symbol (ES6 新增,后面有时间再聊)

这些家伙都是老实人,用的是“按值传递”。啥意思呢?就是把它们的值复制一份,然后传递给函数。函数里面怎么折腾这份复制品,都不会影响到原来的变量。

来,上代码:

let num1 = 10;
let num2 = num1; // 将 num1 的值复制一份给 num2

num2 = 20; // 修改 num2 的值

console.log(num1); // 输出:10 (num1 还是原来的值)
console.log(num2); // 输出:20 (num2 变成了 20)

function changeValue(x) {
  x = 100;
  console.log("函数内部 x:", x); // 函数内部 x: 100
}

changeValue(num1); // 将 num1 的值复制一份传递给函数

console.log(num1); // 输出:10 (num1 还是原来的值)

看到了吧?num1 的值被复制给了 num2,之后修改 num2 的值,num1 纹丝不动。函数 changeValue 里面修改了 x 的值,也丝毫影响不了 num1

这就像你复印了一张钞票,然后把复印件撕了,真钞票还是好好的躺在你的钱包里。

第二幕:对象类型——狡猾的“按引用传递”

接下来,轮到对象类型登场了,它们包括:

  • Object(对象,包括普通对象、数组、函数等)

对象类型就有点狡猾了,它们使用的是“按引用传递”。 啥意思呢? 不是复制值,而是复制一个“指针”(或者说“引用”),指向内存中同一个对象。 也就是说,函数里面如果修改了对象,原来的对象也会跟着改变。

来,上代码:

let obj1 = { name: "张三", age: 20 };
let obj2 = obj1; // 将 obj1 的引用复制一份给 obj2

obj2.age = 30; // 修改 obj2 的 age 属性

console.log(obj1.age); // 输出:30 (obj1 的 age 也变成了 30)
console.log(obj2.age); // 输出:30 (obj2 的 age 是 30)

function changeObject(obj) {
  obj.age = 40;
  console.log("函数内部 obj.age:", obj.age); // 函数内部 obj.age: 40
}

changeObject(obj1); // 将 obj1 的引用传递给函数

console.log(obj1.age); // 输出:40 (obj1 的 age 也变成了 40)

看到了吧?obj1obj2 指向的是同一个对象,修改 obj2age 属性,obj1age 也跟着变了。 函数 changeObject 里面修改了 objage 属性,同样影响了 obj1

这就像你和你的朋友合租一套房子,你们都有房子的钥匙(引用),你把房子里的沙发换了,你的朋友回家一看,沙发也变了。

第三幕:数组——对象的一种

数组也是对象的一种,所以它也遵循“按引用传递”的规则。

上代码:

let arr1 = [1, 2, 3];
let arr2 = arr1; // 将 arr1 的引用复制一份给 arr2

arr2.push(4); // 向 arr2 中添加元素

console.log(arr1); // 输出:[1, 2, 3, 4] (arr1 也被修改了)
console.log(arr2); // 输出:[1, 2, 3, 4] (arr2 是 [1, 2, 3, 4])

function changeArray(arr) {
  arr.push(5);
  console.log("函数内部 arr:", arr); // 函数内部 arr: [1, 2, 3, 4, 5]
}

changeArray(arr1); // 将 arr1 的引用传递给函数

console.log(arr1); // 输出:[1, 2, 3, 4, 5] (arr1 也被修改了)

第四幕:字符串的特殊性

字符串虽然是基本类型,但是当它被作为对象属性访问时,会表现出一些“引用传递”的假象。

let str = "hello";
function modifyString(obj) {
  obj.value = "world"; //  尝试修改字符串对象的value属性
}

let obj = new String(str); // 将字符串包装成对象
modifyString(obj);

console.log(obj.value); // world
console.log(str); // hello

第五幕:如何避免对象被意外修改?

有时候,我们不希望函数修改原来的对象,这时候就需要采取一些措施,来避免“按引用传递”带来的副作用。

  1. 创建对象的副本(深拷贝)

    最常用的方法就是创建对象的副本,这样函数操作的是副本,不会影响到原来的对象。 深拷贝有很多实现方式,比如:

    • JSON.parse(JSON.stringify(object)): 简单粗暴,但是有一些局限性,比如无法拷贝函数、循环引用等。
    • 递归拷贝: 比较灵活,可以处理各种复杂情况,但是代码比较复杂。
    • 第三方库: 比如 Lodash 的 _.cloneDeep() 方法,功能强大,使用方便。

    上代码:

    // JSON.parse(JSON.stringify(object))
    let obj1 = { name: "张三", age: 20, address: { city: "北京" } };
    let obj2 = JSON.parse(JSON.stringify(obj1)); // 创建 obj1 的深拷贝
    
    obj2.age = 30;
    obj2.address.city = "上海";
    
    console.log(obj1.age); // 输出:20 (obj1 的 age 还是 20)
    console.log(obj1.address.city); // 输出:北京 (obj1 的 address.city 还是 北京)
    console.log(obj2.age); // 输出:30 (obj2 的 age 是 30)
    console.log(obj2.address.city); // 输出:上海 (obj2 的 address.city 是 上海)
    
    // 递归拷贝
    function deepClone(obj) {
      if (typeof obj !== "object" || obj === null) {
        return obj; // 如果不是对象或者为null,直接返回
      }
    
      let newObj = Array.isArray(obj) ? [] : {}; // 根据obj类型创建新对象
    
      for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
          newObj[key] = deepClone(obj[key]); // 递归拷贝
        }
      }
    
      return newObj;
    }
    
    let obj3 = { name: "李四", age: 25, address: { city: "广州" } };
    let obj4 = deepClone(obj3);
    
    obj4.age = 35;
    obj4.address.city = "深圳";
    
    console.log(obj3.age); // 输出:25
    console.log(obj3.address.city); // 输出:广州
    console.log(obj4.age); // 输出:35
    console.log(obj4.address.city); // 输出:深圳
  2. 使用 Object.assign() (浅拷贝)

    Object.assign() 可以将一个或多个源对象的属性复制到目标对象,但是它只进行浅拷贝,也就是说,如果源对象的属性也是对象,那么复制的只是引用,而不是创建新的对象。

    上代码:

    let obj1 = { name: "王五", age: 28, address: { city: "重庆" } };
    let obj2 = Object.assign({}, obj1); // 创建 obj1 的浅拷贝
    
    obj2.age = 38;
    obj2.address.city = "成都";
    
    console.log(obj1.age); // 输出:28 (obj1 的 age 还是 28)
    console.log(obj1.address.city); // 输出:成都 (obj1 的 address.city 被修改了,因为是浅拷贝)
    console.log(obj2.age); // 输出:38 (obj2 的 age 是 38)
    console.log(obj2.address.city); // 输出:成都 (obj2 的 address.city 是 成都)

    可以看到,Object.assign() 只能拷贝第一层属性,如果属性是对象,那么拷贝的只是引用,所以修改 obj2.address.city 也会影响到 obj1.address.city

  3. 使用展开运算符 (…) (浅拷贝)

    展开运算符也可以用来创建对象的浅拷贝。

    上代码:

    let obj1 = { name: "赵六", age: 32, address: { city: "南京" } };
    let obj2 = { ...obj1 }; // 创建 obj1 的浅拷贝
    
    obj2.age = 42;
    obj2.address.city = "苏州";
    
    console.log(obj1.age); // 输出:32
    console.log(obj1.address.city); // 输出:苏州 (obj1 的 address.city 被修改了,因为是浅拷贝)
    console.log(obj2.age); // 输出:42
    console.log(obj2.address.city); // 输出:苏州

    Object.assign() 一样,展开运算符也只能拷贝第一层属性。

  4. 使用 Array.from() 拷贝数组

    Array.from() 方法可以从一个类似数组或可迭代对象创建一个新的,浅拷贝的数组实例。

    let arr1 = [1, 2, {value: 3}];
    let arr2 = Array.from(arr1);
    
    arr2[0] = 10;
    arr2[2].value = 30; // 浅拷贝,会影响原数组
    
    console.log(arr1); // [ 1, 2, { value: 30 } ]
    console.log(arr2); // [ 10, 2, { value: 30 } ]

第六幕:总结陈词

特性 基本类型 (Number, String, Boolean, Null, Undefined, Symbol) 对象类型 (Object, Array, Function)
传递方式 按值传递 (Pass by Value) 按引用传递 (Pass by Reference)
函数修改影响 不影响原始变量 影响原始对象
拷贝方式 赋值操作会创建新的独立副本 赋值操作只会复制引用,指向同一对象
避免修改方法 无需特殊处理,修改副本不影响原始值 深拷贝创建新对象,避免影响

记住以下几点:

  • 基本类型是老实人,用“按值传递”,函数里面怎么折腾都不会影响原来的变量。
  • 对象类型有点狡猾,用“按引用传递”,函数里面修改对象,原来的对象也会跟着改变。
  • 如果不想让函数修改原来的对象,就创建对象的副本(深拷贝)。

小贴士:

  • 理解“按值传递”和“按引用传递”是编写高质量 JavaScript 代码的基础。
  • 在编写函数时,要仔细考虑是否需要修改传入的对象,如果不需要,最好创建对象的副本。
  • 深拷贝是一个比较耗费性能的操作,要根据实际情况选择合适的拷贝方式。

尾声:

好了,今天的讲座就到这里了。希望通过今天的讲解,大家对 JavaScript 的“按值传递”和“按引用传递”有了更深入的理解。 记住,理解这些概念,就像掌握了编程世界的魔法,可以让你写出更健壮、更可维护的代码。 祝大家在代码的世界里玩得开心!

下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注