Skip to content

函数新增特性与数组新增特性(Day2 3/4) ​

本页对应 Day02_ES6,共 5 段视频,约 1 小时 2 分。

学习目标 ​

  1. 能使用扩展运算符将数组展开为逗号分隔的参数序列,并说明其与 rest 参数在“展开”和“合并”方向上的区别。
  2. 能使用扩展运算符完成数组复制、多个数组合并,以及将类数组/可迭代对象转换为纯数组,并判断操作是否创建了新数组。
  3. 能在数组解构赋值中正确使用扩展运算符收集剩余元素,并说明其必须位于解构变量末尾的原因。
  4. 能区分 Array.of() 与 Array.from() 的作用,分别用它们创建新数组、将类数组或可迭代对象转换为纯数组。
  5. 能使用 find()、findIndex()、fill() 完成按条件查找元素、查找索引和填充/替换数组元素,并判断 fill() 是否会修改原数组。
  6. 能使用 flat()、flatMap()、includes()、at() 完成数组扁平化、映射后扁平化、包含性判断和按索引(含负数索引)读取元素,并解释各自返回值特征。

知识点 ​

扩展运算符:把数组拆成参数序列 ​

为什么需要:当函数有多个形参,而我们手里只有一个数组时,直接传数组只会占用第一个形参,后面的形参都是 undefined。
怎么想:把数组“拆开”,变成逗号分隔的参数序列,再传给函数。
怎么做:

js
const names = [100, 200, 300, 400];
function func(n1, n2, n3, n4) {
  console.log(n1, n2, n3, n4);
}
func(names);     // n1 是数组,n2~n4 是 undefined
func(...names);  // 100 200 300 400

易错点:三个点要写在数组前;func(...names) 等价于 func(100,200,300,400),不是把数组当整体传入。

复制数组:别把“别名”当复制 ​

为什么需要:const arr202 = arr201 只复制了地址,两个变量指向同一个数组,改一个另一个也变。
怎么想:要得到新数组,就让 [...arr201] 先把元素拆成参数序列,再放进新数组。
怎么做:

js
const arr201 = [100, 200, 300, 400, 500];
const arr202 = [...arr201];
arr202[2] = '刘姥姥';
console.log(arr201); // 不受影响

易错点:直接赋值是引用传递;[...arr201] 是浅拷贝,元素若是对象,对象内部仍共享引用。

合并数组与类数组转纯数组 ​

为什么需要:多个数组合并、querySelectorAll 得到的类数组想用数组方法,都需要转成纯数组。
怎么想:扩展运算符能把数组或可迭代对象拆开,再放进 [] 里。
怎么做:

js
const arr203 = [...names, ...arr201, '刘姥姥'];

const liBooks = document.querySelectorAll('.news li');
const arr204 = [...liBooks];
const arr205 = [...'hello'];

易错点:不加点会把整个数组当元素;类数组、字符串等可迭代对象可以拆,普通对象不行。

rest 参数与解构赋值中的扩展 ​

为什么需要:函数接收不定数量参数,或解构时把剩余值收集起来。
怎么想:rest 参数是“把参数序列合并到数组”,扩展运算符在调用时是“把数组拆成参数序列”,二者方向相反。
怎么做:

js
function func(...args) {
  console.log(args);
}
func(1, 2, 3, 4, 5);

const [a1, a2, ...a3] = ['刘姥姥', '马姥姥', '司马姥姥', '欧阳姥姥', '爱新觉罗姥姥'];

易错点:rest 参数只能放形参最后;解构中的 ... 也要放最后,否则不合理。

Array.of 与 Array.from ​

为什么需要:new Array(12) 会得到长度 12 的空数组,而不是元素 12;类数组转纯数组也需要专门方法。
怎么想:Array.of 让参数直接成为元素;Array.from 从可迭代对象或类数组创建新数组。
怎么做:

js
Array.of(12);        // [12]
Array.of(1, 2, 3);   // [1,2,3]

const liBooks = document.querySelectorAll('.news li');
const arr = Array.from(liBooks);

