返回首页

ES6中数组新增了哪些扩展

问题解析

ES6 对数组进行了大量增强,提供了更强大的数组操作能力。这个问题考察对 ES6 数组新特性的全面掌握程度,包括扩展运算符、新的静态方法、实例方法等。

核心概念

1. 扩展运算符(...)

  • 复制数组
  • 合并数组
  • 解构赋值
  • 与字符串/类数组的转换

2. 静态方法

  • Array.from():类数组/可迭代对象转数组
  • Array.of():创建数组,解决 Array() 的参数歧义

3. 实例方法

  • copyWithin():数组内部复制
  • find()/findIndex():查找元素
  • fill():填充数组
  • entries()/keys()/values():遍历器方法
  • includes():判断是否包含某值
  • flat()/flatMap():数组扁平化

4. 其他改进

  • 数组空位处理规范化
  • 排序稳定性保证

详细解答

1. 扩展运算符(...)

复制数组

const arr1 = [1, 2, 3];
const arr2 = [...arr1]; // 浅拷贝

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

// 对比 ES5
const arr3 = arr1.concat();
const arr4 = arr1.slice();

合并数组

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

// ES6
const merged = [...arr1, ...arr2, ...arr3];
console.log(merged); // [1, 2, 3, 4, 5, 6]

// ES5
const mergedES5 = arr1.concat(arr2, arr3);

解构赋值

const [first, ...rest] = [1, 2, 3, 4, 5];
console.log(first); // 1
console.log(rest);  // [2, 3, 4, 5]

// 与解构结合提取部分元素
const [a, b, ...others] = ['a', 'b', 'c', 'd', 'e'];
console.log(others); // ['c', 'd', 'e']

// 注意:扩展运算符必须是最后一个参数
const [x, ...y, z] = [1, 2, 3, 4]; // SyntaxError

与字符串/类数组的转换

// 字符串转数组
const chars = [...'hello'];
console.log(chars); // ['h', 'e', 'l', 'l', 'o']

// 类数组转数组
const nodeList = document.querySelectorAll('div');
const nodeArray = [...nodeList];

// Set 转数组
const set = new Set([1, 2, 3, 3, 3]);
const unique = [...set];
console.log(unique); // [1, 2, 3]

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

2. Array.from()

将类数组对象或可迭代对象转为真正的数组。

// 类数组对象
const arrayLike = {
  0: 'a',
  1: 'b',
  2: 'c',
  length: 3
};
const arr = Array.from(arrayLike);
console.log(arr); // ['a', 'b', 'c']

// NodeList
const divs = Array.from(document.querySelectorAll('div'));

// 字符串
const chars = Array.from('hello'); // ['h', 'e', 'l', 'l', 'o']

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

第二个参数:mapFn

// 类似于 map 方法
const numbers = Array.from([1, 2, 3], x => x * 2);
console.log(numbers); // [2, 4, 6]

// 生成序列
const range = Array.from({ length: 5 }, (_, i) => i);
console.log(range); // [0, 1, 2, 3, 4]

// 替代 new Array().fill().map()
const squares = Array.from({ length: 5 }, (_, i) => i * i);
console.log(squares); // [0, 1, 4, 9, 16]

第三个参数:thisArg

const doubler = {
  factor: 2,
  multiply(x) {
    return x * this.factor;
  }
};

const numbers = [1, 2, 3];
const doubled = Array.from(numbers, doubler.multiply, doubler);
console.log(doubled); // [2, 4, 6]

3. Array.of()

解决 Array() 构造函数的参数歧义问题。

// Array() 的问题
Array();        // []
Array(3);       // [empty × 3] - 创建长度为3的空数组
Array(3, 4, 5); // [3, 4, 5]

// Array.of() 行为一致
Array.of();        // []
Array.of(3);       // [3]
Array.of(3, 4, 5); // [3, 4, 5]

// 使用场景:创建包含单个数字元素的数组
const single = Array.of(5); // [5]
const emptyLength5 = Array(5); // [empty × 5]

