Skip to content

JavaScript 展开语法

什么是展开语法

展开语法(Spread Syntax)是 ES6(ECMAScript 2015)引入的一种新语法,它使用三个点 ... 来表示将一个可迭代对象(如数组、字符串、Set、Map 等)展开为多个元素,或将一个对象展开为多个键值对。展开语法使得我们可以更简洁、更方便地处理数组和对象。

数组展开语法

1. 基本语法

javascript
// 基本数组展开
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const arr3 = [...arr1, ...arr2];
console.log(arr3); // [1, 2, 3, 4, 5, 6]

// 示例:合并数组
const fruits = ["apple", "banana"];
const vegetables = ["carrot", "broccoli"];
const foods = [...fruits, "rice", ...vegetables];
console.log(foods); // ['apple', 'banana', 'rice', 'carrot', 'broccoli']

// 示例:复制数组
const original = [1, 2, 3];
const copy = [...original];
console.log(copy); // [1, 2, 3]
console.log(original === copy); // false(浅拷贝)

// 示例:将字符串展开为数组
const str = "hello";
const chars = [...str];
console.log(chars); // ['h', 'e', 'l', 'l', 'o']

// 示例:将 Set 展开为数组
const set = new Set([1, 2, 3, 3, 4]);
const setArray = [...set];
console.log(setArray); // [1, 2, 3, 4]

// 示例:将 Map 展开为数组
const map = new Map([
  ["a", 1],
  ["b", 2],
]);
const mapArray = [...map];
console.log(mapArray); // [['a', 1], ['b', 2]]

2. 函数参数展开

javascript
// 函数参数展开
function sum(a, b, c) {
  return a + b + c;
}

const numbers = [1, 2, 3];
console.log(sum(...numbers)); // 6

// 示例:Math 函数
const nums = [1, 5, 3, 9, 2];
console.log(Math.max(...nums)); // 9
console.log(Math.min(...nums)); // 1

// 示例:push 方法
const arr = [1, 2, 3];
const more = [4, 5, 6];
arr.push(...more);
console.log(arr); // [1, 2, 3, 4, 5, 6]

// 示例:apply 方法的替代
function greet(name, age) {
  console.log(`Hello, ${name}! You are ${age} years old.`);
}

const person = ["John", 30];
// 传统方式
// greet.apply(null, person);
// 展开语法方式
greet(...person); // Hello, John! You are 30 years old.

3. 剩余参数

剩余参数(Rest Parameters)也是使用三个点 ... 表示,但它与展开语法的用法相反,它是将多个参数收集为一个数组。

javascript
// 剩余参数
function sum(...numbers) {
  return numbers.reduce((total, num) => total + num, 0);
}

console.log(sum(1, 2, 3)); // 6
console.log(sum(1, 2, 3, 4, 5)); // 15

// 示例:混合使用
function calculate(action, ...numbers) {
  if (action === "sum") {
    return numbers.reduce((total, num) => total + num, 0);
  } else if (action === "multiply") {
    return numbers.reduce((total, num) => total * num, 1);
  }
  return 0;
}

console.log(calculate("sum", 1, 2, 3)); // 6
console.log(calculate("multiply", 1, 2, 3)); // 6

// 示例:剩余参数必须是最后一个参数
function test(a, b, ...rest) {
  console.log(a, b, rest);
}

test(1, 2, 3, 4, 5); // 1 2 [3, 4, 5]

对象展开语法

1. 基本语法

对象展开语法是 ES2018(ECMAScript 2018)引入的,它使用三个点 ... 来表示将一个对象展开为多个键值对。

javascript
// 基本对象展开
const obj1 = { a: 1, b: 2 };
const obj2 = { c: 3, d: 4 };
const obj3 = { ...obj1, ...obj2 };
console.log(obj3); // { a: 1, b: 2, c: 3, d: 4 }

// 示例:合并对象
const user = { name: "John", age: 30 };
const address = { city: "New York", country: "USA" };
const userWithAddress = { ...user, ...address };
console.log(userWithAddress); // { name: 'John', age: 30, city: 'New York', country: 'USA' }

// 示例:复制对象
const original = { a: 1, b: 2 };
const copy = { ...original };
console.log(copy); // { a: 1, b: 2 }
console.log(original === copy); // false(浅拷贝)

