Learning Record
今日踩坑记录
项目中有个需求:需要动态替换某个域名到另一个域名,因此使用正则去做匹配。域名列表是一个字符串数组。
问题代码
const domains = ["domain1.oldsite.com", "domain2.oldsite.com", "domain3.example.com"];
"https://domain1.oldsite.com/api/list".indexOf("https:") > -1 && domains.forEach((i) => (i = i.replace("oldsite", "newsite")));
console.log(domains, "domains");
// 输出:['domain1.oldsite.com', 'domain2.oldsite.com', 'domain3.example.com']
// 期望:['domain1.newsite.com', 'domain2.newsite.com', 'domain3.example.com']
问题:我期望 forEach 方法能够修改原数组,但实际上原数组 domains 并没有发生任何变化
原因分析
这里涉及到两个核心概念:
1. forEach 回调中对参数重新赋值无法修改原数组
Array.prototype.forEach() 虽然会遍历数组,但回调函数的参数 i 只是数组元素的一个局部副本。对 i 重新赋值,只是修改了这个局部变量,原数组中的元素不会受到影响。
const arr = [1, 2, 3];
arr.forEach((i) => (i = i * 2)); // 只是修改了局部变量 i
console.log(arr); // [1, 2, 3] - 原数组未变
2. forEach 和 map 的区别
forEach:遍历数组,没有返回值(返回undefined),通常用于执行副作用操作map:遍历数组,返回一个新数组,不会修改原数组
但无论使用哪个方法,在回调中通过 i = xxx 重新赋值参数,都无法修改原数组元素,因为参数只是元素的副本。
const arr = [1, 2, 3];
// forEach:对参数赋值无效
arr.forEach((i) => (i = i + 1));
console.log(arr); // [1, 2, 3] - 原数组未变
// map:同样无法修改原数组,但至少能拿到返回值
const newArr = arr.map((i) => i + 1);
console.log(arr); // [1, 2, 3] - 原数组未变
console.log(newArr); // [2, 3, 4] - 新数组
3. 基本类型 vs 引用类型
即使我们能修改 i,由于字符串和数字都是基本类型(primitive type),它们是按值传递的,修改局部变量不会影响原数组。
let num = 10;
function modify(n) {
n = 20; // 只修改了局部副本
return n;
}
modify(num);
console.log(num); // 10 - 原变量未变
正确的解决方案
方案 1:使用 map 的返回值(推荐)
const domains = ["domain1.oldsite.com", "domain2.oldsite.com", "domain3.example.com"];
if ("https://domain1.oldsite.com/api/list".indexOf("https:") > -1) {
const newDomains = domains.map((domain) => domain.replace("oldsite", "newsite"));
console.log(newDomains); // ['domain1.newsite.com', 'domain2.newsite.com', 'domain3.example.com']
}
// 或者直接重新赋值
let domains = ["domain1.oldsite.com", "domain2.oldsite.com", "domain3.example.com"];
domains = domains.map((domain) => domain.replace("oldsite", "newsite"));
console.log(domains); // ['domain1.newsite.com', 'domain2.newsite.com', 'domain3.example.com']
方案 2:使用 forEach 修改数组元素(需要下标)
如果确实需要修改原数组(不推荐),可以使用 forEach 配合索引:
const domains = ["domain1.oldsite.com", "domain2.oldsite.com", "domain3.example.com"];
domains.forEach((domain, index) => {
domains[index] = domain.replace("oldsite", "newsite");
});
console.log(domains); // ['domain1.newsite.com', 'domain2.newsite.com', 'domain3.example.com']
方案 3:使用传统 for 循环
const domains = ["domain1.oldsite.com", "domain2.oldsite.com", "domain3.example.com"];
for (let i = 0; i < domains.length; i++) {
domains[i] = domains[i].replace("oldsite", "newsite");
}
console.log(domains); // ['domain1.newsite.com', 'domain2.newsite.com', 'domain3.example.com']
相似的易错点汇总
1. filter 和 map 不会修改原数组
const arr = [1, 2, 3, 4, 5];
// ❌ 错误:期望修改原数组
arr.filter((i) => i > 2);
console.log(arr); // [1, 2, 3, 4, 5] - 原数组未变
// ✅ 正确:接收返回值
const filtered = arr.filter((i) => i > 2);
console.log(filtered); // [3, 4, 5]
2. slice 返回新数组,splice 修改原数组
const arr = [1, 2, 3, 4, 5];
// slice 不修改原数组
const sliced = arr.slice(1, 3);
console.log(arr); // [1, 2, 3, 4, 5]
console.log(sliced); // [2, 3]
// splice 修改原数组
const spliced = arr.splice(1, 2);
console.log(arr); // [1, 4, 5] - 原数组被修改
console.log(spliced); // [2, 3]
3. concat 不修改原数组
const arr1 = [1, 2];
const arr2 = [3, 4];
// ❌ 错误
arr1.concat(arr2);
console.log(arr1); // [1, 2] - 原数组未变
// ✅ 正确
const combined = arr1.concat(arr2);
console.log(combined); // [1, 2, 3, 4]
4. sort 和 reverse 会修改原数组
const arr = [3, 1, 2];
// ⚠️ 注意:sort 会修改原数组
arr.sort();
console.log(arr); // [1, 2, 3] - 原数组被修改
// 如果不想修改原数组,先复制
const original = [3, 1, 2];
const sorted = [...original].sort();
console.log(original); // [3, 1, 2]
console.log(sorted); // [1, 2, 3]
5. 对象数组中修改对象属性会影响原数组
const users = [
{ name: "Alice", age: 20 },
{ name: "Bob", age: 25 },
];
// ⚠️ 注意:这会修改原数组中的对象
users.map((user) => (user.age = user.age + 1));
console.log(users); // [{ name: 'Alice', age: 21 }, { name: 'Bob', age: 26 }]
// 如果不想修改原数组,需要创建新对象
const updated = users.map((user) => ({ ...user, age: user.age + 1 }));
console.log(users); // [{ name: 'Alice', age: 20 }, { name: 'Bob', age: 25 }]
console.log(updated); // [{ name: 'Alice', age: 21 }, { name: 'Bob', age: 26 }]
6. 字符串方法不会修改原字符串
const str = "hello";
// ❌ 错误:字符串是不可变的
str.toUpperCase();
console.log(str); // 'hello' - 原字符串未变
// ✅ 正确
const upper = str.toUpperCase();
console.log(upper); // 'HELLO'
总结
核心要点
- map/filter/concat/slice 等方法都返回新数组,不会修改原数组
- sort/reverse/splice/push/pop/shift/unshift 等方法会修改原数组
- 基本类型(string、number、boolean)是按值传递,赋值不影响原变量
- 引用类型(object、array)是按引用传递,修改属性会影响原对象
- 养成习惯:使用数组方法时,明确该方法是否修改原数组
最佳实践
- 优先使用纯函数:接收返回值,不修改原数组,代码更可预测
- 需要修改原数组时:使用明确会修改的方法(splice、sort 等)或通过索引直接赋值
- 处理引用类型时:注意深拷贝 vs 浅拷贝的区别