功能概述
toNumber 函数是 Lodash 中用于将各种类型的值转换为数字的工具函数。它能处理多种输入类型,包括原始数字、Symbol、对象、字符串等,并尝试将它们转换为有效的数字。如果转换失败,则返回 NaN。
源码实现
js
function toNumber(value) {
if (typeof value == "number") {
return value;
}
if (isSymbol(value)) {
return NAN;
}
if (isObject(value)) {
var other = typeof value.valueOf == "function" ? value.valueOf() : value;
value = isObject(other) ? other + "" : other;
}
if (typeof value != "string") {
return value === 0 ? value : +value;
}
value = baseTrim(value);
var isBinary = reIsBinary.test(value);
return isBinary || reIsOctal.test(value)
? freeParseInt(value.slice(2), isBinary ? 2 : 8)
: reIsBadHex.test(value)
? NAN
: +value;
}
var NAN = 0 / 0;
var reIsBinary = /^0b[01]+$/i;
var reIsOctal = /^0o[0-7]+$/i;
var freeParseFloat = parseFloat,
freeParseInt = parseInt;
var reIsBadHex = /^[-+]0x[0-9a-f]+$/i;
实现原理解析
整体思路
toNumber 函数采用了一种逐步判断和转换的策略,按照不同的数据类型采用相应的转换方法。主要处理以下几种情况:
- 原始数字类型直接返回
- Symbol 类型返回 NaN
- 对象类型尝试转换为原始值
- 字符串类型处理特殊格式(二进制、八进制、十六进制)
让我们详细看看每个判断条件:
1. typeof value == 'number'
处理原始数字类型,这是最简单的情况。
js
_.toNumber(42); // => 42
_.toNumber(3.14); // => 3.14
_.toNumber(-0); // => -0
_.toNumber(Infinity); // => Infinity
2. isSymbol(value)
处理 Symbol 类型,由于 Symbol 不能转换为数字,统一返回 NaN。
js
_.toNumber(Symbol("123")); // => NaN
_.toNumber(Symbol.iterator); // => NaN
3. isObject(value)
处理对象类型,这里采用了两步转换策略:
- 先尝试调用对象的 valueOf 方法
- 如果结果还是对象,则转换为字符串
让我们逐行分析对象转换的代码:
js
if (isObject(value)) {
// 第一步:尝试获取原始值
var other = typeof value.valueOf == "function" ? value.valueOf() : value;
// 第二步:如果还是对象则转字符串,否则使用原始值
value = isObject(other) ? other + "" : other;
}
代码解读:
-
if (isObject(value))
- 首先判断是否为对象类型- 通过 isObject 函数判断,该函数会检查 typeof 结果是否为 'object' 或 'function',并且值不为 null
- 这样可以捕获普通对象、数组、函数等复杂类型
-
typeof value.valueOf == "function" ? value.valueOf() : value
- 检查对象是否有 valueOf 方法
- valueOf 是 JavaScript 内置的对象方法,用于获取对象的原始值
- 如果 valueOf 方法存在,则调用它尝试获取原始值
- 如果不存在,则保持原对象不变
-
value = isObject(other) ? other + "" : other
- 检查 valueOf 的返回值是否还是对象
- 如果是对象,则通过
other + ""
强制转换为字符串 - 如果不是对象,则直接使用 valueOf 的返回值
- 这里的
+ ""
操作会隐式调用对象的 toString 方法
示例分析:
js
// 使用 valueOf
_.toNumber(new Number(123)); // => 123
// 解析:
// 1. new Number(123) 是对象,进入 isObject 分支
// 2. Number 对象的 valueOf 返回原始值 123
// 3. 123 不是对象,直接使用这个值
// 对象转字符串
var obj = {
toString: function () {
return "42";
},
};
_.toNumber(obj); // => 42
// 解析:
// 1. 普通对象,进入 isObject 分支
// 2. obj.valueOf() 返回对象本身
// 3. 由于还是对象,使用 toString 方法转换为字符串 "42"
// 4. 字符串 "42" 最终被转换为数字 42
// 自定义 valueOf
var customObj = {
valueOf: function () {
return 99;
},
};
_.toNumber(customObj); // => 99
// 解析:
// 1. 自定义对象,进入 isObject 分支
// 2. customObj.valueOf() 返回数字 99
// 3. 99 不是对象,直接使用这个值
这种实现方式遵循了 JavaScript 的类型转换规则,通过 valueOf 和 toString 方法的配合,确保了对象到数字的可靠转换。特别是在处理包装对象(如 new Number())和自定义对象时,能够按照预期的方式进行转换。
4. 字符串类型处理
字符串的处理是最复杂的部分,需要处理多种进制的数字表示。让我们逐行分析代码的执行过程:
js
value = baseTrim(value);
var isBinary = reIsBinary.test(value);
return isBinary || reIsOctal.test(value)
? freeParseInt(value.slice(2), isBinary ? 2 : 8)
: reIsBadHex.test(value)
? NAN
: +value;
代码解读:
-
value = baseTrim(value)
- 使用 baseTrim 函数去除字符串两端的空白字符
- baseTrim 是 Lodash 的内部函数,专门用于处理字符串修剪
- 例如:
" 123 "
会被处理成"123"
-
var isBinary = reIsBinary.test(value)
- 使用正则表达式
/^0b[01]+$/i
检测是否为二进制字符串 ^0b
匹配以 "0b" 或 "0B" 开头[01]+
匹配一个或多个 0 或 1$
确保匹配到字符串末尾i
标志使匹配大小写不敏感
- 使用正则表达式
-
isBinary || reIsOctal.test(value)
- 检查是否为二进制或八进制字符串
- 八进制使用正则
/^0o[0-7]+$/i
检测 ^0o
匹配以 "0o" 或 "0O" 开头[0-7]+
匹配一个或多个 0-7 的数字
-
freeParseInt(value.slice(2), isBinary ? 2 : 8)
- 如果是二进制或八进制,截取掉前缀("0b" 或 "0o")
- 根据进制类型选择基数(2 或 8)
- 使用 parseInt 将字符串转换为对应进制的数字
- 例如:"0b101" → "101"(二进制) → 5(十进制)
-
reIsBadHex.test(value)
- 使用正则
/^[-+]0x[0-9a-f]+$/i
检测是否为十六进制 ^[-+]
匹配可选的正负号0x
匹配十六进制前缀[0-9a-f]+
匹配十六进制数字- 注意:Lodash 认为这是无效格式,返回 NaN
- 使用正则
-
+value
- 如果不是特殊进制格式,使用一元加操作符转换
- 可以处理普通数字字符串,如 "123"、"-3.14"
- 无效数字字符串会返回 NaN
这种实现方式能够优雅地处理各种数字字符串格式:
js
// 普通数字字符串
_.toNumber("123"); // => 123
_.toNumber(" 3.14 "); // => 3.14
// 二进制
_.toNumber("0b101"); // => 5
_.toNumber("0B101"); // => 5
// 八进制
_.toNumber("0o7"); // => 7
_.toNumber("0O10"); // => 8
// 十六进制(无效格式返回 NaN)
_.toNumber("0x1f"); // => NaN
_.toNumber("-0x1f"); // => NaN
// 无效数字字符串
_.toNumber("abc"); // => NaN
_.toNumber(""); // => 0
总结
toNumber 是一个强大的类型转换函数,它通过层层判断和处理,能够优雅地处理各种类型的输入。特别是在处理字符串类型时,考虑了多种数字表示法,使其在实际应用中具有很强的适应性。不过需要注意的是,并非所有输入都能被成功转换为有效数字,在使用时要注意处理 NaN 的情况。