ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

彻底搞懂new Array、Array.of与Array.from:参数语义、稀疏数组与转换机制全解析

彻底搞懂new Array、Array.of与Array.from:参数语义、稀疏数组与转换机制全解析 很多前端同学写了好几年代码提到创建数组第一反应还是new Array()。真到了把伪数组转真数组、批量生成数据、处理长度与元素分不清的场景又因为Array.from()和Array.of()的加入开始犹豫不决。这三个API表面上看都是搞出一个数组来但设计出发点完全不同混用轻则写出难以维护的代码重则直接在边界条件下翻车。我自己带团队面试时最爱问的一个低级但致命的问题是new Array(5)和Array.of(5)分别得到什么。十个里有三四个答不对还有两三个虽然答对了但说不出为什么。今天就一次性把这三个API的来龙去脉、底层机制、实际用法和坑全拆开讲清楚看完之后你就不会再在这上面浪费哪怕一秒钟。1. 三个API的出身与设计初衷1.1 为什么数组创建会有三条路子要理解这三个API首先要回到JavaScript的历史现场。new Array()是最老牌的从ES1开始就有跟JavaScript本身一样老。它的问题在于参数语义分裂传一个数字参数时它不把它当作数组元素而是当作数组的长度。这个设计在当年足够简单粗暴也算好用但放到今天各种边界情况就会变得非常别扭。后来ES6带来了Array.of()和Array.from()这两个不是简单的语法糖而是为了解决new Array()在一些真实场景中无法优雅应对的问题而专门设计的。Array.of()的核心宗旨是参数即元素不管传什么传多少都老老实实成为数组元素。而Array.from()的核心宗旨是万物皆可转数组类数组对象、可迭代对象只要有length或者带迭代器都能转成真正的数组。我个人的理解是new Array()是构造数组的思路Array.of()是收集参数的思路Array.from()是转换来源的思路。三条路各不相同可要是没搞清楚每个API的真实定位写出来的代码就会显得很野。1.2 从规范层面看本质差异我不爱讲太枯燥的规范但这个话题绕不开。打开ECMAScript规范你会发现Array.of和Array.from刻意避开了new Array()的单参数陷阱。具体来说规范里对Array.of的描述是利用this作为构造函数来创建数组然后逐个填入参数。你甚至可以通过Array.of.call(SomeSubClass, 1, 2, 3)让子类也能得到正确类型的实例。而new Array()没法做到这种以调用方式决定构造函数的灵活度。Array.from就更复杂一些它内部先获取一个构造器默认就是Array然后创建数组再根据来源对象的length和键值依次填充之后如果你传了映射函数还会对每个元素执行一次映射。最妙的是Array.from支持类数组对象和可迭代对象两种来源这是new Array()完全不具备的能力。这些规范细节平时写业务代码不一定直接碰得到但理解之后你再看到Array.from({length: 3})这种写法就不会觉得神秘了无非是它真的去读了那个对象的length属性。2. 核心区别逐条拆解2.1 参数语义长度还是元素这是三个API最容易混淆的第一道坎。new Array(3)创建的是长度等于3的稀疏数组sparse array里面的三个槽位是空的不是undefined是真正的empty。你说它是数组吗是。你能直接访问arr[0]得到undefined吗能。但用map遍历时它会直接跳过这些空槽这在后面会专门讲。而Array.of(3)创建的数组是[3]长度是1元素就是数字3。展现在控制台里前者是[empty x 3]后者是[3]一眼就能看出差别。我见过太多人写new Array(5).map((item, index) index)以为自己生成了一个[0, 1, 2, 3, 4]的数组结果返回的还是稀疏数组。map 访问到空槽时压根不执行回调直接跳过最终你得到的还是一个长度为5的空数组。这个坑几乎每个前端都踩过区别只是有没有意识到自己踩了。反过来如果你真的想要一个长度为5但元素为undefined的密集数组用new Array(5).fill(undefined)是常规做法或者直接上Array.from({length: 5})。后者生成的是每个槽位都明明白白属于undefined的密集数组dense array处理起来更符合直觉。2.2 稀疏数组与密集数组不止是概念差异很多人觉得稀疏和密集不就是空和undefined的区别吗实际操作上差远了。稀疏数组在V8引擎内部有专门的表示方式它会把空槽以hole的形式记录各种数组方法对hole的处理并不统一。比如map和forEach会跳过但find和indexOf不会跳过。这种不一致性导致同样的数据处理流程换个方法结果就不一样极容易造成隐藏bug。密集数组就友好得多所有槽位都是真实存在的属性只是值为undefined。你在遍历时能正常经过每一项逻辑不会出现莫名少了几次的情况。Array.from({length: 3})出来的是密集数组Array.of出来的永远是密集数组只有new Array(n)在单参数且参数是非负整数时会产生稀疏数组。理解了这层关系很多奇奇怪怪的问题都有了解释。比如你console.log(new Array(3))看到[empty x 3]第一反应就不该是这数组是空的吗而是哦这是稀疏数组我要小心处理。2.3 类数组与可迭代对象的转换能力Array.from()最强大的地方在于它能接收两类没有血缘关系的数据类数组对象array-like和可迭代对象iterable。类数组对象是指有数字索引和length属性的普通对象最典型的就是arguments和DOM操作返回的NodeList。在ES6之前要把arguments转成数组你得写Array.prototype.slice.call(arguments)虽然能用但有一种借鸡生蛋的投机感。现在直接Array.from(arguments)干净利落。同理document.querySelectorAll(div)返回的NodeList也能直接Array.from转成真数组然后尽情使用map、filter等方法。可迭代对象则包括Set、Map、字符串、生成器等。字符串能转成字符数组Array.from(abc)得到[a, b, c]这个能力用来做字符级别的处理非常方便尤其是处理emoji等Unicode字符时比split()更安全。new Array()从来没有这种转换能力它只能基于你给它的参数老老实实构造数组。Array.of()也没有它的世界只有参数没有来源对象的概念。3. 实战场景中的选择策略与代码示例3.1 把类数组转成数组的演进史我们拿arguments举例。这是一段很有年代感的代码function sum() { // 老写法 var args Array.prototype.slice.call(arguments); // 更新一点的写法 var args [].slice.call(arguments); // ES6写法 const args Array.from(arguments); return args.reduce((acc, cur) acc cur, 0); }三种写法在功能上没有本质区别但第三种可读性最好。更重要的是Array.from的语义是从这个类数组对象创建数组而不是借用Array原型上的方法去操作别人的对象。slice.call这种写法虽然广为流传但实际上是在利用slice的内部逻辑闭着眼睛去读arguments的索引和length理解起来多绕了一层弯。如果还需要在转换过程中改造元素Array.from的第二个参数就是为此准备的function sumWithTax() { const prices Array.from(arguments, item item * 1.13); return prices.reduce((acc, cur) acc cur, 0); }一步到位完成了转换加映射不用先转数组再map走一遍性能更好代码也少几行。3.2 批量生成数据时 Array.from 是神器前端日常开发中你经常会遇到我要生成一个从0到n的数组这种需求。老派做法是写个循环然后push或者用new Array(n).fill().map((_, i) i)但最优雅的其实是// 生成 [0, 1, 2, ..., 9] const range Array.from({ length: 10 }, (_, i) i);第一参数{ length: 10 }是个类数组对象Array.from读它的length创建长度为10的密集数组第二参数的映射函数接收两个参数当前值和当前索引。这里索引i直接用来当值返回一条语句解决。如果你需要生成指定步长的序列也可以在这个基础上扩展// 生成偶数序列 [0, 2, 4, ..., 18] const evens Array.from({ length: 10 }, (_, i) i * 2);这种写法比手写循环更声明式——你要什么直接说长度多少元素怎么算而不必关心怎么一步步往数组里塞。3.3 数量和长度分不清时的 Array.ofArray.of的经典使用场景是我有一堆参数要把它们全部作为元素放进数组。说白了就是对new Array单参数陷阱的补救。function createArray() { return Array.of(...arguments); }虽然有了展开运算符之后这种做法看起来没那么必要了但Array.of真正的价值在于子类继承的场景。假设你封装了一个自定义数组类MyArrayclass MyArray extends Array { customMethod() { return this.map(item item * 2); } } // new Array 无法指定子类除非手动调用 const a new MyArray(1, 2, 3); // 但如果是从别的地方动态收集参数Array.of 就非常重要 const b MyArray.of(1, 2, 3); console.log(b instanceof MyArray); // trueMyArray.of会使用MyArray作为构造函数而不是Array。这种以类为尊的特性是new Array()做不到的。你当然可以自己写静态方法去处理但Array.of是原生规范提供的通用方案团队里任何使用这个子类的人都能拿到预期类型。4. 常见踩坑记录与排查思路4.1 new Array 单参数陷阱的连环坑最经典的坑就是开头提到的new Array(5).map((_, i) i)。我们来实际推演一下const arr new Array(5); console.log(arr); // [empty × 5] console.log(arr.length); // 5 const mapped arr.map((_, i) i); console.log(mapped); // [empty × 5] 回调根本没执行原因在于稀疏数组的空槽不参与数组方法的遍历。map、forEach、filter都会跳过空槽而indexOf却会返回-1find却能正常处理。这种不一致容易让人在排查问题时很崩溃明明同一批数据为什么find能抓到元素map却输出空数组解决办法有两种。如果你想要的是密集数组用Array.from({ length: 5 })如果你只是想要一个固定长度的占位数组用Array.from({ length: 5 }).fill(0)或者new Array(5).fill(0)。无论如何不要直接对一个稀疏数组做map期望它遍历全部空槽。我还见过有人用new Array(1).length当数字用觉得挺聪明其实不如直接写上1。代码不是越短越好越不容易出错才好。4.2 转换来源时的隐蔽陷阱Array.from虽然强大但也有自己的脾气。如果你传入的是没有length和迭代器的普通对象比如Array.from({})它不会报错但返回的是一个空数组[]。这不算bug却容易让粗心的人误以为是数据没传进来。另外一点类数组对象的length必须是非负整数如果length是3.5Array.from会把它当作3来处理。这种隐式转换在极端情况下会带来困惑所以尽量保证length属性是清晰的整数。字符串转数组时也有个细节。Array.from()会把整个家庭表情作为一个代码点来处理得到长度为1的数组而.split()会把这个多码点表情拆成好几段乱码。在处理国际化内容时这个差异很重要我已经不止一次看到有人因为用split处理表情字符导致乱码问题而换成Array.from一下子就对了。4.3 性能对比与选型建议我做了一个简单的性能和代码可读性对比供参考场景推荐方式说明创建指定长度占位数组Array.from({ length: n })密集数组直接遍历不出意外元素全相同new Array(n).fill(value)语义明确性能也还行参数个数不定且都作为元素Array.of(...args)没有单参数陷阱类数组或可迭代对象转数组Array.from(iterable)最标准的现代写法同时转换加映射Array.from(iterable, mapper)一步到位语义清晰需要子类实例SubClass.of(...args)利用Array.of的构造器传递机制从字符串生成字符数组Array.from(str)正确处理码点性能上new Array(n)单纯创建一个稀疏数组是最快的因为它几乎不分配元素内存。Array.from({ length: n })会逐一填充undefined稍慢一些但也只在超大数组时才有感知差异。业务代码里优先考虑可读性而不是这几毫秒的差异。我自己在百万级数据测试下Array.from相对new Array大概有20%左右的性能差距但这点差距远不如一个隐藏bug带来的排查成本高。如果你追求极致的性能又要密集数组其实还有一种写法const arr new Array(n); for (let i 0; i n; i) arr[i] 0;这是最快的但写起来啰嗦。所以我个人建议默认使用Array.from或Array.of只有在明确需要极致性能或者写基础库时才去手动优化。5. 项目落地时的整体建议5.1 团队规范与代码评审的要点如果你在带团队或者参与代码评审我建议把禁止使用new Array(n)直接创建后立即遍历写进规范。因为新手很容易踩进稀疏数组的坑而且踩的姿势千奇百怪排查起来很费时间。建议在规范里列一个简单的对照表让大家根据场景自己选。不用太复杂就记住三句话要创造一个指定长度的数组用Array.from({ length: n })要把参数原封不动变成数组元素用Array.of(...args)要转换类数组或可迭代对象用Array.from(source)。这三句话覆盖了90%以上的使用场景。剩下那10%是性能极客或者库编写者的专属领域靠new Array裸写也完全没问题但代码里必须注释清楚为什么这么写。我在评审中见过最可惜的一种写法是有人为了把一个数字转成长度等于该数字的数组写了[...Array(num)].map((_, i) i)。虽然可行但可读性远不如Array.from({ length: num }, (_, i) i)。前者打断阅读思路要在大脑里先展开两个操作后者一眼就看明白从长度生成数组并按索引取值。5.2 学习教育路径的思考从学习角度来说我建议初学者直接跳过new Array的单参数构造用法从Array.of和Array.from入手。因为这两个API的设计更符合直觉要么收集参数要么转换来源。等理解了数组的密集和稀疏概念之后再回去看new Array(n)的稀疏行为就很容易建立起完整的认知框架。很多教程还在教new Array的三种形态无参、单数字参、多参这当然没错但我见过太多人学完这三种形态依然分不清何时用哪个。反而是先学Array.of和Array.from再回头看老式API整个脉络会非常清晰。补充一个我私藏的调试技巧如果你在控制台看到[empty × 5]不要慌你完全可以快速判断这是稀疏数组。但如果你不确定某个数组是不是稀疏可以遍历属性键const sparse new Array(3); console.log(0 in sparse); // false const dense Array.from({ length: 3 }); console.log(0 in dense); // truein运算符可以判断属性是否存在稀疏数组的空槽视为属性不存在密集数组的undefined值是存在的属性。这个小技巧在调试别人写的“神秘数组”时特别有用。最后说一句我的真实感受。Array.from、Array.of、new Array这三个API从功能上看似乎重叠但在设计哲学上完全不是一个层次的东西。new Array是上个时代的工具带着历史的包袱Array.of和Array.from是ES6带来的一对手术刀分别精准解决参数成数组和来源转数组两个问题。日常写代码时我几乎不再主动使用new Array(n)来创建数组了只有在需要稀疏数组的罕见场景下或者在极强调性能的底层工具里它才会重出江湖。工具不在多关键是用对地方。
返回列表