先记住这个答案
Array.from 接受两类输入。如果参数有 Symbol.iterator 方法,它就按迭代器一个个取值;如果没有,它就把参数当类数组,读出 length,再按 0 到 length - 1 的下标逐个取值。数组展开 [...x] 只认迭代器,只有 length 和下标的对象不可迭代,展开时会抛 TypeError。两种条件都满足时,Array.from 优先用迭代器,结果不一定等于那些下标属性。它创建的是新数组,但只是浅拷贝,缺失的下标会变成真实的 undefined。
Array.from支持可迭代对象和类数组两种输入- 数组展开只支持可迭代对象,类数组会报错
- 两者都有时,
Array.from优先用迭代器 - 类数组里缺失的下标会变成
undefined
同一个对象,两种写法
类数组指有 length 属性、并且用数字作为键的对象,长得像数组,但原型上没有数组方法,也不一定能迭代。
Set 和生成序列JavaScriptconst likeArr = { 0: 'a', 1: 'b', length: 2 } // 只有下标和 length
console.log(JSON.stringify(Array.from(likeArr)))
try {
console.log([...likeArr])
} catch (e) {
console.log(e.name) // 普通对象没有迭代器
}
console.log(JSON.stringify(Array.from(new Set([1, 1, 2])))) // 可迭代对象
console.log(JSON.stringify(Array.from({ length: 3 }, (_, i) => i * 2))) // 第二个参数是映射函数查看输出与解释
["a","b"]
TypeError
[1,2]
[0,2,4]likeArr 没有 Symbol.iterator,展开语法不知道怎么逐个取值,只能报错。Array.from 发现没有迭代器,就退回到按 length 读下标的模式,所以能转换成功。
最后一行是 Array.from 很常用的一个技巧:{ length: 3 } 是一个只有长度的类数组,配合第二个参数 mapFn,就能直接生成 [0, 2, 4] 这样的序列。mapFn 在创建过程中对每一项调用,不会先生成中间数组。
迭代器优先,缺失下标变 undefined
如果一个对象既有迭代器又有 length 和下标,Array.from 会忽略下标,只看迭代器产出了什么。
const both = {
0: 'index', length: 1,
*[Symbol.iterator]() { yield 'iter1'; yield 'iter2' }
}
console.log(JSON.stringify(Array.from(both))) // 走迭代器,下标被忽略
const holes = Array.from({ length: 2, 1: 'x' }) // 下标 0 不存在
console.log(0 in holes, holes[0], holes[1])查看输出与解释
["iter1","iter2"]
true undefined x第一段结果里完全没有 'index',而且长度是 2 而不是 length 声明的 1。所以说 Array.from 的结果「不一定等于索引属性」。
第二段里,原对象下标 0 根本不存在,Array.from 读到 undefined 后照样写进了新数组,0 in holes 是 true。也就是说原来的「空位」变成了「有值,值是 undefined」。之后调用 forEach、map 时,这一项会被访问到,而原来的空位是会被跳过的。
容易答错的地方
- 以为
Array.from是深拷贝 - 它只创建一个新的外层数组。
Array.from([{ n: 1 }])里的对象还是原来那个,修改它会影响原数组。 - 用
Array.from转普通对象的键值 Array.from({ a: 1, b: 2 })得到空数组,因为这个对象既没有迭代器,length也是undefined,按 0 处理。要把普通对象转成数组,用Object.keys、Object.values或Object.entries。- 以为能直接转换异步迭代器
Array.from只处理同步迭代器。传入异步生成器,它找不到Symbol.iterator,会退回类数组模式,最后得到一个空数组。异步场景用for await...of收集,较新的环境也提供了Array.fromAsync。
面试官还会怎么问?
Array.from(str) 和 str.split('') 有什么区别?
字符串的迭代器按 Unicode 码点遍历,split('') 按 UTF-16 编码单元切。对于 emoji 这类占两个编码单元的字符,Array.from('👍a') 得到 2 项,'👍a'.split('') 得到 3 项,其中两项是乱码的半个字符。
Array.from 和 Array.prototype.slice.call 都能转类数组,区别是什么?
slice.call(arguments) 是 ES5 时代的写法,只按 length 和下标读取,不认迭代器,而且会保留空位。Array.from 两种都认,缺失下标变成 undefined,还能顺带传 mapFn。新代码直接用 Array.from。
传入 null 或 undefined 会怎样?
抛 TypeError,因为它们没法转成对象。传入数字、布尔值不会报错,它们被转成包装对象,没有迭代器,length 也是 undefined,结果是空数组。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。