Skip to content

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. 参数与类型校验

  1. arrayLike == nullnullundefined)直接抛 TypeError
  2. 只有在显式传入 mapFn(非 undefined)时才校验其是否可调用,不接受时抛 TypeError
  3. 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:每次调用形如:
    • thisArgmapFn(current, index)
    • thisArgmapFn.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 的主要差异

  1. 缺失“通用 iterator”分支:不检测 Symbol.iterator 再逐步 next()
  2. 没有迭代器关闭逻辑(无法保证异常时调用 iterator.return())。
  3. WeakMap / WeakSet 错误地尝试展开。
  4. ToLength 超界抛错 vs 原生截断。
  5. 不支持 this 为构造函数的 species 机制(始终返回普通 Array)。
  6. 稀疏结果中使用显式 undefined 而非创建“空槽”(观察工具区别:Object.keys / in 行为)。
  7. 未对 mapFn 的参数数量或返回值做任何包装处理。

9. 异常与错误抛出策略

场景抛出触发点
arrayLike == nullTypeError入口参数检验
mapFn 非函数TypeErrormapFn 校验
长度 > MAX_SAFE_INTEGERRangeErrortoLength 中主动抛出
WeakSet / WeakMapTypeError展开阶段(由引擎抛)
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_INTEGERRangeError与原生不同
纯 iterator(无 length)[]原生会消费 iterator
稀疏 {0:'a',2:'c',length:4}['a',undefined,'c',undefined]原生可能保留空槽
WeakSet()直接抛错试图展开

12. 可改进方向(按收益排序)

  1. 真正检测并消费 Symbol.iterator,统一 iterable 路径。
  2. 过滤掉 WeakMap / WeakSet,或在文档中声明不支持。
  3. toLength 改为 clamp 行为,贴合规范。
  4. 添加 fast-path(数组无 mapFn / 字符串无 mapFn)。
  5. 引入 iterator 关闭(try/finally)。
  6. 可选实现 species(支持子类化 Array.from.call(SubClass, ...))。
  7. 区分“空槽”与 undefined(若教学需要展示稀疏数组特性)。
  8. 针对 Map 可开放自定义键值展开策略(仅值 / 仅键)。

内容基于 MIT 许可 | 保持节奏 · 持续积累