// 示例:覆盖属性
const obj1 = { a: 1, b: 2 };
const obj2 = { b: 3, c: 4 };
const obj3 = { ...obj1, ...obj2 };
console.log(obj3); // { a: 1, b: 3, c: 4 }(后面的对象覆盖前面的对象)

// 示例:添加新属性
const user = { name: "John", age: 30 };
const updatedUser = { ...user, age: 31, city: "New York" };
console.log(updatedUser); // { name: 'John', age: 31, city: 'New York' }

2. 嵌套对象展开

javascript
// 嵌套对象展开(浅拷贝)
const user = {
  name: "John",
  address: {
    city: "New York",
    country: "USA",
  },
};

const copy = { ...user };
console.log(copy); // { name: 'John', address: { city: 'New York', country: 'USA' } }
console.log(user.address === copy.address); // true(嵌套对象是引用)

// 示例:深拷贝嵌套对象
const deepCopy = {
  ...user,
  address: {
    ...user.address,
  },
};
console.log(deepCopy); // { name: 'John', address: { city: 'New York', country: 'USA' } }
console.log(user.address === deepCopy.address); // false(嵌套对象也是拷贝)

// 示例:更新嵌套对象属性
const updatedUser = {
  ...user,
  address: {
    ...user.address,
    city: "Los Angeles",
  },
};
console.log(updatedUser); // { name: 'John', address: { city: 'Los Angeles', country: 'USA' } }
console.log(user.address.city); // 'New York'(原对象未改变)

3. 特殊对象展开

javascript
// 展开数组为对象(键为索引)
const arr = ["a", "b", "c"];
const obj = { ...arr };
console.log(obj); // { '0': 'a', '1': 'b', '2': 'c' }

// 展开字符串为对象(键为索引)
const str = "hello";
const obj2 = { ...str };
console.log(obj2); // { '0': 'h', '1': 'e', '2': 'l', '3': 'l', '4': 'o' }

// 展开 Set 为对象(键为索引)
const set = new Set(["a", "b", "c"]);
const obj3 = { ...set };
console.log(obj3); // { '0': 'a', '1': 'b', '2': 'c' }

// 展开 Map 为对象(键为 Map 的键)
const map = new Map([
  ["a", 1],
  ["b", 2],
]);
const obj4 = { ...map };
console.log(obj4); // { a: 1, b: 2 }

展开语法的应用场景

1. 数组操作

javascript
// 数组操作

// 1. 合并数组
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const merged = [...arr1, ...arr2];
console.log(merged); // [1, 2, 3, 4, 5, 6]

// 2. 复制数组
const original = [1, 2, 3];
const copy = [...original];
console.log(copy); // [1, 2, 3]

// 3. 添加元素到数组开头
const arr = [3, 4, 5];
const newArr = [1, 2, ...arr];
console.log(newArr); // [1, 2, 3, 4, 5]

// 4. 添加元素到数组中间
const arr = [1, 4, 5];
const newArr = [1, 2, 3, ...arr.slice(1)];
console.log(newArr); // [1, 2, 3, 4, 5]

// 5. 转换可迭代对象为数组
const str = "hello";
const chars = [...str];
console.log(chars); // ['h', 'e', 'l', 'l', 'o']

const set = new Set([1, 2, 3, 3, 4]);
const setArray = [...set];
console.log(setArray); // [1, 2, 3, 4]

const map = new Map([
  ["a", 1],
  ["b", 2],
]);
const mapArray = [...map];
console.log(mapArray); // [['a', 1], ['b', 2]]

2. 对象操作

javascript
// 对象操作

// 1. 合并对象
const obj1 = { a: 1, b: 2 };
const obj2 = { c: 3, d: 4 };
const merged = { ...obj1, ...obj2 };
console.log(merged); // { a: 1, b: 2, c: 3, d: 4 }

// 2. 复制对象
const original = { a: 1, b: 2 };
const copy = { ...original };
console.log(copy); // { a: 1, b: 2 }

// 3. 更新对象属性
const user = { name: "John", age: 30 };
const updatedUser = { ...user, age: 31 };
console.log(updatedUser); // { name: 'John', age: 31 }

// 4. 添加新属性
const user = { name: "John" };
const userWithAge = { ...user, age: 30 };
console.log(userWithAge); // { name: 'John', age: 30 }

