JS使用Array.from和Array.of创建数组的方法
深入解析JavaScript中Array.of与Array.from创建数组的方法,涵盖传统Array构造函数的参数陷阱、类数组与可迭代对象转换、内置映射管道与实战应用技巧。
[] 是构造数组最常用的直观手段,但在处理动态参数和集合类型转换时,传统方式存在显著短板:new Array() 构造函数存在严重的参数重载歧义,而 DOM NodeList、函数 arguments 等类数组对象又缺乏原生数组方法。针对这两类具体痛点,ES6 正式引入了 Array.of() 与 Array.from() 两个静态方法。前者旨在实现确定性的参数装箱,彻底消除构造歧义;后者则提供了一站式的类数组及可迭代对象转换流水线,并支持单次遍历的原地映射。
一、Array.of():解决构造函数的参数重载歧义
在 ES6 之前,通过构造函数 new Array() 动态生成数组时,存在一个长期困扰开发者的设计缺陷:函数的行为会根据传入参数的数量和类型发生突变。
1. 传统 Array 构造函数的行为缺陷
当向 Array() 传入多个参数时,它会将所有参数作为新数组的元素;但当只传入一个单一数值参数时,它并不会生成包含该数值的数组,而是将其解释为数组的预设长度(length),并在内存中生成一串稀疏的“空槽”(empty slots / holes)。
// 传统 Array() 构造函数的怪异行为
const arr1 = new Array(3); // 期望得到 [3],实际输出: [ <3 empty items> ]
const arr2 = new Array(1, 2); // 输出: [1, 2]
const arr3 = new Array('3'); // 输出: ['3']
// 稀疏空槽无法被正常高阶函数迭代
arr1.forEach(item => console.log(item)); // 完全无输出,循环直接跳过空槽!
const mapped = arr1.map(() => 1); // 输出: [ <3 empty items> ],回调根本未执行
这种不一致性在编写通用工具函数或高阶组件时极易引发偶发性 Bug。如果传入参数是通过变量动态计算或解构注入的,开发者无法预料传入的单一变量何时会是一个数字,从而导致数组结构被破坏。
2. Array.of() 的统一规范语法
Array.of() 的设计理念非常纯粹:无论传入几个参数、无论参数是何种数据类型,均一律作为生成的数组元素依次排列。
语法定义:Array.of(element0[, element1[, ...[, elementN]]])
// Array.of() 表现出高度的确定性与一致性
const of1 = Array.of(3); // 输出示例: [3],长度为 1,索引 0 为数字 3
const of2 = Array.of(1, 2, 3); // 输出示例: [1, 2, 3]
const of3 = Array.of(undefined);// 输出示例: [undefined],非空槽,具备真实索引
const of4 = Array.of(); // 输出示例: [],空参数返回空数组
// 安全进行链式处理
of1.map(x => x * 2); // 输出示例: [6],正常执行
Array.of() 主要用于修复 Array() 构造函数的历史缺陷。在绝大多数需要通过常量字面量定义数组的场景中,直接书写 [1, 2, 3] 仍是首选;而当你在编写工厂函数、包装函数或需要将未知参数列表安全装箱成数组时,应坚决使用 Array.of(...args) 取代 new Array(...)。
二、Array.from():类数组与可迭代对象的转换流水线
与专注参数装箱的 Array.of() 不同,Array.from() 承担的是结构转换与数据映射的双重任务。它是现代 JavaScript 中将各种“类似数组的集合”转换为正统数组的标准枢纽。
1. 转换目标的判定范围
Array.from() 可以处理两大类典型结构:
-
类数组对象(Array-like Object):拥有非负整数属性
length,且各元素具有数字索引的对象。典型如函数的内置变量arguments、DOM 的HTMLCollection,以及开发者手写的形如{ 0: 'a', 1: 'b', length: 2 }的普通对象。 -
可迭代对象(Iterable Object):实现了 ES6 迭代器接口
[Symbol.iterator]的数据结构,包括Set、Map、原生字符串String、DOMNodeList以及 Generator 生成器对象等。
2. 语法签名与第二参数(内置映射函数)
语法定义:Array.from(arrayLike[, mapFn[, thisArg]])
arrayLike(必需):待转换的目标对象。mapFn(可选):转换过程中对每个元素执行的回调函数,签名类似于数组的map方法:(element, index) => transformedElement。thisArg(可选):执行mapFn时的上下文对象。
Array.from(source).map(fn),这会在内存中先完整分配一个临时数组,然后再通过 .map() 产生第二个目标数组,造成额外的内存开销与二次垃圾回收。而使用 Array.from(source, fn),引擎在由源对象读取数据向新数组落盘的过程中,直接在同一遍历周期内原地完成了计算与赋值,只分配一次数组内存,对大容量集合处理具有显著的性能提升。
三、Array.from() 实战操作步骤与典型应用场景
为了让读者能够在日常业务中迅速套用,以下整理了四种最具代表性的操作步骤与代码模板。
场景一:无中生有生成指定长度与步长的数字序列
传统实现“生成 1 到 5 的数字序列”往往需要编写一个空数组并执行 for 循环,代码繁琐。利用包含 length 的普通对象配合 Array.from(),可以一行代码优雅生成:
// 生成连续数字序列 [1, 2, 3, 4, 5]
const range = Array.from({ length: 5 }, (_, index) => index + 1);
console.log(range);
// 示例输出: [1, 2, 3, 4, 5]
// 生成偶数序列 (步长为 2) [0, 2, 4, 6, 8]
const evens = Array.from({ length: 5 }, (_, index) => index * 2);
console.log(evens);
// 示例输出: [0, 2, 4, 6, 8]
运行原理:{ length: 5 } 满足类数组对象的最低契约。因为没有声明具体索引,Array.from 默认将其 5 个位置视为 undefined,并传递给第二参数中的映射函数。首参使用下划线 _ 占位忽略该 undefined,直接利用自增的索引 index 计算出业务所需的数值。
场景二:DOM 集合的批量提取与功能增强
通过 document.querySelectorAll() 获取的 NodeList 虽然在现代浏览器中支持 forEach,但仍然缺少 filter、map、reduce 和 some 等强力工具方法。
// 业务场景:批量获取所有选中的复选框的真实值,并转为小写
const checkboxes = document.querySelectorAll('input[type="checkbox"]:checked');
// 一步到位完成转换与属性提取
const values = Array.from(checkboxes, el => el.value.toLowerCase());
// 此时 values 是正统数组,可直接调用链式数组方法
const filtered = values.filter(val => val !== '');
console.log(filtered);
场景三:精准拆分包含 Emoji 与多字节的 Unicode 字符串
在 JavaScript 中,部分生僻汉字(如“𠮷”)或现代表情符号(Emoji 如“🚀”、“👨👩👧”)由 4 个字节(即两个 UTF-16 代理对 / Surrogate Pairs)构成。如果直接调用字符串的 .split('') 方法,会按 2 字节将代理对强行撕裂成乱码。
const text = '𠮷野家🚀';
// 传统 split 方式产生乱码
const splitArr = text.split('');
console.log(splitArr);
// 示例输出: ['\uD842', '\uDFB7', '野', '家', '\uD83D', '\uDE80'] (长度 6,前缀乱码)
// Array.from 遵循 Unicode 码点 (Code Point) 迭代
const codePointArr = Array.from(text);
console.log(codePointArr);
// 示例输出: ['𠮷', '野', '家', '🚀'] (长度 4,字符完整保留,完全准确)
场景四:配合 Set 实现高效去重与类型恢复
const rawNumbers = [10, 20, 10, 30, 20, 40];
// 利用 Set 快速剔除重复项,并无缝转回标准数组
const uniqueNumbers = Array.from(new Set(rawNumbers));
console.log(uniqueNumbers);
// 示例输出: [10, 20, 30, 40]
四、Array.of 与 Array.from 核心差异对比与注意事项
为了帮助开发者在编码时准确选型,下表从入参形式、处理机制和行为边界两个维度对两项技术进行了全面对照:
| 对比维度 | Array.of() | Array.from() |
|---|---|---|
| 核心设计目的 | 参数一致性装箱,替代易生歧义的 new Array() |
类型跨越转换,将类数组/可迭代对象转换为真实数组 |
| 入参形态 | 可变参数列表(...items) |
首参必须为一个集合对象(带 length 或可迭代) |
| 参数与元素关系 | 传入的每一个入参,对应最终数组里的一个成员 | 传入一个对象,提取拆解其内部数据成为多个成员 |
| 内置映射处理 | 不支持(需后续链式调用) | 支持第二参数 mapFn,单次遍历原位处理 |
| 空位与稀疏数组 | 严格按实参生成密集数组 | 将源对象未定义的空位自动转换为 undefined 密集元素 |
| 对比展开运算符 [...] | 等价于字面量组装,但适合动态函数式封装 | 适用面明显大于 [...](展开运算符无法处理非可迭代的纯类数组) |
开发注意事项与边界防范
-
浅拷贝(Shallow Copy)陷阱:
Array.from()产生的新数组是一个全新分配的浅拷贝实例。如果源集合中包含对象或嵌套数组,新数组中保存的仍然是对应内存地址的引用。修改新数组中的嵌套对象,源数据也会同步改变。 -
length 属性合法性校验:如果向
Array.from()传递的对象没有实现迭代器协议,且其length属性不是有效的正数(例如为负数、NaN或无法转化为数字的字符串),转换结果将直接退化为一个空数组[],而不会抛出运行时错误。 -
Array.from vs 展开运算符(Spread Operator):很多初学者认为
[...source]可以完全替代Array.from(source)。实际上,展开运算符依赖底层必须部署了Symbol.iterator。对于最常见的纯类数组对象{ 0: 'a', length: 1 },使用[...obj]会直接抛出TypeError: obj is not iterable致命错误,而Array.from(obj)则能完美转换为['a']。
JS处理含Emoji或中文的字符串转数组乱码问题
2026-11-05 16:12:28
JS怎么使用展开运算符合并对象
2026-11-05 14:42:18
ES6+环境下判断JS对象为空的简洁写法
2026-11-05 12:12:23
怎么通过replacer函数自定义JS中JSON的格式化输出
2026-11-05 11:19:44
JS正则表达式基础:字面量与RegExp构造函数的区别及常用元字符
2026-11-05 11:16:58
相关标签:







![JS创建二维数组:为什么Array(n).fill([])会导致数据联动](https://img.javascript.cn/tit/articles/13236/20261102/f618c991891ff37efb12df6af2ce0ccb0da8779a907fe1d2f7275d60c84fdd77.webp)
