「一秒不够,那就再学亿秒钟」
前言
你好,我是Rance。一直以来想学习优秀的源码提升自己技术力,但是苦于自己的功力不够,类似 Vue, React 这类大型框架源码,看起来又比较吃力。所以经过一番搜索,发现lodash的源码分割的较小,工具函数也非常实用,适合作为源码学习材料。于是乎有了这个系列文章。这是学习 Lodash 源码的第一篇,话不多说,让我们开始吧!
add方法的目录结构
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。
- 我们先不用管 baseToNumber 和 baseToString 的具体实现,现在只需知道它们会分别把值转为数字和字符。
- 整理下createMathOperation函数内部返回的匿名函数的逻辑:
- 首先对用户传的2个值进行校验
- 如果2个值都是undefined,返回默认值defaultValue
- 如果第1个值不是undefined,第2个是undefind, 返回第1个值
- 如果第2个值不是undefined,第1个是undefind, 返回第2个值
- 如果其中一个值是字符串,那么两个值都使用baseToString转为字符串
- 否则都使用baseToNumber转为数字
- 然后返回操作函数的调用结果,就是
(augend, addend) => augend + addend的返回结果
- 首先对用户传的2个值进行校验
也就是说,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实现逻辑本身,那些内部的转换方法,判断类型的方法都值得我们学习。感谢大家的观看。