4. copyWithin()

在数组内部将指定位置的成员复制到其他位置(会覆盖原有成员)。

// 语法:arr.copyWithin(target, start = 0, end = this.length)

// 将索引3到结束的元素复制到索引0开始的位置
[1, 2, 3, 4, 5].copyWithin(0, 3);
// [4, 5, 3, 4, 5]

// 将索引3到4的元素复制到索引0开始的位置
[1, 2, 3, 4, 5].copyWithin(0, 3, 4);
// [4, 2, 3, 4, 5]

// 负数索引
[1, 2, 3, 4, 5].copyWithin(-2, -3, -1);
// [1, 2, 3, 3, 4]

// 实际应用:数组旋转
const rotate = (arr, k) => {
  const n = arr.length;
  k = k % n;
  return [...arr.slice(-k), ...arr.slice(0, -k)];
};

5. find() 和 findIndex()

// find():返回第一个满足条件的元素
const users = [
  { id: 1, name: 'Alice' },
  { id: 2, name: 'Bob' },
  { id: 3, name: 'Charlie' }
];

const user = users.find(u => u.id === 2);
console.log(user); // { id: 2, name: 'Bob' }

// 找不到返回 undefined
const notFound = users.find(u => u.id === 999);
console.log(notFound); // undefined

// findIndex():返回第一个满足条件的索引
const index = users.findIndex(u => u.id === 2);
console.log(index); // 1

// 找不到返回 -1
const notFoundIndex = users.findIndex(u => u.id === 999);
console.log(notFoundIndex); // -1

// 对比 filter(返回所有匹配项)
const adults = users.filter(u => u.id > 1);
console.log(adults); // [{ id: 2, ... }, { id: 3, ... }]

6. fill()

用给定值填充数组。

// 填充整个数组
new Array(5).fill(0);
// [0, 0, 0, 0, 0]

// 指定起始和结束位置
[1, 2, 3, 4, 5].fill('x', 1, 3);
// [1, 'x', 'x', 4, 5]

// 填充对象时的引用问题
const arr = new Array(3).fill({});
arr[0].name = 'Alice';
console.log(arr); // [{ name: 'Alice' }, { name: 'Alice' }, { name: 'Alice' }]
// 三个元素引用同一个对象

// 正确做法:使用 Array.from()
const arr2 = Array.from({ length: 3 }, () => ({}));
arr2[0].name = 'Alice';
console.log(arr2); // [{ name: 'Alice' }, {}, {}]

7. entries()、keys()、values()

返回遍历器对象,可用 for...of 遍历。

const arr = ['a', 'b', 'c'];

// keys():索引遍历器
for (const index of arr.keys()) {
  console.log(index); // 0, 1, 2
}

// values():值遍历器
for (const value of arr.values()) {
  console.log(value); // 'a', 'b', 'c'
}

// entries():键值对遍历器
for (const [index, value] of arr.entries()) {
  console.log(index, value);
  // 0 'a'
  // 1 'b'
  // 2 'c'
}

// 使用解构和 entries 获取索引和值
arr.entries().forEach(([i, v]) => console.log(i, v));

8. includes()

判断数组是否包含给定值。

// 基本用法
[1, 2, 3].includes(2);     // true
[1, 2, 3].includes(4);     // false

// 从指定位置开始查找
[1, 2, 3, 2].includes(2, 2); // true(从索引2开始查找)
[1, 2, 3].includes(2, 3);    // false

// 与 indexOf 的区别
// 1. includes 使用 SameValueZero 算法,可以正确判断 NaN
[NaN].includes(NaN);        // true
[NaN].indexOf(NaN);         // -1

// 2. includes 更简洁,不需要比较 -1
if (arr.includes(item)) { }  // 清晰
if (arr.indexOf(item) !== -1) { }  // 繁琐

// 3. includes 不适用于对象引用比较(与 indexOf 相同)
const obj = { a: 1 };
[{ a: 1 }].includes({ a: 1 }); // false
[obj].includes(obj);            // true