易错点:Array.of(12) 与 new Array(12) 不同;Array.from 是浅拷贝。

find/findIndex/fill ​

为什么需要:查找满足条件的第一个元素或其索引;批量填充/替换数组值。
怎么想:find、findIndex 接收回调,返回 true 表示当前元素符合条件;fill 用固定值替换元素。
怎么做:

js
users.find(item => item.age >= 45);
users.findIndex(item => item.age >= 45);

users.fill(250);
users.fill(150, 1, 3); // 从索引1到3,顾头不顾尾

易错点:find 返回元素本身,findIndex 返回索引;fill 是修改器方法,会改原数组。

flat/flatMap/includes/at ​

为什么需要:多维数组扁平化、判断包含、按索引读取(支持负数)。
怎么想:flat 默认只拆一层,可用 Infinity 彻底扁平;includes 返回布尔值;at(-1) 取倒数第一个。
怎么做:

js
nameList.flat(Infinity);
numbers.includes(300);
numbers.includes(300, 3);
numbers.at(-1);

易错点:flat() 默认只拆一层;flatMap 等价于先 map 再深度为 1 的 flat;at 的负索引是倒数,不是普通下标。

扩展运算符:把数组拆成参数序列 ​

为什么需要:当函数有多个形参,而我们手里只有一个数组时,直接传数组只会占住第一个形参,后面的形参拿不到值。
怎么想:扩展运算符 ... 写在数组前,相当于把数组“拆开”,变成逗号分隔的参数序列;它和 rest 参数方向相反,rest 是“合”,扩展是“拆”。
怎么做:

js
const numbers = [10, 20, 30, 40];
function func(n1, n2, n3, n4) {
  console.log(n1, n2, n3, n4);
}
func(...numbers); // 10 20 30 40

调试时先写 func(numbers),会发现只有 n1 得到整个数组,n2 到 n4 是 undefined;加上 ... 后四个形参分别拿到元素。
易错点:... 不是数组方法,不能写成 numbers...;它要写在数组或可迭代对象前面。直接量也可以:func(...["刘姥姥", "马姥姥", "欧阳姥姥"])。

用扩展运算符复制数组 ​

为什么需要:const arr2 = arr1 只是把地址赋给另一个变量,两个变量指向同一个数组,改一个会影响另一个。
怎么想:要真正复制,需要创建一个新数组,再把原数组元素展开进去。
怎么做:

js
const arr1 = [100, 200, 300, 400, 500];
const arr2 = [...arr1];
arr2[2] = "刘姥姥";
console.log(arr1); // [100, 200, 300, 400, 500]
console.log(arr2); // [100, 200, "刘姥姥", 400, 500]

调试时先写 const arr2 = arr1,修改 arr2[2] 后 arr1 也变,说明二者是同一份数据;改成 [...arr1] 后互不影响。
易错点:这是浅拷贝。元素如果是对象,对象内部仍可能共享引用;不要把它理解成深拷贝。

合并数组与类数组转纯数组 ​

为什么需要:合并多个数组、把 querySelectorAll 得到的类数组变成真正数组,都是常见需求。
怎么想:扩展运算符可以把多个数组展开到新数组中;可迭代对象/类数组也能被展开。
怎么做:

js
const names = ["刘姥姥", "马姥姥"];
const arr201 = [100, 200, 300];
const merged = [...names, ...arr201, "欧阳姥姥"];

const liBooks = document.querySelectorAll(".news li");
const arr204 = [...liBooks]; // 纯数组

调试时先写 [...names, arr201],会发现 arr201 被当成一个元素;两边都加 ... 才是合并。
易错点:类数组不是所有场景都能直接展开;优先用 Array.from 或扩展运算符处理可迭代对象。

Array.of 与 Array.from ​

为什么需要:new Array(12) 会创建长度为 12 的空数组,而不是 [12],容易误解。
怎么想:Array.of 让参数直接成为数组元素;Array.from 把可迭代对象或类数组转成纯数组。
怎么做:

js
Array.of(12);        // [12]
Array.of(1, 2, 3);   // [1, 2, 3]