// 5. 覆盖属性
const defaultConfig = { theme: "light", fontSize: 16 };
const userConfig = { theme: "dark" };
const finalConfig = { ...defaultConfig, ...userConfig };
console.log(finalConfig); // { theme: 'dark', fontSize: 16 }

// 6. 解构和展开结合
const { a, ...rest } = { a: 1, b: 2, c: 3 };
console.log(a); // 1
console.log(rest); // { b: 2, c: 3 }

3. 函数调用

javascript
// 函数调用

// 1. 传递数组作为参数
function sum(a, b, c) {
  return a + b + c;
}

const numbers = [1, 2, 3];
console.log(sum(...numbers)); // 6

// 2. 传递可变数量的参数
function max(...numbers) {
  return Math.max(...numbers);
}

console.log(max(1, 5, 3, 9, 2)); // 9

// 3. 与解构结合
function createUser({ name, age, ...rest }) {
  return {
    name,
    age,
    ...rest,
    createdAt: new Date(),
  };
}

const userData = { name: "John", age: 30, city: "New York" };
const user = createUser(userData);
console.log(user); // { name: 'John', age: 30, city: 'New York', createdAt: Date }

// 4. 构造函数调用
const dateParts = [2023, 11, 1]; // 年, 月(0-11), 日
const date = new Date(...dateParts);
console.log(date); // 2023-12-01T00:00:00.000Z

4. 函数返回值

javascript
// 函数返回值

// 1. 返回多个值
function getUser() {
  return { name: "John", age: 30 };
}

const user = { ...getUser(), city: "New York" };
console.log(user); // { name: 'John', age: 30, city: 'New York' }

// 2. 合并返回值
function getDefaultConfig() {
  return { theme: "light", fontSize: 16 };
}

function getUserConfig() {
  return { theme: "dark" };
}

const config = { ...getDefaultConfig(), ...getUserConfig() };
console.log(config); // { theme: 'dark', fontSize: 16 }

5. 其他应用

javascript
// 其他应用

// 1. 数组去重
const arr = [1, 2, 3, 3, 4, 5, 5];
const unique = [...new Set(arr)];
console.log(unique); // [1, 2, 3, 4, 5]

// 2. 字符串反转
const str = "hello";
const reversed = [...str].reverse().join("");
console.log(reversed); // 'olleh'

// 3. 生成随机数组
const randomNumbers = [...Array(5)].map(() => Math.floor(Math.random() * 100));
console.log(randomNumbers); // [随机数, 随机数, 随机数, 随机数, 随机数]

// 4. 构建配置对象
const baseConfig = { env: "production", debug: false };
const featureConfig = { feature1: true, feature2: false };
const finalConfig = { ...baseConfig, ...featureConfig };
console.log(finalConfig); // { env: 'production', debug: false, feature1: true, feature2: false }

// 5. 展开运算符与解构赋值结合
const [first, ...rest] = [1, 2, 3, 4, 5];
console.log(first); // 1
console.log(rest); // [2, 3, 4, 5]

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

展开语法的注意事项

1. 浅拷贝

展开语法执行的是浅拷贝,对于嵌套对象,只会复制引用,而不会复制嵌套对象本身。

javascript
// 浅拷贝示例
const user = {
  name: "John",
  address: {
    city: "New York",
    country: "USA",
  },
};

const copy = { ...user };
copy.address.city = "Los Angeles";
console.log(user.address.city); // 'Los Angeles'(原对象也被修改了)

// 深拷贝示例
const deepCopy = {
  ...user,
  address: {
    ...user.address,
  },
};
deepCopy.address.city = "Chicago";
console.log(user.address.city); // 'Los Angeles'(原对象未被修改)
console.log(deepCopy.address.city); // 'Chicago'

2. 展开顺序

当展开多个对象时,后面的对象会覆盖前面的对象的同名属性。

javascript
// 展开顺序
const obj1 = { a: 1, b: 2 };
const obj2 = { b: 3, c: 4 };
const obj3 = { ...obj1, ...obj2 };
console.log(obj3); // { a: 1, b: 3, c: 4 }(obj2 的 b 覆盖了 obj1 的 b)

// 示例:默认值与用户配置
const defaultConfig = { theme: "light", fontSize: 16 };
const userConfig = { theme: "dark" };
const finalConfig = { ...defaultConfig, ...userConfig };
console.log(finalConfig); // { theme: 'dark', fontSize: 16 }(用户配置覆盖默认配置)

3. 可迭代对象

