学习Lodash源码(add函数)

331 阅读5分钟

「一秒不够,那就再学亿秒钟」

前言


你好,我是Rance。一直以来想学习优秀的源码提升自己技术力,但是苦于自己的功力不够,类似 Vue, React 这类大型框架源码,看起来又比较吃力。所以经过一番搜索,发现lodash的源码分割的较小,工具函数也非常实用,适合作为源码学习材料。于是乎有了这个系列文章。这是学习 Lodash 源码的第一篇,话不多说,让我们开始吧!

add方法的目录结构 微信图片_20230104164653.png

1. 首先来看下add方法的入口

// add.js
import createMathOperation from './.internal/createMathOperation.js'

/**
 * Adds two numbers.
 * 两数相加
 *
 * @since 3.4.0
 * @category Math
 * @param {number} augend The first number in an addition. 第一个数
 * @param {number} addend The second number in an addition. 第二个数
 * @returns {number} Returns the total. 返回总和
 * @example
 *
 * add(6, 4)
 * // => 10
 */
const add = createMathOperation((augend, addend) => augend + addend, 0)

export default add

  • add函数的目的是返回两数相加之和。从这里我们可以看到,它在内部使用createMathOperation的函数,接受2个参数,第一个是函数,表明你要执行的操作;第二个是默认值,用于异常情况下作为返回值。
  • add函数的返回值是createMathOperation的函数的调用结果,那么createMathOperation的函数里面又是什么样子呢,我们进入createMathOperation内部看看。

2. createMathOperation函数

// createMathOperation.js
import baseToNumber from './baseToNumber.js'
import baseToString from './baseToString.js'

/**
 * Creates a function that performs a mathematical operation on two values.
 * 创建一个对两个值进行数学操作的函数
 *
 * @private
 * @param {Function} operator The function to perform the operation. 要执行操作的函数
 * @param {number} [defaultValue] The value used for `undefined` arguments. 用来应对传递undefined参数的默认值
 * @returns {Function} Returns the new mathematical operation function. 返回一个新的数学操作函数
 */
function createMathOperation(operator, defaultValue) {
  return (value, other) => {
    // 如果2个值都是undefined,返回默认值
    if (value === undefined && other === undefined) {
      return defaultValue
    }
    // 第1个值不是undefined,第2个是undefind, 返回第1个值
    if (value !== undefined && other === undefined) {
      return value
    }
    // 第2个值不是undefined,第1个是undefind, 返回第2个值
    if (other !== undefined && value === undefined) {
      return other
    }

    // 如果一个是字符串,那么全部转为字符串
    if (typeof value === 'string' || typeof other === 'string') {
      value = baseToString(value)
      other = baseToString(other)
    }
    else {
      // 否则全部转为数字
      value = baseToNumber(value)
      other = baseToNumber(other)
    }
    return operator(value, other)
  }
}

export default createMathOperation

  • 可以看到这个createMathOperation.js导入了2个方法:baseToNumber, baseToString
  • 我们先不用管 baseToNumberbaseToString 的具体实现,现在只需知道它们会分别把值转为数字和字符。
  • 整理下createMathOperation函数内部返回的匿名函数的逻辑:
    • 首先对用户传的2个值进行校验
      • 如果2个值都是undefined,返回默认值defaultValue
      • 如果第1个值不是undefined,第2个是undefind, 返回第1个值
      • 如果第2个值不是undefined,第1个是undefind, 返回第2个值
      • 如果其中一个值是字符串,那么两个值都使用baseToString转为字符串
      • 否则都使用baseToNumber转为数字
    • 然后返回操作函数的调用结果,就是(augend, addend) => augend + addend 的返回结果

也就是说,createMathOperation函数就做了一件事:创建一个新函数,并返回。

这个新函数做了2件事:

  • 第一,校验下用户传递的参数是否合规,把不合规的剔除或者转换。
  • 第二,返回(augend, addend) => augend + addend的调用结果。

3.接下来我们看下其他辅助方法内部做了什么?请直接看注释

baseToString内部实现

// baseToString
import isSymbol from '../isSymbol.js'

/** Used as references for various `Number` constants. */
/** 由于Infinity是标识符,可能被覆写,所以用1 / 0代替 */
const INFINITY = 1 / 0

/** Used to convert symbols to primitives and strings. */
/** 用于把symbol类型转为字符串 */
const symbolToString = Symbol.prototype.toString

