Array.from
题目描述
手写实现 Array.from 方法,要求:
- 将类数组对象或可迭代对象转换为数组
- 支持可选的映射函数 mapFn
- 支持可选的 this 绑定 thisArg
- 实现辅助函数 toInteger 和 toLength
- 完整的参数验证和错误处理
本题考查 ES6 规范理解、类型检测 和 函数式编程 的综合能力。
核心知识点
1. Array.from 的多重用途
- 类数组转换: 将 NodeList、arguments 等转为真正数组
- 可迭代对象转换: 处理 Set、Map、字符串等
- 数组映射: 结合 map 功能,一步完成转换和处理
- 填充数组: 配合 Array 构造函数创建指定长度数组
2. 类数组对象识别标准
javascript
// 类数组对象特征:
// 1. 有 length 属性
// 2. length 是非负整数
// 3. 有对应索引的属性
const arrayLike = {
0: 'first',
1: 'second',
2: 'third',
length: 3,
}3. 可迭代对象协议
javascript
// 实现 Symbol.iterator 方法的对象
const iterable = {
* [Symbol.iterator]() {
yield 1
yield 2
yield 3
},
}4. ECMAScript 规范中的抽象操作
- ToInteger: 将值转换为整数
- ToLength: 确保长度在安全范围内
- IsCallable: 检测值是否可调用
- GetMethod: 获取对象的方法
代码实现
javascript
/**
* 自定义实现 Array.from 方法,将类数组对象或可迭代对象转换为数组。
*
* @param {ArrayLike|Iterable} arrayLike - 类数组对象或可迭代对象。
* @param {Function} [mapFn] - 可选的映射函数,用于对每个元素进行处理。
* @param {*} [thisArg] - 可选的上下文对象,用于绑定 `mapFn` 的 `this` 值。
* @returns {Array} 返回一个新数组,包含从 `arrayLike` 转换而来的元素。
* @throws {TypeError} 如果 `arrayLike` 为 null 或 undefined,抛出类型错误。
* @throws {TypeError} 如果提供的 `mapFn` 不是函数,抛出类型错误。
*/
export default function myFrom(arrayLike, mapFn, thisArg) {
const isCallable = fn => typeof fn === 'function' || Object.prototype.toString.call(fn) === '[object Function]'
const toInteger = (v) => {
const _v = Number(v)
if (Number.isNaN(_v))
return 0
if (v === 0 || !Number.isFinite(_v))
return _v
return (_v > 0 ? 1 : -1) * Math.floor(Math.abs(_v))
}
const maxSafeInteger = Number.MAX_SAFE_INTEGER
const toLength = (v) => {
const n = toInteger(v)
if (n > maxSafeInteger)
throw new RangeError('length exceeds MAX_SAFE_INTEGER')
return Math.max(0, n)
}
if (arrayLike == null) {
throw new TypeError(
`provided arrayLike must be an array-like object - not null/undefined`,
)
}
const items
= arrayLike instanceof Set || arrayLike instanceof Map || arrayLike instanceof WeakMap || arrayLike instanceof WeakSet
? [...arrayLike]
: new Object(arrayLike)
if (typeof mapFn !== 'undefined') {
if (!isCallable(mapFn))
throw new TypeError(`provided mapFn must be a function`)
}
const len = toLength(items.length)
const arr = new Array(len)
let i = 0
let current
while (i < len) {
current = items[i]
if (mapFn) {
arr[i]
= typeof thisArg === 'undefined'
? mapFn(current, i)
: mapFn.call(thisArg, current, i)
}
else {
arr[i] = current
}
i += 1
}
arr.length = len
return arr
}关键技术点(基于当前实现的真实行为)
1. 参数与类型校验
arrayLike == null(null或undefined)直接抛TypeError。- 只有在显式传入
mapFn(非undefined)时才校验其是否可调用,不接受时抛TypeError。 thisArg不做任何合法性判断,直接在mapFn.call中使用。
2. 内部辅助函数实现细节
toInteger 行为:
NaN → 0±Infinity原样返回(不截断)- 其它数值:带符号向下取整(
Math.floor(Math.abs(v))再恢复符号)
toLength 行为:
- 先走
toInteger - 若结果大于
Number.MAX_SAFE_INTEGER直接抛RangeError(原生规范是“截断 clamp”,这里选择“抛错”) - 负值归 0
⚠ 与规范差异:原生 ToLength 不会因为超过范围抛错,而是截断到 2^53 - 1。
3. 数据源标准化策略(核心分支)
javascript
const items = (arrayLike instanceof Set
|| arrayLike instanceof Map
|| arrayLike instanceof WeakMap
|| arrayLike instanceof WeakSet)
? [...arrayLike] // 对 Set / Map 有效;对 WeakMap / WeakSet 运行时将抛错
: new Object(arrayLike)- 目标:把“可迭代集合”快速转为普通数组再按索引复制。
- 实际:
WeakMap/WeakSet无迭代接口,[...weakSet]会抛TypeError,当前代码并未过滤(潜在 Bug)。 - 其它对象直接
Object()包装,只依赖其length。若仅实现Symbol.iterator但无length,结果长度会被推断为 0。
4. 长度获取与遍历方式
- 唯一信息源:
items.length→ 经过toLength得到len。 - 没有检测索引属性是否真实存在(不会判断
i in items),直接读取,缺失位置得到undefined并写入结果(与“原生空槽”语义不同——原生可能产生稀疏数组,这里是显式值)。
5. 结果数组构造策略
- 先
new Array(len)预分配;循环结束后再次arr.length = len(冗余但无害)。 - 线性 O(len) 填充;无中间临时扩容逻辑。
6. mapFn 执行语义
- 若存在
mapFn:每次调用形如:- 无
thisArg:mapFn(current, index) - 有
thisArg:mapFn.call(thisArg, current, index)
- 无
- 不包裹 try/catch;内部抛错直接冒泡。
7. 支持的数据类型范围(当前真实状态)
| 类型 | 行为 | 备注 |
|---|---|---|
| Array / 类数组(有 length) | 正常复制 | 稀疏索引写入 undefined |
| String | 按 UTF-16 单元拆分(与原生一致,对合并表情拆分成两个 code unit 的情况未特别处理) | |
| Set | [...set] 复制后再一次写入 | 两次遍历成本 |
| Map | 生成 [[k,v], ...] | 行为与 [...map] 保持一致 |
| WeakSet / WeakMap | 运行时抛错 | 代码误判为可迭代 |
| 自定义 iterator 但无 length | 返回 [] | 偏差:未真正消费迭代器 |
| 仅有 length 的对象 | 按索引读取 | 典型类数组场景 |
8. 与原生 Array.from 的主要差异
- 缺失“通用 iterator”分支:不检测
Symbol.iterator再逐步next()。 - 没有迭代器关闭逻辑(无法保证异常时调用
iterator.return())。 WeakMap/WeakSet错误地尝试展开。ToLength超界抛错 vs 原生截断。- 不支持
this为构造函数的 species 机制(始终返回普通Array)。 - 稀疏结果中使用显式
undefined而非创建“空槽”(观察工具区别:Object.keys/in行为)。 - 未对 mapFn 的参数数量或返回值做任何包装处理。
9. 异常与错误抛出策略
| 场景 | 抛出 | 触发点 |
|---|---|---|
arrayLike == null | TypeError | 入口参数检验 |
mapFn 非函数 | TypeError | mapFn 校验 |
长度 > MAX_SAFE_INTEGER | RangeError | toLength 中主动抛出 |
WeakSet / WeakMap | TypeError | 展开阶段(由引擎抛) |
mapFn 内部异常 | 透传 | 循环调用时 |
10. 性能特征
- 单次 O(n) 复制(数组 / 类数组)。
- 对 Set/Map:
[...x]+ 第二次写入,等价“两次遍历”。 - 无 fast-path:真数组 + 无 mapFn 仍逐元素赋值(可优化为
slice())。
11. 边界行为速览
| 输入 | 结果(当前实现) | 备注 |
|---|---|---|
{ length: -5 } | [] | 负值归 0 |
{ length: 2.7, 0:'a',1:'b' } | ['a','b'] | 2.7 → 2 |
| 长度极大 > MAX_SAFE_INTEGER | 抛 RangeError | 与原生不同 |
| 纯 iterator(无 length) | [] | 原生会消费 iterator |
稀疏 {0:'a',2:'c',length:4} | ['a',undefined,'c',undefined] | 原生可能保留空槽 |
WeakSet() | 直接抛错 | 试图展开 |
12. 可改进方向(按收益排序)
- 真正检测并消费
Symbol.iterator,统一 iterable 路径。 - 过滤掉
WeakMap/WeakSet,或在文档中声明不支持。 toLength改为 clamp 行为,贴合规范。- 添加 fast-path(数组无 mapFn / 字符串无 mapFn)。
- 引入 iterator 关闭(
try/finally)。 - 可选实现 species(支持子类化
Array.from.call(SubClass, ...))。 - 区分“空槽”与
undefined(若教学需要展示稀疏数组特性)。 - 针对 Map 可开放自定义键值展开策略(仅值 / 仅键)。