哈喽,各位未来的代码大师们,欢迎来到“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)
看到了吧?obj1
和 obj2
指向的是同一个对象,修改 obj2
的 age
属性,obj1
的 age
也跟着变了。 函数 changeObject
里面修改了 obj
的 age
属性,同样影响了 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
第五幕:如何避免对象被意外修改?
有时候,我们不希望函数修改原来的对象,这时候就需要采取一些措施,来避免“按引用传递”带来的副作用。
-
创建对象的副本(深拷贝)
最常用的方法就是创建对象的副本,这样函数操作的是副本,不会影响到原来的对象。 深拷贝有很多实现方式,比如:
- 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); // 输出:深圳
-
使用
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
。 -
使用展开运算符 (…) (浅拷贝)
展开运算符也可以用来创建对象的浅拷贝。
上代码:
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()
一样,展开运算符也只能拷贝第一层属性。 -
使用
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 的“按值传递”和“按引用传递”有了更深入的理解。 记住,理解这些概念,就像掌握了编程世界的魔法,可以让你写出更健壮、更可维护的代码。 祝大家在代码的世界里玩得开心!
下次再见!