JS `Spread Syntax (…)`:数组与对象的非破坏性拷贝、合并与函数参数展开

各位观众,各位听众,大家好!我是今天的主讲人,咱们今天聊聊JavaScript里那个神奇的三点 ...,也就是 Spread Syntax(展开语法)。这玩意儿看着简单,用起来可是妙用无穷,能让你少写不少代码,还不容易出错。

开场白:三点也能成精?

别看这三个点貌不惊人,它可是JavaScript里的一颗闪耀明星。很多人一开始觉得它神秘莫测,摸不着头脑。但只要你掌握了它,你会发现它简直就是你的代码小助手,哪里需要哪里搬。

第一部分:数组的非破坏性拷贝

咱们先从数组的拷贝开始。在JavaScript里,数组的拷贝可不是那么简单的事儿。如果你直接用赋值符号 =,那可就掉进坑里了。

let arr1 = [1, 2, 3];
let arr2 = arr1; // 错误的拷贝方式!
arr2.push(4);

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

看到了吧? arr1 也被修改了!这是因为 arr2 = arr1 只是让 arr2 指向了 arr1 在内存中的地址,它们俩其实是同一个东西,改谁都一样。这叫做“浅拷贝”。

那么问题来了,如何才能真正地拷贝一个数组,让它们互不干扰呢?这时候,... 就派上用场了。

let arr1 = [1, 2, 3];
let arr2 = [...arr1]; // 使用展开语法进行拷贝

arr2.push(4);

console.log(arr1); // [1, 2, 3]  arr1 没变!
console.log(arr2); // [1, 2, 3, 4]

Bingo! arr1 毫发无损,arr2 成功添加了元素。这就是 ... 的魅力所在。它创建了一个全新的数组,并将 arr1 中的所有元素复制到新数组中。这种拷贝方式被称为“深拷贝”(其实对于简单类型来说,也可以理解为深拷贝,但是如果数组中包含对象,那依然是浅拷贝,这个后面会说到)。

更高级的拷贝:处理嵌套数组

如果你的数组里嵌套了数组或者对象,那 ... 还能用吗?答案是:可以,但要注意!

let arr1 = [1, [2, 3], 4];
let arr2 = [...arr1];

arr2[1].push(5);

console.log(arr1); // [ 1, [ 2, 3, 5 ], 4 ]  arr1 的嵌套数组也被修改了!
console.log(arr2); // [ 1, [ 2, 3, 5 ], 4 ]

啊哦,arr1 里的嵌套数组也被修改了。这是因为 ... 只拷贝了数组的第一层,对于嵌套的数组或对象,它只是复制了引用。也就是说,arr1[1]arr2[1] 指向的是同一个数组对象。

要实现真正的深拷贝,你需要使用其他方法,比如 JSON.parse(JSON.stringify(arr1)) 或者递归函数。但对于简单的数组,... 已经足够好用了。

第二部分:对象的非破坏性拷贝

对象和数组一样,直接赋值也会导致浅拷贝。

let obj1 = { a: 1, b: 2 };
let obj2 = obj1; // 错误的拷贝方式!

obj2.b = 3;

console.log(obj1); // { a: 1, b: 3 }  obj1 也被修改了!
console.log(obj2); // { a: 1, b: 3 }

解决方法同样是 ...

let obj1 = { a: 1, b: 2 };
let obj2 = { ...obj1 }; // 使用展开语法进行拷贝

obj2.b = 3;

console.log(obj1); // { a: 1, b: 2 }  obj1 没变!
console.log(obj2); // { a: 1, b: 3 }

...obj1 会将 obj1 的所有属性复制到一个新的对象中。这样 obj2 就拥有了自己的独立副本,修改它不会影响到 obj1

对象拷贝的深度问题

和数组一样,如果对象里嵌套了对象或者数组,... 也只能进行浅拷贝。

let obj1 = { a: 1, b: { c: 2 } };
let obj2 = { ...obj1 };

obj2.b.c = 3;

console.log(obj1); // { a: 1, b: { c: 3 } }  obj1 的嵌套对象也被修改了!
console.log(obj2); // { a: 1, b: { c: 3 } }

要实现对象的深拷贝,你需要使用其他方法,例如 JSON.parse(JSON.stringify(obj1)) 或者递归函数。

第三部分:数组的合并

... 的另一个强大功能是合并数组。这比使用 concat() 方法更简洁。

let arr1 = [1, 2, 3];
let arr2 = [4, 5, 6];

let arr3 = [...arr1, ...arr2]; // 合并两个数组

console.log(arr3); // [1, 2, 3, 4, 5, 6]

你可以合并任意数量的数组。

let arr1 = [1, 2];
let arr2 = [3, 4];
let arr3 = [5, 6];

let arr4 = [...arr1, ...arr2, ...arr3];

console.log(arr4); // [1, 2, 3, 4, 5, 6]

你甚至可以在合并的过程中添加新的元素。

let arr1 = [1, 2];
let arr2 = [4, 5];