数组展开语法只适用于可迭代对象(如数组、字符串、Set、Map 等),对于普通对象,需要使用对象展开语法。

javascript
// 可迭代对象
const str = "hello";
const arr = [...str];
console.log(arr); // ['h', 'e', 'l', 'l', 'o']

const set = new Set([1, 2, 3]);
const setArr = [...set];
console.log(setArr); // [1, 2, 3]

// 普通对象需要使用对象展开语法
const obj = { a: 1, b: 2 };
const objCopy = { ...obj };
console.log(objCopy); // { a: 1, b: 2 }

// 错误:尝试使用数组展开语法展开普通对象
// const objArr = [...obj]; // TypeError: obj is not iterable

4. 空对象和数组

展开空对象或数组不会产生错误。

javascript
// 空对象和数组
const obj1 = { a: 1 };
const obj2 = {};
const mergedObj = { ...obj1, ...obj2 };
console.log(mergedObj); // { a: 1 }

const arr1 = [1, 2];
const arr2 = [];
const mergedArr = [...arr1, ...arr2];
console.log(mergedArr); // [1, 2]

5. undefined 和 null

展开 undefined 或 null 会抛出错误,除非在对象展开中。

javascript
// undefined 和 null

// 错误:尝试展开 undefined 或 null
// const arr = [...undefined]; // TypeError: undefined is not iterable
// const arr = [...null]; // TypeError: null is not iterable

// 但在对象展开中,展开 undefined 或 null 不会产生错误,也不会添加任何属性
const obj1 = { a: 1 };
const obj2 = undefined;
const obj3 = null;
const merged = { ...obj1, ...obj2, ...obj3 };
console.log(merged); // { a: 1 }

展开语法的兼容性

1. 浏览器兼容性

特性ChromeFirefoxSafariEdgeIE
数组展开语法46+36+9+12+不支持
对象展开语法60+55+11.1+16+不支持

2. 转译工具

为了在旧版本浏览器中使用展开语法,可以使用转译工具如 Babel 进行转译。

javascript
// Babel 配置示例 (.babelrc)
{
  "presets": ["@babel/preset-env"]
}

3. 替代方案

在不支持展开语法的环境中,可以使用以下替代方案:

数组展开替代方案

javascript
// 数组合并
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];

// 替代方案:concat
const merged = arr1.concat(arr2);
console.log(merged); // [1, 2, 3, 4, 5, 6]

// 数组复制
const original = [1, 2, 3];

// 替代方案:slice
const copy = original.slice();
console.log(copy); // [1, 2, 3]

// 函数参数展开
function sum(a, b, c) {
  return a + b + c;
}

const numbers = [1, 2, 3];

// 替代方案:apply
sum.apply(null, numbers); // 6

对象展开替代方案

javascript
// 对象合并
const obj1 = { a: 1, b: 2 };
const obj2 = { c: 3, d: 4 };

// 替代方案:Object.assign
const merged = Object.assign({}, obj1, obj2);
console.log(merged); // { a: 1, b: 2, c: 3, d: 4 }

// 对象复制
const original = { a: 1, b: 2 };

// 替代方案:Object.assign
const copy = Object.assign({}, original);
console.log(copy); // { a: 1, b: 2 }

// 对象更新
const user = { name: "John", age: 30 };

// 替代方案:Object.assign
const updated = Object.assign({}, user, { age: 31 });
console.log(updated); // { name: 'John', age: 31 }

总结

展开语法是 ES6 引入的一种强大的语法特性,它使用三个点 ... 来表示将一个可迭代对象展开为多个元素,或将一个对象展开为多个键值对。展开语法的主要特点包括:

  1. 数组展开:可以将数组、字符串、Set、Map 等可迭代对象展开为多个元素
  2. 对象展开:可以将对象展开为多个键值对
  3. 函数参数展开:可以将数组展开为函数的多个参数
  4. 剩余参数:可以将多个参数收集为一个数组
  5. 浅拷贝:执行的是浅拷贝,对于嵌套对象只会复制引用

展开语法的应用场景非常广泛,包括:

  • 数组合并、复制、添加元素
  • 对象合并、复制、更新属性
  • 函数调用时传递参数
  • 转换可迭代对象为数组
  • 与解构赋值结合使用

通过合理使用展开语法,我们可以编写更加简洁、可读和维护性更高的 JavaScript 代码。