一、引言
在 JavaScript 编程中,数值的处理是极为常见且关键的操作。随着 JavaScript 的不断演进,ES6 及后续版本为数值处理带来了诸多实用的扩展,让开发者能更加便捷、高效地应对各种复杂的数值场景。无论是日常的简单计算,还是涉及高精度科学计算、金融数据处理等专业领域,这些数值扩展都能发挥巨大作用,极大提升代码的质量与开发效率。接下来,就让我们深入探究 JavaScript 数值扩展的奇妙世界。
二、二进制与八进制表示法
(一)新写法介绍
ES6 带来了二进制和八进制数值表示的新写法,让我们在处理这类数值时更加直观、便捷。二进制数值现在可以用 0b(或 0B)作为前缀,后面紧跟由 0 和 1 组成的序列;八进制数值则用 0o(或 0O)开头,后面接上 0 到 7 的数字组合。这里的大小写字母 b 和 o 均可使用,例如:
0b111110111 === 503; // true
0B111110111 === 503; // true
0o767 === 503; // true
0O767 === 503; // true
需要注意的是,从 ES5 开始,在严格模式下八进制就不再允许使用前缀 0 表示,ES6 进一步明确要使用前缀 0o。在非严格模式下,旧的八进制前缀 0 表示法可能还会被部分兼容,但为了遵循规范和避免潜在问题,建议统一使用新的 0o 前缀:
// 非严格模式
(function() {console.log(0o11 === 011);
})(); // true// 严格模式
(function() {'use strict';console.log(0o11 === 011);
})(); // Uncaught SyntaxError: Octal literals are not allowed in strict mode.
(二)转换为十进制
当我们遇到以 0b 或 0o 前缀表示的字符串数值,想要转换为十进制进行常规运算或处理时,就要使用 Number 方法。它就像是一把万能钥匙,能轻松解锁不同进制数值转换的大门,例如:
Number('0b111'); // 7
Number('0o10'); // 8
这种转换在很多场景下都非常实用,比如在处理底层二进制数据的交互,或者特定八进制权限设置的解析时,能快速将数据转换为我们熟悉的十进制进行后续操作。
三、Number 对象的新增方法
(一)Number.isFinite ()
Number.isFinite() 是一个极为实用的方法,它的核心功能是检查传入的值是否为有限的数值。在 JavaScript 中,我们常常会遇到需要判断一个数值是否合理、是否处于可处理范围的情况,这时候它就派上用场了。与传统的全局函数 isFinite() 相比,它有着显著的区别。传统的 isFinite() 在判断时,会先尝试将传入的非数值类型的值转换为数值,然后再进行有限性的判断,这就可能导致一些意想不到的结果。而 Number.isFinite() 则非常 “纯粹”,它只对数值类型有效,如果传入的不是数值,会直接返回 false。来看下面的示例:
Number.isFinite(1); // true
Number.isFinite(0.1); // true
Number.isFinite(NaN); // false,NaN 不是有限值
Number.isFinite(Infinity); // false,正无穷不是有限值
Number.isFinite(-Infinity); // false,负无穷不是有限值
Number.isFinite('foo'); // false,非数值直接返回 false
Number.isFinite('15'); // false,即使看起来像数值的字符串也返回 false
Number.isFinite(true); // false,布尔值同样返回 false
(二)Number.isNaN ()
Number.isNaN() 专注于解决数值判断中的一个痛点 —— 准确判断一个值是否为 NaN(Not a Number)。在 JavaScript 的数值运算中,NaN 常常作为一种特殊的 “错误标识” 出现,比如 0 / 0 的结果就是 NaN。与传统的全局函数 isNaN() 不同,Number.isNaN() 不会进行隐式的类型转换。全局 isNaN() 在判断前,会先尝试将非数值转换为数值,这就使得一些非 NaN 的非数值也可能被误判。而 Number.isNaN() 只有在传入的值本身就是 NaN 且为数值类型时,才会返回 true,其他情况一概返回 false,例如:
Number.isNaN(NaN); // true
Number.isNaN('true'/0); // true,这是一个产生 NaN 的运算
Number.isNaN(undefined); // false,不会被误判为 NaN
Number.isNaN({}); // false
Number.isNaN('true'); // false
Number.isNaN('NaN'); // false,字符串 'NaN' 不是真正的 NaN
(三)Number.parseInt () 与 Number.parseFloat ()
ES6 做出了一个巧妙的改进,将原本的全局方法 parseInt() 和 parseFloat() 移植到了 Number 对象上。这么做的目的是逐步减少全局方法的使用,让 JavaScript 的模块化程度更高,代码结构更加清晰。这两个方法移植后,它们的行为与在 ES5 中的表现完全一致,开发者可以无缝切换使用。
- Number.parseInt():用于从字符串中解析出整数。当不指定进制时,默认按照十进制进行解析。它会忽略字符串开头的空白字符,一旦遇到非数字字符(除了指定进制前缀的字符),就停止解析并返回已经解析到的整数部分,如果第一个字符就不是有效数字字符,则返回 NaN,例如:
Number.parseInt('12.34'); // 12,忽略小数部分 Number.parseInt(12.34); // 12,会先将数值转换为字符串再解析,同样得到 12 Number.parseInt('0011', 2); // 3,按照二进制解析字符串 '0011',得到十进制的 3 Number.parseInt === parseInt; // true,证明是同一个函数
- Number.parseFloat():专注于把字符串解析成浮点数。它同样会忽略开头的空白字符,遇到第一个无效的浮点数字符(比如第二个小数点)就停止解析,返回前面解析到的浮点数,如果无法解析出有效的浮点数,则返回 NaN,例如:
Number.parseFloat('123.45'); // 123.45 Number.parseFloat('123.45abc'); // 123.45,忽略后面的非数字字符 Number.parseFloat('abc'); // NaN,无法解析 Number.parseFloat === parseFloat; // true,也是同一个方法
(四)Number.isInteger ()
在很多业务场景中,我们需要明确一个值到底是不是整数,Number.isInteger() 就为此而生。它用于判断给定的参数是否为整数,这里需要注意 JavaScript 内部的一个小细节:整数和浮点数实际上采用的是同样的储存方法,所以在 JavaScript 中,1 和 1.0 被视为相同的值,都会被判定为整数。该方法在判断时,如果传入的值不是数值类型,会直接返回 false,像字符串、布尔值等非数值传入都会得到 false 的结果。不过,由于 JavaScript 浮点数的精度问题,当一个数值的精度超过 53 个二进制位时,可能会出现误判,例如:
Number.isInteger(0); // true Number.isInteger(1); // true Number.isInteger(1.0); // true Number.isInteger(1.1); // false Number.isInteger(Math.PI); // false,圆周率不是整数 Number.isInteger(NaN); // false Number.isInteger(Infinity); // false Number.isInteger(-Infinity); // false Number.isInteger("10"); // false,字符串不是整数 Number.isInteger(true); // false,布尔值不是整数 Number.isInteger(1.0000000000000001); // true,精度问题导致误判,实际上这个数在 JavaScript 中存储类似整数
(五)Number.EPSILON
在 JavaScript 进行浮点数计算时,精度问题常常像一个隐藏的 “小怪兽”,时不时跳出来捣乱。ES6 引入了 Number.EPSILON 这个极小的常量,它的目的就是为我们提供一个判断浮点数计算结果是否在可接受误差范围内的 “标尺”。简单来说,它表示 1 与大于 1 的最小浮点数之间的差值,这个差值极其微小。在实际应用中,比如判断 0.1 + 0.2 的计算结果是否等于 0.3 时,由于浮点数的二进制存储和运算规则,直接比较往往会得到 false:
0.1 + 0.2 === 0.3; // false,由于浮点数精度问题,结果并不相等
但如果我们借助 Number.EPSILON,就可以在一定的精度范围内判断它们是否足够接近:
function withinErrorMargin(a, b) {return Math.abs(a - b) < Number.EPSILON; } withinErrorMargin(0.1 + 0.2, 0.3); // true,在误差范围内认为相等
(六)安全整数与 Number.isSafeInteger ()
JavaScript 并非万能的,在处理整数时,它能够精确表示的整数范围是有限的,这个范围大致在 -2^53 到 2^53 之间(不含两个端点)。一旦超出这个范围,整数就可能无法被精确表示,从而引发一些难以察觉的计算错误。为了让开发者能轻松应对这个问题,ES6 引入了 Number.MAX_SAFE_INTEGER 和 Number.MIN_SAFE_INTEGER 这两个常量,分别代表安全整数范围的上限和下限。而 Number.isSafeInteger() 方法则像是一个 “守门员”,用于判断传入的整数是否落在这个安全范围之内。如果传入的值不是整数,或者超出了安全范围,它都会返回 false。在实际编程中,尤其是涉及到大型数值计算、金融数据处理等对精度要求极高的场景,一定要时刻警惕这个问题,确保参与运算的所有数值都在安全范围内,例如:
Number.isSafeInteger(Number.MIN_SAFE_INTEGER - 1); // false,小于最小安全整数 Number.isSafeInteger(Number.MAX_SAFE_INTEGER + 1); // false,大于最大安全整数 Number.isSafeInteger(3); // true,在安全范围内的整数 Number.isSafeInteger(1.2); // false,不是整数 Number.isSafeInteger(9007199254740990); // true,接近上限的安全整数 Number.isSafeInteger(9007199254740992); // false,超出安全上限
四、Math 对象的扩展
-
(一)Math.trunc ()
Math.trunc() 就像是一把精准的 “手术刀”,专门用于去除一个数的小数部分,只返回整数部分。当面对非数值类型的输入时,它内部会巧妙地使用 Number 方法先将其转换为数值,然后再进行截取操作。如果传入的值为空值(undefined 或 null),或者无法成功截取整数(比如传入的非数值无法转换为有效数字),就会返回 NaN。在实际编程中,当我们只关心数值的整数部分,用于索引计算、数量统计等场景时,它能让代码更加简洁、准确,例如:
Math.trunc(4.1); // 4 Math.trunc(4.9); // 4 Math.trunc(-4.1); // -4 Math.trunc(-4.9); // -4 Math.trunc(-0.1234); // -0 Math.trunc('123.456'); // 123,先将字符串转换为数值 123.456,再截取整数部分 Math.trunc(NaN); // NaN,无法截取整数 Math.trunc('foo'); // NaN,非数值无法转换为有效数字 Math.trunc(); // NaN,空值情况
这里要注意它与 Number.parseInt() 的一个小区别,在处理一些特殊的非数值类型时,比如 Boolean 值,Number.parseInt() 会按照规则将其转换为数值后再解析,而 Math.trunc() 则是先看能否转换为有效数字,不能则返回 NaN:
Number.parseInt(true); // 1,将布尔值 true 转换为数值 1 后解析 Math.trunc(true); // NaN,无法将布尔值 true 转换为有效数字进行截取
(二)Math.sign ()
在编程过程中,我们常常需要快速判断一个数到底是正数、负数还是零,Math.sign() 就是为此而生的 “裁判”。它的规则十分清晰:对于正数,返回 +1;负数返回 -1;零返回 0;特别地,对于 -0,它会返回 -0,以区分正零和负零这种特殊情况;而对于其他非数值类型,会先将其转换为数值,再按照规则判断,如果转换后是 NaN,则返回 NaN。在条件判断、数据筛选等场景中,它能让我们轻松区分不同正负性的数据,例如:
Math.sign(-5); // -1 Math.sign(5); // +1,通常显示为 1 Math.sign(0); // +0,通常显示为 0 Math.sign(-0); // -0 Math.sign(NaN); // NaN Math.sign('foo'); // NaN,先尝试将 'foo' 转换为数值,失败后返回 NaN Math.sign(); // NaN,空值情况
(三)Math.cbrt ()
当我们需要计算一个数的立方根时,Math.cbrt() 就派上了用场,它的功能简洁明了,就是精准地求出输入数值的立方根。对于非数值类型的参数,它内部会自动调用 Number 方法进行转换,确保能进行后续的立方根计算。无论是在数学计算、图形处理(如计算三维空间中立方体的边长与体积关系),还是物理模拟(如根据物体的体积计算边长)等领域,它都能提供有力的支持,例如:
Math.cbrt(-1); // -1 Math.cbrt(0); // 0 Math.cbrt(1); // 1 Math.cbrt(2); // 1.2599210498948734 Math.cbrt('8'); // 2,先将字符串 '8' 转换为数值 8,再计算立方根 Math.cbrt('hello'); // NaN,无法将 'hello' 转换为有效数字进行立方根计算
(四)Math.clz32 ()
Math.clz32() 专注于一个比较 “底层” 但又很实用的功能 —— 返回一个数的 32 位无符号整数形式的前导 0 的个数。在 JavaScript 中,整数是以 32 位二进制形式表示的,这个方法在一些涉及二进制位操作、底层数据处理的场景中非常关键。比如在网络协议解析、加密算法中,对于二进制数据的处理常常需要了解数据的位结构,它就能快速给出前导 0 的信息。它还有一个有趣的关联,与左移运算符 << 配合使用,可以实现一些高效的位运算技巧。需要注意的是,对于小数,它只考虑整数部分;对于非数值,同样会先转换为数值再进行计算,如果转换后是 NaN、+0、-0、+Infinity 或 -Infinity,则最终结果为 +0,例如:
Math.clz32(0); // 32,32 位二进制全为 0,前导 0 个数为 32 Math.clz32(1); // 31,二进制表示为 00000000000000000000000000000001,前导 0 个数为 31 Math.clz32(1000); // 22 Math.clz32(0b01000000000000000000000000000000); // 1 Math.clz32(0b00100000000000000000000000000000); // 2 Math.clz32(3.14); // 30,只考虑整数部分 3,二进制为 00000000000000000000000000000011,前导 0 个数为 30 Math.clz32('738.64'); // 22,先将字符串转换为数值 738,再计算二进制前导 0 个数 Math.clz32(NaN); // 32,转换后为特殊值,结果为 +0,32 位二进制全 0,前导 0 个数为 32
(五)Math.imul ()
Math.imul() 承担着一个特殊的乘法使命,它返回两个 32 位带符号整数相乘的结果,并且返回值同样是一个 32 位的带符号整数。在 JavaScript 中,普通的乘法运算对于大数值可能会出现精度问题,因为 JavaScript 的 Number 类型是基于 64 位双精度浮点数存储的。而 Math.imul() 则按照 32 位整数规则进行运算,在一些对整数乘法精度有严格要求的场景,比如游戏开发中的物理碰撞计算(涉及坐标、速度等整数运算)、图形渲染中的像素坐标计算,它能确保结果在 32 位整数范围内的准确性,例如:
Math.imul(2, 4); // 8 Math.imul(-1, 8); // -8 Math.imul(-2, -2); // 4 // 对比普通乘法 Math.imul(0x7fffffff, 2); // 2,32 位有符号整数相乘,正确处理溢出,返回低位值 (0x7fffffff * 2); // -2,普通乘法导致溢出,结果错误
(六)Math.fround ()
Math.fround() 专注于浮点数的精度转换,它的作用是返回一个数的单精度浮点数形式。对于整数来说,它的返回结果与原数基本没有不同,因为整数在单精度和双精度下表示较为一致。但对于那些无法用 64 位二进制精度精确表示的小数,它就发挥大作用了,会返回最接近这个小数的单精度浮点数。在一些对浮点数精度要求不高、更注重性能的图形处理(如 WebGL 中的简单图形绘制)、音频处理(如音频频率的初步处理)场景中,使用单精度浮点数可以减少计算量,提高性能,例如:
Math.fround(0); // 0 Math.fround(1); // 1 Math.fround(1.337); // 1.3370000123977661,将双精度小数转换为最接近的单精度浮点数 Math.fround(1.5); // 1.5 Math.fround(NaN); // NaN
(七)Math.hypot ()
Math.hypot() 提供了一种便捷的计算方式,它能够返回所有参数的平方和的平方根。在数学计算、物理模拟(如计算合力的大小、两点之间的距离)等领域,经常需要用到这个功能。它的设计非常智能,对于非数值参数,会先尝试将其转换为数值,如果其中有一个参数无法成功转换为数值,就会直接返回 NaN,确保计算的准确性和有效性,例如:
Math.hypot(3, 4); // 5,即 Math.sqrt(3 * 3 + 4 * 4),勾股定理计算直角三角形斜边 Math.hypot(3, 4, 5); // 7.0710678118654755,计算多个数的平方和的平方根 Math.hypot(); // 0,没有参数时返回 0 Math.hypot(NaN); // NaN,只要有一个参数无法转换为数值就返回 NaN Math.hypot(3, 4, 'foo'); // NaN,字符串 'foo' 无法转换为数值 Math.hypot(3, 4, '5'); // 7.0710678118654755,先将字符串 '5' 转换为数值 5 再计算 Math.hypot(-3); // 3,相当于 Math.sqrt((-3) * (-3))## 五、指数运算符 ![need_search_image_by_title]()ES7 为我们带来了一个简洁且强大的指数运算符(`**`),它让幂运算变得更加直观、便捷。以往我们要计算一个数的幂次方,通常会使用 `Math.pow()` 方法,像这样:`Math.pow(2, 3)` 来表示 `2` 的 `3` 次方,结果为 `8`。现在有了指数运算符,我们可以直接写成 `2 ** 3`,同样得到 `8`,代码更加简洁易读。不过,在一些极端情况下,比如处理特别大的数值运算时,指数运算符与 `Math.pow()` 在不同 JavaScript 引擎中的实现可能会出现细微差异。以 `V8` 引擎为例,计算 `99 ** 99` 和 `Math.pow(99, 99)` 的结果,最后几位有效数字可能稍有不同: ```javascript Math.pow(99, 99) // 3.697296376497263e+197 99 ** 99 // 3.697296376497268e+197
这是因为不同实现方式在处理大数的精度、舍入策略等方面存在细微差别,但在绝大多数日常开发场景中,这些差异基本可以忽略不计。
五、指数运算符
在运算顺序方面,指数运算符具有较高的优先级,它属于二元运算符,优先级高于加法、减法等常规算术运算符,但低于一元运算符(如 ++、--、+(正号)、-(负号))。例如:
2 + 3 ** 2; // 先计算 3 ** 2 = 9,再计算 2 + 9 = 11
并且它遵循右结合律,当多个指数运算符连续出现时,会从最右边开始依次计算,比如:
2 ** 2 ** 3; // 先计算 2 ** 3 = 8,再计算 2 ** 8 = 256,相当于 2 ** (2 ** 3)
如果想要改变默认的运算顺序,可以使用括号来明确优先级:
(2 ** 2) ** 3; // 先计算 2 ** 2 = 4,再计算 4 ** 3 = 64
需要注意的是,在使用指数运算符时,其左侧的一元表达式有一定限制,只能使用 ++ 或 -- 运算符。像 -5 ** 2 这样的写法是错误的,因为不清楚负号是作用于 5 还是 5 ** 2 的结果,存在歧义。要实现类似功能,需要用括号明确意图,写成 (-5) ** 2 或者 -(5 ** 2)。
ES7 引入了指数运算符(**),这为幂运算带来了更简洁的写法,它的功能与 Math.pow() 方法类似,比如 2 ** 3 等同于 Math.pow(2, 3),结果都是 8。不过,在一些特别大的运算结果场景下,两者在 V8 引擎中的实现会出现细微差异,像 99 ** 99 和 Math.pow(99, 99) 的最后几位有效数字可能不同,这是因为底层算法在处理极大数值时的精度策略稍有差别。指数运算符具有右结合性,多个指数运算符连用时,计算顺序是从最右边开始,例如 2 ** 2 ** 3,实际是先计算 2 ** 3 得到 8,再计算 2 ** 8 结果为 256,等同于 2 ** (2 ** 3);若想改变计算顺序,可以使用括号,如 (2 ** 2) ** 3 会先算 2 ** 2 得 4,再算 4 ** 3 为 64。此外,指数运算符左侧的一元表达式有一定限制,只能使用 ++ 或 --,像 -5 ** 2 这样写会报错,因为运算顺序不明确,不清楚是对 5 取负再平方,还是对 5 ** 2 的结果取负,此时需要用括号明确意图,写成 (-5) ** 2 或 -(5 ** 2) 来分别表示不同的运算逻辑。
六、结语
JavaScript 的数值扩展为开发者提供了诸多便利,从简洁的二进制、八进制表示,到严谨的数值判断方法,再到强大的数学运算扩展,每一项都能在合适的场景中大放异彩。在日常开发中,无论是处理数据、优化算法,还是构建复杂的数学模型,这些新特性都能助我们一臂之力。积极应用这些数值扩展,不仅能让代码更加简洁、高效,还能提升程序的稳定性与准确性,建议各位开发者多多尝试,挖掘更多实用的技巧,让 JavaScript 编程更加得心应手。