Skip to content

sum()

题目描述

实现一个 sum() 函数,支持柯里化累加功能:

  • 支持连续调用:sum(1)(2)(3) == 6
  • 支持空调用:sum(1)()(2) == 3
  • 默认值为 0:sum() == 0
  • 使用 == 比较(触发类型转换)

本题考查 函数柯里化Symbol.toPrimitive闭包机制 的综合运用。

核心知识点

1. 函数柯里化 (Currying)

  • 定义: 将多参数函数转换为一系列单参数函数的技术
  • 特点: 每次调用返回一个新函数,支持部分应用
  • 优势: 代码复用、参数预设、函数组合
  • 实现: 通过闭包保持参数状态

2. Symbol.toPrimitive 类型转换

  • 作用: 控制对象到原始值的转换行为
  • 触发场景: == 比较、数学运算、字符串拼接
  • 优先级: 最高,优先于 valueOf()toString()
  • 参数: hint 可为 'number'、'string' 或 'default'

3. 闭包状态管理

  • 机制: 内部函数访问外部函数的变量
  • 持久化: 变量在函数执行完后依然存在
  • 应用: 保持累加状态,实现状态封装

代码实现

javascript
export default function sum(v = 0) {
  // 返回包装函数,支持继续调用和累加
  function wrapper(newArg = 0) {
    return sum(v + newArg) // 递归调用,传入累加后的值
  }

  // 定义类型转换行为,返回当前累加值
  wrapper[Symbol.toPrimitive] = () => v

  return wrapper
}

关键技术点

1. 递归累加机制

javascript
// 每次调用的执行流程
sum(1)(
  // v=1, 返回 wrapper 函数
  2,
)(
  // v=1+2=3, 返回新的 wrapper 函数
  3,
) // v=3+3=6, 返回新的 wrapper 函数
== 6 // 触发 Symbol.toPrimitive,返回 6

2. Symbol.toPrimitive 的工作原理

javascript
// 当函数用于比较或运算时触发
const result = sum(1)(2)(3)
console.log(result == 6) // true,触发 toPrimitive
console.log(result + 0) // 6,触发 toPrimitive
console.log(String(result)) // "6",触发 toPrimitive
console.log(Number(result)) // 6,触发 toPrimitive

// 直接调用
console.log(result[Symbol.toPrimitive]()) // 6

3. 闭包变量的生命周期

javascript
function sum(v = 0) {
  // v 被闭包捕获,在 wrapper 生命周期内持续存在
  function wrapper(newArg = 0) {
    return sum(v + newArg) // v 在这里依然可访问
  }

  wrapper[Symbol.toPrimitive] = () => v // v 在这里也可访问
  return wrapper
}

4. 默认参数的巧妙运用

javascript
// 支持空调用的关键
function wrapper(newArg = 0) {
  return sum(v + newArg) // newArg 默认为 0
}

// 使用效果
sum(1)() // 等价于 sum(1)(0)
sum(1)()(2) // 等价于 sum(1)(0)(2)

5. 常见陷阱和坑点

  • 严格相等: === 不会触发类型转换,需要使用 ==
  • 函数引用: 每次调用返回新函数,引用不相等
  • 内存泄漏: 深度嵌套调用可能产生大量闭包
  • 类型转换: 必须正确实现 Symbol.toPrimitive

使用示例

javascript
// 基本累加
console.log(sum(1)(2)(3) == 6) // true
console.log(sum(5)(10) == 15) // true

// 空调用处理
console.log(sum(1)()(2) == 3) // true
console.log(sum()(5) == 5) // true
console.log(sum()() == 0) // true

// 默认值
console.log(sum() == 0) // true
console.log(sum(0) == 0) // true

// 负数处理
console.log(sum(-1)(2)(-3) == -2) // true
console.log(sum(10)(-5) == 5) // true

// 小数处理
console.log(sum(1.5)(2.3) == 3.8) // true
console.log(sum(0.1)(0.2) == 0.3) // false (浮点精度问题)

// 多次连续调用
const chain = sum(1)(2)(3)(4)(5)
console.log(chain == 15) // true
console.log(chain(10) == 25) // true

// 中间结果保存
const intermediate = sum(10)(20)
console.log(intermediate == 30) // true
console.log(intermediate(5) == 35) // true
console.log(intermediate == 30) // true (不影响原值)