let arr3 = [0, ...arr1, 3, ...arr2, 6];

console.log(arr3); // [0, 1, 2, 3, 4, 5, 6]

第四部分:对象的合并

... 也可以用来合并对象。如果两个对象有相同的属性,后面的对象会覆盖前面的对象。

let obj1 = { a: 1, b: 2 };
let obj2 = { b: 3, c: 4 };

let obj3 = { ...obj1, ...obj2 };

console.log(obj3); // { a: 1, b: 3, c: 4 }  obj2 的 b 覆盖了 obj1 的 b

同样,你可以合并任意数量的对象。

let obj1 = { a: 1 };
let obj2 = { b: 2 };
let obj3 = { c: 3 };

let obj4 = { ...obj1, ...obj2, ...obj3 };

console.log(obj4); // { a: 1, b: 2, c: 3 }

你还可以在合并的过程中添加新的属性。

let obj1 = { a: 1 };
let obj2 = { c: 3 };

let obj3 = { ...obj1, b: 2, ...obj2, d: 4 };

console.log(obj3); // { a: 1, b: 2, c: 3, d: 4 }

第五部分:函数的参数展开

... 最酷炫的用法之一就是用来展开函数的参数。这可以让你轻松地将一个数组传递给一个需要多个参数的函数。

function add(a, b, c) {
  return a + b + c;
}

let numbers = [1, 2, 3];

let result = add(...numbers); // 将数组展开为三个参数

console.log(result); // 6

如果没有 ...,你就需要这样写:

let result = add(numbers[0], numbers[1], numbers[2]); // 麻烦死了!

... 大大简化了代码。

剩余参数 (Rest Parameters)

... 还有一个兄弟,叫做“剩余参数”。它用在函数定义的时候,用来收集剩余的参数到一个数组中。

function myFunc(a, b, ...args) {
  console.log("a:", a);
  console.log("b:", b);
  console.log("args:", args);
}

myFunc(1, 2, 3, 4, 5);
// a: 1
// b: 2
// args: [3, 4, 5]

在这个例子中,ab 分别接收了第一个和第二个参数,剩下的参数都被收集到了 args 数组中。

arguments 对象 vs 剩余参数

在ES6之前,我们通常使用 arguments 对象来访问函数的所有参数。但是 arguments 对象并不是一个真正的数组,而是一个类数组对象。剩余参数则是一个真正的数组,这使得它更加方便使用。

第六部分:实际应用场景举例

为了让大家更好地理解 ... 的用法,我们来看几个实际应用场景的例子。

1. 创建新的 state 对象 (React/Redux)

在 React 和 Redux 中,我们经常需要更新 state 对象。使用 ... 可以很方便地创建新的 state 对象,而不会修改原来的 state。

const initialState = {
  name: "John",
  age: 30,
};

const newState = {
  ...initialState,
  age: 31, // 更新 age 属性
};

console.log(initialState); // { name: 'John', age: 30 }
console.log(newState); // { name: 'John', age: 31 }

2. 扩展现有对象 (Vue.js)

在 Vue.js 中,我们经常需要将一些属性添加到现有的对象中。

const baseOptions = {
  method: "GET",
  headers: {
    "Content-Type": "application/json",
  },
};

const postOptions = {
  ...baseOptions,
  method: "POST",
  body: JSON.stringify({ data: "some data" }),
};

console.log(postOptions);
// {
//   method: 'POST',
//   headers: { 'Content-Type': 'application/json' },
//   body: '{"data":"some data"}'
// }

3. 动态生成数组

function generateArray(length, ...values) {
  return [...Array(length)].map((_, i) => values[i % values.length]);
}

const arr = generateArray(5, 'a', 'b');
console.log(arr); // Output: ['a', 'b', 'a', 'b', 'a']

4. 解构赋值中的应用

展开语法也可以和解构赋值一起使用。

const numbers = [1, 2, 3, 4, 5];
const [first, second, ...rest] = numbers;

console.log(first);   // Output: 1
console.log(second);  // Output: 2
console.log(rest);    // Output: [3, 4, 5]

总结:... 的优点

特性 优点
非破坏性拷贝 避免修改原始数据,提高代码的可维护性
简洁的合并语法 concat()Object.assign() 更简洁易懂
灵活的参数展开 可以轻松地将数组传递给函数
代码可读性 提高代码的可读性,让代码更清晰

温馨提示:注意事项

  • ... 只能用于数组和对象。
  • ... 只能进行浅拷贝,对于嵌套的数组或对象,需要使用其他方法进行深拷贝。
  • ... 在合并对象时,后面的对象会覆盖前面的对象。

结束语:三点虽小,能量巨大

... 展开语法是 JavaScript 中一个非常实用的小技巧。掌握它,可以让你写出更简洁、更高效、更易于维护的代码。希望今天的讲解能帮助大家更好地理解和使用 ...

今天的讲座就到这里,谢谢大家!下次有机会再和大家分享更多的编程技巧。

发表回复

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