const lis = document.querySelectorAll("li");
const arr = Array.from(lis);
const chars = Array.from("hello"); // ["h","e","l","l","o"]

易错点:Array.of(12) 和 new Array(12) 结果不同;Array.from 是浅拷贝,不是深拷贝。

find / findIndex / fill ​

为什么需要:按条件查找第一个满足项,或批量替换数组元素。
怎么想:find 返回第一个满足条件的元素,findIndex 返回其索引;fill 用固定值替换元素。
怎么做:

js
const users = [{ name: "刘备", age: 40 }, { name: "诸葛亮", age: 46 }];
users.find(item => item.age >= 45);      // 诸葛亮
users.findIndex(item => item.age >= 45); // 1

const nums = [1, 2, 3, 4];
nums.fill(250);       // [250, 250, 250, 250]
nums.fill(150, 1, 3); // 从索引1到3,不含3

调试时注意:find 的回调要返回布尔值;不写回调会报错。
易错点:fill 是修改器方法,会改原数组;结束索引“顾头不顾尾”。

flat / flatMap / includes / at ​

为什么需要:多维数组扁平化、判断包含、按索引读取(尤其负数索引)。
怎么想:flat(n) 按层级拉平,默认一层;flatMap 等价于先 map 再 flat(1);includes 返回布尔值;at 支持负数。
怎么做:

js
const list = [1, [2, [3, [4]]]];
list.flat();          // 只拉平一层
list.flat(Infinity);  // 彻底扁平化

[1, 2, 3].includes(3); // true
[10, 20, 30].at(-1);   // 30

易错点:flat() 默认只拉平一层,不是无限层;flatMap 的扁平深度固定为一层;at(-1) 是倒数第一个,不存在 -0。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

代码示例 ​

示例一:扩展运算符的核心用法(转参数序列 / 复制 / 合并 / 类数组转纯数组) ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>扩展运算符</title>
</head>
<body>
  <ul class="news">
    <li>刘备</li>
    <li>关羽</li>
    <li>张飞</li>
  </ul>

  <script>
    /* 1. 将数组转为逗号分隔的参数序列 */
    const numbers = [100, 200, 300, 400, 500];
    function func(n1, n2, n3, n4) {
      console.log(n1, n2, n3, n4);
    }
    func(numbers);      // 100 undefined undefined undefined —— 整个数组只当第一个实参
    func(...numbers);   // 100 200 300 400 —— 相当于 func(100, 200, 300, 400)

    /* 2. 复制数组:不加点是“起别名”,加点是真复制 */
    const arr01 = [100, 200, 300, 400, 500];
    const arr02 = arr01;        // 只把地址给了 arr02,两个变量指向同一个数组
    arr02[2] = '刘姥姥';
    console.log(arr01);         // [100, 200, '刘姥姥', 400, 500] 原数组被连带修改

    const arr03 = [...arr01];   // 中括号创建新数组,再把 arr01 拆开塞进去
    arr03[2] = '司马姥姥';
    console.log(arr01);         // 不受影响,arr01 与 arr03 是两个数组

    /* 3. 合并数组 */
    const names = ['刘姥姥', '马姥姥'];
    const arr04 = [...names, ...arr01, '欧阳姥姥'];
    console.log(arr04);

    /* 4. 类数组(可迭代对象)转纯数组 */
    const liBooks = document.querySelectorAll('.news li');
    const arr05 = [...liBooks];
    console.log(Array.isArray(arr05), arr05);   // true,里面是 li 元素
    const arr06 = [...'Hello'];
    console.log(arr06);                         // ['H','e','l','l','o']
  </script>
</body>
</html>

要点解释与易错点

  • func(numbers) 与 func(...numbers) 的区别是本节最关键的一行:不加 ... 时数组是“一个实参”,形参 n2~n4 全部拿到 undefined;加了 ... 才是“把数组拆成一个个值”。
  • 扩展运算符是 rest 参数(...args)的逆运算:rest 把多个参数合并成数组,... 把数组拆成多个参数。
  • const arr02 = arr01 不是复制,只是把地址(引用)交给另一个变量,改 arr02 等于改 arr01。必须写 [...arr01] 才是真正复制一份新数组。
  • ... 只能写在数组/可迭代对象的前面,写在后面(如 arr...)是语法错误。
  • querySelectorAll 返回的是类数组(可迭代对象),不是纯数组,所以才能用 [...liBooks] 拆开后再装进新数组。