// 函数特性验证
const fn = sum(5)
console.log(typeof fn) // "function"
console.log(typeof fn === 'function') // true
console.log(fn.length) // 1 (参数个数)

记忆要点

核心记忆点

  1. 递归结构 - 每次调用返回新的 sum 函数
  2. 闭包保存状态 - v 参数在闭包中累积
  3. Symbol.toPrimitive - 控制类型转换返回累加值
  4. 默认参数 - newArg = 0 处理空调用
  5. 不可变性 - 每次调用创建新函数,不修改原函数

类型转换记忆

javascript
// == 运算符的转换优先级
// 1. Symbol.toPrimitive (hint: 'default')
// 2. valueOf()
// 3. toString()

扩展思考

1. 支持多参数版本

javascript
function sum(...args) {
  const total = args.reduce((acc, val) => acc + (val || 0), 0)

  function wrapper(...newArgs) {
    return sum(total, ...newArgs)
  }

  wrapper[Symbol.toPrimitive] = () => total
  return wrapper
}

// 使用示例
console.log(sum(1, 2, 3)(4, 5) == 15) // true

2. 支持其他运算符

javascript
function calculator(initial = 0) {
  function wrapper(value = 0) {
    return calculator(initial + value)
  }

  // 支持多种转换
  wrapper[Symbol.toPrimitive] = (hint) => {
    switch (hint) {
      case 'number':
        return initial
      case 'string':
        return String(initial)
      default:
        return initial
    }
  }

  // 支持 valueOf 和 toString
  wrapper.valueOf = () => initial
  wrapper.toString = () => String(initial)

  // 扩展运算方法
  wrapper.add = value => calculator(initial + value)
  wrapper.subtract = value => calculator(initial - value)
  wrapper.multiply = value => calculator(initial * value)
  wrapper.divide = value => calculator(initial / value)

  return wrapper
}

// 使用示例
console.log(calculator(10).add(5).multiply(2) == 30) // true

3. 链式调用终结器

javascript
function sum(v = 0) {
  function wrapper(newArg) {
    // 如果没有参数且不是第一次调用,返回结果
    if (arguments.length === 0 && v !== 0) {
      return v
    }

    return sum(v + (newArg || 0))
  }

  wrapper[Symbol.toPrimitive] = () => v
  wrapper.end = () => v // 显式终结方法

  return wrapper
}

// 使用示例
console.log(sum(1)(2)(3)()) // 6
console.log(sum(1)(2)(3).end()) // 6

4. 支持异步累加

javascript
function asyncSum(v = 0) {
  function wrapper(newArg = 0) {
    if (newArg instanceof Promise) {
      return newArg.then(val => asyncSum(v + val))
    }
    return asyncSum(v + newArg)
  }

  wrapper[Symbol.toPrimitive] = () => v
  wrapper.then = onResolve => Promise.resolve(v).then(onResolve)

  return wrapper
}

// 使用示例
asyncSum(1)(Promise.resolve(2))(3).then((result) => {
  console.log(result) // 6
})

5. 内存优化版本

javascript
// 使用 WeakMap 缓存结果,避免重复计算
const cache = new WeakMap()

function optimizedSum(v = 0) {
  // 检查缓存
  if (cache.has(optimizedSum) && cache.get(optimizedSum)[v]) {
    return cache.get(optimizedSum)[v]
  }

  function wrapper(newArg = 0) {
    return optimizedSum(v + newArg)
  }

  wrapper[Symbol.toPrimitive] = () => v

  // 缓存结果
  if (!cache.has(optimizedSum)) {
    cache.set(optimizedSum, {})
  }
  cache.get(optimizedSum)[v] = wrapper

  return wrapper
}

6. 类型安全版本

javascript
function typedSum(v = 0) {
  // 类型检查
  if (typeof v !== 'number' || isNaN(v)) {
    throw new TypeError('参数必须是有效数字')
  }

  function wrapper(newArg = 0) {
    if (typeof newArg !== 'number' || isNaN(newArg)) {
      throw new TypeError('参数必须是有效数字')
    }
    return typedSum(v + newArg)
  }

  wrapper[Symbol.toPrimitive] = () => v

  // 添加类型信息
  wrapper.getType = () => 'number'
  wrapper.getValue = () => v
  wrapper.isValid = () => !isNaN(v) && isFinite(v)

  return wrapper
}

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