/**
 * The base implementation of `toString` which doesn't convert nullish
 * values to empty strings.
 * 不会把空值转为空字符串的toString的基本实现
 *
 * @private
 * @param {*} value The value to process. 要处理的值
 * @returns {string} Returns the string. 返回字符串
 */
function baseToString(value) {
  // Exit early for strings to avoid a performance hit in some environments.
  // 对于字符串,应该尽早返回,避免某些情况的性能问题
  if (typeof value === 'string') {
    return value
  }

  // 如果是数组类型,那么递归转换里面每一项
  if (Array.isArray(value)) {
    // Recursively convert values (susceptible to call stack limits).
    // 递归转换值(容易受堆栈调用限制)
    return `${value.map(baseToString)}`
  }

  // 如果是symbol类型
  if (isSymbol(value)) {
    // 先看下Symbol转字符串的方法是否可用,可用就调用来转换,不可用就返回空字符串
    return symbolToString ? symbolToString.call(value) : ''
  }

  // 其他情况就是用类似 "" + 123 => "123" 的方式进行转换
  const result = `${value}`

  // 还有一种特殊情况: "" + (-0)  => "0"  负号丢失了,需要用下面的方式补上去
  return (result === '0' && (1 / value) === -INFINITY) ? '-0' : result
}

export default baseToString

baseToNumber内部实现

// baseToNumber.js
import isSymbol from '../isSymbol.js'

/** Used as references for various `Number` constants. */
/** 由于NaN是标识符,可能被覆写,所以用0 / 0代替 */
const NAN = 0 / 0

/**
 * The base implementation of `toNumber` which doesn't ensure correct
 * conversions of binary, hexadecimal, or octal string values.
 * 不能保证成功转换二进制,十六进制和八进制的toNumber的基础实现
 *
 * @private
 * @param {*} value The value to process. 要处理的值
 * @returns {number} Returns the number. 返回数字
 */
function baseToNumber(value) {
  // 如果是数字,直接返回
  if (typeof value === 'number') {
    return value
  }

  // 如果是symbol类型,那是不能转为数字的,返回NaN
  if (isSymbol(value)) {
    return NAN
  }

  // 其他情况,使用加号调用js自身的转换规则转换
  return +value
}

export default baseToNumber

isSymbol内部实现

// isSymbol.js
import getTag from './.internal/getTag.js'

/**
 * Checks if `value` is classified as a `Symbol` primitive or object.
 * 检查值是否是symbol原始值或者symbol对象
 *
 * @since 4.0.0
 * @category Lang
 * @param {*} value The value to check. 要检查的值
 * @returns {boolean} Returns `true` if `value` is a symbol, else `false`. 如果是symbol,返回true,否则返回false
 * @example
 *
 * isSymbol(Symbol.iterator)
 * // => true
 *
 * isSymbol('abc')
 * // => false
 */
function isSymbol(value) {
  // 判断类型: 可能的值有  "number" "string" "boolean" "undefined" "symbol" "bigint" "function" "object"
  const type = typeof value

  /**
   * type == 'symbol' 判断是否是symbol原始值, 比如 Symbol(132) 就是symbol原始值
   * (type === 'object' && value != null && getTag(value) == '[object Symbol]') 判断是否是对象,并且类型标签是"[object Symbol]", 如果是,就是symbol对象
   * 
   * symbol对象是什么?   
   * Object(Symbol(123))  这就是symbol对象
   */
  return type == 'symbol' || (type === 'object' && value != null && getTag(value) == '[object Symbol]')
}

export default isSymbol

getTag内部实现

// getTag.js
const toString = Object.prototype.toString

/**
 * Gets the `toStringTag` of `value`.
 * 获取值的toStringTag标签
 *
 * @private
 * @param {*} value The value to query. 要查询的值
 * @returns {string} Returns the `toStringTag`. 返回toStringTag
 */
function getTag(value) {
  // 如果是null 或者 undefined
  // 这里对null和undefined这样判断是为了提高性能,使用toString.call(undefined) === "[object Undefined]" 没这种判断来得快
  if (value == null) {
    // 进一步判断,如果是undefined, 返回'[object Undefined]', 否则返回'[object Null]'
    return value === undefined ? '[object Undefined]' : '[object Null]'
  }

  // 其他情况,全部采用Object.prototype.toString.call()的方式
  return toString.call(value)
}

export default getTag

小结

一个小小的add方法,内部却有这么多门道,除开add实现逻辑本身,那些内部的转换方法,判断类型的方法都值得我们学习。感谢大家的观看。