示例二:Array.of / Array.from 与解构赋值中的剩余元素 ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>Array.of / Array.from / 解构</title>
</head>
<body>
  <ul class="news">
    <li>刘备</li>
    <li>关羽</li>
    <li>张飞</li>
  </ul>

  <script>
    /* 1. Array.of:参数个数 = 新数组元素个数 */
    console.log(Array.of(12));                  // [12]     一个元素
    console.log(new Array(12));                 // 长度 12 的空数组,不是 [12]
    console.log(Array.of(12, 34, 56));          // [12, 34, 56]
    console.log(Array.of(12, [1, 2, 3], '司马姥姥')); // 数组整体作为一个元素

    /* 2. Array.from:可迭代对象 / 类数组 -> 纯数组 */
    const liBooks = document.querySelectorAll('.news li');
    const arr01 = Array.from(liBooks);
    console.log(Array.isArray(arr01), arr01);
    const arr02 = Array.from('Hello');
    console.log(arr02);                         // ['H','e','l','l','o']

    /* 3. 解构赋值中的剩余元素(rest 在左边,与扩展运算符互逆) */
    const [a1, a2, ...a3] =
      ['刘姥姥', '马姥姥', '司马姥姥', '欧阳姥姥', '爱新觉罗姥姥'];
    console.log(a1, a2, a3);
    // 刘姥姥 马姥姥 ['司马姥姥', '欧阳姥姥', '爱新觉罗姥姥']
  </script>
</body>
</html>

要点解释与易错点

  • Array.of(12) 得到 [12],而 new Array(12) 得到的是长度为 12 的空数组——这正是 Array.of 相对 new Array 做出的改进:只有一个数字参数时不再被当成“长度”。
  • Array.from 与 [...liBooks] 效果相同(都是把类数组/可迭代对象转纯数组),实际开发中任选其一,不必纠结。
  • Array.from('Hello') 说明字符串也是可迭代对象,每个字符成为一个元素。
  • 解构中的 ...a3 必须放在最后,写 [a1, ...a2, a3] 不合理也不合法(后面的元素无处分配),这和 rest 参数必须放在形参最后是同一条规则。

示例三:数组实例新增方法 find / fill / flat / includes / at ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>数组实例方法</title>
</head>
<body>
  <script>
    const users = [
      { name: '刘备',   age: 30 },
      { name: '关羽',   age: 38 },
      { name: '诸葛亮', age: 45 },
      { name: '曹操',   age: 50 }
    ];

    /* 1. find / findIndex:回调返回 true 表示“符合条件”,只找第一个 */
    console.log(users.find(item => item.age >= 45));       // 诸葛亮 这个对象
    console.log(users.findIndex(item => item.age >= 45));  // 2

    /* 2. fill:用固定值替换元素,注意它是【修改器方法】 */
    const nums = [100, 200, 300, 400, 500];
    nums.fill(250);
    console.log(nums);            // [250,250,250,250,250],原数组被改
    nums.fill(150, 1, 3);         // 从索引 1 到 3,顾头不顾尾
    console.log(nums);            // [250,150,150,250,250]
    console.log(new Array(12).fill('OK')); // 12 个 'OK',常和 new Array 搭配

    /* 3. flat / flatMap */
    const nameList = [1, [2, 3], [4, [5, [6, 7]]]];
    console.log(nameList.flat());            // 默认只拆一层
    console.log(nameList.flat(2));           // 拆两层,里面还有数组
    console.log(nameList.flat(Infinity));    // 彻底扁平化
    console.log([1, 2, 3].flatMap(item => item + 100)); // 等价 map 后 flat(1)

    /* 4. includes / at */
    console.log(nums.includes(300));      // true
    console.log(nums.includes(300, 3));   // false,从索引 3 开始找就找不到了
    console.log(nums.at(1));              // 150
    console.log(nums.at(-1));             // 250 倒数第一个
    console.log(nums.at(-3));             // 150 倒数第三个
    console.log(nums[-1]);                // undefined,中括号不能这样用
  </script>