9. flat() 和 flatMap()

flat() - 数组扁平化

// 基本扁平化
[1, 2, [3, 4]].flat();
// [1, 2, 3, 4]

// 指定深度
[1, [2, [3, [4]]]].flat(2);
// [1, 2, 3, [4]]

// 完全扁平化(Infinity)
[1, [2, [3, [4]]]].flat(Infinity);
// [1, 2, 3, 4]

// 移除空位
[1, 2, , 4, 5].flat();
// [1, 2, 4, 5]

// 实际应用:处理嵌套数据
const comments = [
  { id: 1, replies: [{ id: 2 }, { id: 3 }] },
  { id: 4, replies: [{ id: 5 }] }
];
const allReplies = comments.map(c => c.replies).flat();
// [{ id: 2 }, { id: 3 }, { id: 5 }]

flatMap() - map + flat(1)

// 等价于 arr.map(...).flat(1),但更高效
const sentences = ['Hello world', 'Good morning'];
const words = sentences.flatMap(s => s.split(' '));
// ['Hello', 'world', 'Good', 'morning']

// 对比
const words2 = sentences.map(s => s.split(' ')).flat();
// 结果相同,但 flatMap 只遍历一次

// 过滤并映射
const numbers = [1, 2, 3, 4];
const evensDoubled = numbers.flatMap(n => n % 2 === 0 ? [n * 2] : []);
// [4, 8](过滤奇数,偶数翻倍)

// 一对多映射
const duplicates = [1, 2, 3].flatMap(x => [x, x]);
// [1, 1, 2, 2, 3, 3]

10. 数组空位处理

ES6 规范化了数组空位的处理,将空位转为 undefined

// ES5 中空位处理不一致
// forEach, map, filter, every, some 会跳过空位
// join, toString 会将空位视为 undefined 或空字符串

// ES6 中,空位转为 undefined
Array.from([, , ,]);           // [undefined, undefined, undefined]
[...['a', , 'b']];             // ['a', undefined, 'b']
Array.of(...[, , ,]);          // [undefined, undefined, undefined]

// 注意:部分旧方法仍跳过空位
[,'a'].forEach((x, i) => console.log(i)); // 只输出 1

// 使用 fill 处理空位
new Array(3).fill(0); // [0, 0, 0]

11. 排序稳定性

ES2019 规定 sort() 必须是稳定排序。

// 稳定排序:相等元素的相对顺序保持不变
const items = [
  { name: 'Edward', value: 21 },
  { name: 'Sharpe', value: 37 },
  { name: 'And', value: 45 },
  { name: 'The', value: -12 },
  { name: 'Magnetic', value: 21 },
  { name: 'Zeros', value: 37 }
];

// 按 value 排序
items.sort((a, b) => a.value - b.value);

// 稳定排序保证 value 相同的元素保持原有顺序
// Edward (21) 在 Magnetic (21) 之前
// Sharpe (37) 在 Zeros (37) 之前

深入理解

1. 扩展运算符的实现原理

扩展运算符内部使用迭代器协议(Iterator Protocol):

// 可迭代对象必须实现 Symbol.iterator
const iterable = {
  [Symbol.iterator]() {
    let i = 0;
    return {
      next() {
        if (i < 3) {
          return { value: i++, done: false };
        }
        return { done: true };
      }
    };
  }
};

const arr = [...iterable];
console.log(arr); // [0, 1, 2]

2. Array.from 的性能优势

// 方式1:扩展运算符(需要实现迭代器)
const arr1 = [...set]; // 需要遍历迭代器

// 方式2:Array.from(针对类数组优化)
const arr2 = Array.from(set); // 可能更快,内部优化

// 方式3:解构赋值
const arr3 = Array.prototype.slice.call(arrayLike);

Array.from 针对类数组对象进行了优化,在某些场景下性能更好。

3. flatMap 的应用模式

// 模式1:过滤并映射
const files = ['a.txt', 'b.jpg', 'c.png', 'd.txt'];
const images = files.flatMap(f => f.endsWith('.jpg') || f.endsWith('.png') ? [f] : []);

// 模式2:一对多映射
const ranges = [[1, 3], [5, 7]];
const numbers = ranges.flatMap(([start, end]) => {
  const result = [];
  for (let i = start; i <= end; i++) result.push(i);
  return result;
});
// [1, 2, 3, 5, 6, 7]

// 模式3:添加/删除元素
const updates = [
  { type: 'add', value: 1 },
  { type: 'remove', value: 2 },
  { type: 'add', value: 3 }
];
const applyUpdate = (arr, update) => {
  if (update.type === 'add') return [update.value];
  if (update.type === 'remove') {
    const idx = arr.indexOf(update.value);
    return idx > -1 ? [,] : []; // 返回空数组表示删除
  }
};

4. 类数组对象的精确判断

// 类数组对象:有 length 属性且为非负整数
function isArrayLike(obj) {
  if (!obj || typeof obj !== 'object') return false;
  const length = obj.length;
  return typeof length === 'number' &&
         length >= 0 &&
         length === Math.floor(length) &&
         length < Number.MAX_SAFE_INTEGER;
}

// 例子
isArrayLike({ 0: 'a', 1: 'b', length: 2 }); // true
isArrayLike('abc'); // true(字符串有 length)
isArrayLike({ length: -1 }); // false
isArrayLike({ length: 1.5 }); // false

最佳实践

1. 数组复制

// 浅拷贝数组
const copy = [...original];
// 或
const copy2 = Array.from(original);
// 或
const copy3 = original.slice();

// 深拷贝(简单对象)
const deepCopy = JSON.parse(JSON.stringify(arr));

// 深拷贝(复杂场景使用库)
import _ from 'lodash';
const deepCopy2 = _.cloneDeep(arr);

2. 数组去重

// 使用 Set(最简单)
const unique = [...new Set(arr)];

// 对象数组去重
const uniqueById = Array.from(new Map(arr.map(item => [item.id, item])).values());

3. 生成序列

// 生成 0 到 n-1
const range = n => Array.from({ length: n }, (_, i) => i);

// 生成 start 到 end
const range2 = (start, end) => Array.from({ length: end - start }, (_, i) => start + i);

// 生成随机数组
const randomArray = (length, max) =>
  Array.from({ length }, () => Math.floor(Math.random() * max));

4. 数组分组

// 使用 reduce 分组
const groupBy = (arr, key) =>
  arr.reduce((acc, item) => {
    const group = item[key];
    acc[group] = acc[group] || [];
    acc[group].push(item);
    return acc;
  }, {});

// 使用 flatMap 展开分组结果
const flattenGroups = grouped => Object.values(grouped).flat();

5. 链式操作

const result = arr
  .filter(x => x > 0)
  .map(x => x * 2)
  .flatMap(x => [x, x + 1])
  .slice(0, 10);

面试要点

  1. 扩展运算符与剩余参数的区别

    • 扩展运算符:... 在等号右边或函数调用时,展开数组
    • 剩余参数:... 在等号左边或函数参数时,收集剩余元素
  2. Array.from 与扩展运算符的选择

    • 类数组对象优先使用 Array.from
    • 可迭代对象两者皆可
    • 需要映射时使用 Array.from(arr, mapFn)
  3. find vs filter

    • find:找到第一个匹配项就返回,性能更好
    • filter:返回所有匹配项,返回数组
  4. includes vs indexOf

    • includes 可以正确判断 NaN
    • includes 返回布尔值,更语义化
  5. flatMap 的优势

    • map().flat() 更高效(只遍历一次)
    • 适合过滤+映射、一对多映射场景
  6. 数组空位的处理

    • ES6 新方法将空位视为 undefined
    • 旧方法(如 forEach)会跳过空位
    • 建议避免使用空位,显式使用 undefined
  7. 排序稳定性

    • ES2019 起 sort() 保证稳定
    • 稳定排序在多字段排序时很重要