</body>
</html>

要点解释与易错点

  • find 的回调形式和 map / filter / forEach 一致(元素、索引、数组本身),回调返回 true 即表示当前元素符合条件;find 返回元素本身,findIndex 返回索引,且只返回第一个匹配项。
  • fill 是修改器方法,会直接改掉调用它的数组本身,不是返回新数组;区间 fill(值, 起始, 结束) 是“顾头不顾尾”(包含起始、不包含结束)。
  • flat() 默认只拆一层(默认参数是 1),要彻底扁平化写 flat(Infinity);flatMap 等价于先 map 再 flat(1),深度固定为一层。
  • includes 返回布尔值,第二个参数是起始查找位置,索引之前的元素不参与查找;at 的亮点是支持负数索引(at(-1) 取最后一个),而 nums[-1] 只会得到 undefined。

常见错误 ​

  1. 现象:fn(nums) 后第一个形参拿到整个数组,其余形参为 undefined。→ 原因:数组被当作单个实参传入。→ 修法:改为 fn(...nums)。
  2. 现象:const b = a; b[0] = 1; 后原数组 a 也变了。→ 原因:数组是引用类型,= 只复制地址。→ 修法:用 const b = [...a] 或 Array.from(a) 做浅拷贝。
  3. 现象:const [a, ...rest, b] = arr; 报语法错误。→ 原因:解构中的 rest 元素必须位于末尾。→ 修法:改为 const [a, b, ...rest] = arr;。
  4. 现象:new Array(3) 得到长度为 3 的空数组,不是 [3]。→ 原因:new Array 单数字参数有长度歧义。→ 修法:需要元素时用 Array.of(3)。
  5. 现象:对 querySelectorAll 结果直接调用 map 报错。→ 原因:NodeList 是类数组,不是纯数组。→ 修法:先 [...nodeList] 或 Array.from(nodeList)。
  6. 现象:arr.fill(0) 后原数组被改,误以为返回新数组。→ 原因:fill 是修改器方法。→ 修法:保留原数组时用 [...arr].fill(0),或明确接受原地修改。

练习 ​

  1. 改错:function sum(a, b, c) {};const nums = [1, 2, 3]; sum(nums); 要求让 a/b/c 分别得到 1/2/3。
  2. 用扩展运算符完成:真复制 [1, 2],与 [3, 4] 合并,并在末尾追加 5;不得修改原数组。
  3. 给定 users,用 find 找第一个 age >= 45 的元素,用 findIndex 找其索引,用 at(-1) 取最后一项;写出关键代码。
  4. 将 document.querySelectorAll('li') 转为纯数组,并分别用扩展运算符、Array.from 两种写法实现。
  5. 对多层数组 [1, [2, [3, [4]]]] 做彻底扁平化;再复制一份并用 fill 填充索引 1 到 3(不含 3),要求原数组不变。

原视频对照 ​

#视频时长
1扩展运算符-将数组转为参数序列10:00
2扩展运算符-将数组转为参数序列0208:26
3扩展运算符-将多个参数序列合并到数组中05:42
4Array构造函数本身的属性方法06:50
5数组对象的属性方法31:58

小结 ​

  • 扩展运算符负责“展开”,rest 参数负责“收集”,二者语义相反且 rest 必须在末尾。
  • 数组复制、合并优先用扩展运算符生成新数组,避免引用赋值带来的联动修改。
  • Array.of 解决 new Array 单数字参数歧义;Array.from 适合类数组/可迭代对象转纯数组。
  • find/findIndex 依赖回调返回真值定位第一个匹配项,不传回调会报错。
  • fill 是修改器;flat 默认只扁平一层;at 支持负索引。
  • 类数组不等于数组,先转纯数组再调用数组高阶方法。
  • 选用数组 API 前先确认是否修改原数组,避免副作用。