跳转到内容

第六章 数值的扩展

本章节阮一峰老师从二进制与八进制、数值实例的方法、Math 对象的扩展、运算符、Integer 数据类型五个方面着手讨论。

二进制与八进制表示法

ES6 引入了新的进制表示法,可以使用前缀 "0b" 或 "0B" 表示二进制数值,以及使用前缀 "0o" 或 "0O" 表示八进制数值。例如,"0b111110111" 和 "0o767" 都表示十进制数值 503。

在 ES5 中,在严格模式中已经不再允许使用前缀 0 表示八进制数值。而在 ES6 中,对这一点进行了进一步的明确,要使用前缀 0o 来表示八进制数值。

在非严格模式下,可以使用前缀表示八进制数值,例如:

js
(function() {
  console.log(0o11 === 011);
})(); // true

而在严格模式下,使用前缀表示八进制数值会导致语法错误,例如:

js
(function () {
  'use strict';
  console.log(011 === 011); // "Uncaught SyntaxError: Octal literals are not allowed in strict mode."
})();

如果要将使用 "0b" 或 "0o" 前缀的字符串数值转为十进制数值,可以使用 Number 方法进行转换,例如 Number('0b111') 的结果是 7,Number('0o10') 的结果是 8。

总结,ES6 提供了更方便的进制表示法,但在严格模式下不再允许直接使用前缀表示八进制数值,需要使用 Number 方法进行转换。

Number.isFinite()、Number.isNaN()

这两个是 ES6 给 Numer 对象提供的新方法。Number.isFinite() 用于判断一个数值是否有限;Number.isNaN() 判断一个数值是否是 NaN

js
// Number.isFinite()
Number.isFinite(15) // true
Number.isFinite(0.8) // true
Number.isFinite(NaN) // false
Number.isFinite(Infinity) // false
Number.isFinite(-Infinity) // false
Number.isFinite('foo') // false
Number.isFinite('15') // false
Number.isFinite(true) // false

// Number.isNaN()
Number.isNaN(NaN) // true
Number.isNaN(8) // false
Number.isNaN('8') // false
Number.isNaN(true) // false
Number.isNaN('daodao'/'daodao') // true
Number.isNaN(NaN/9) // true

手写这两个方法的底层逻辑原理。

js
(function(global) {
  var global_isNaN = global.isNaN
  
  Object.defineProperty(Number, 'isNaN', {
    value: function isNaN(value) {
      return typeof value === 'number' && global_isNaN(value) // 判断是否是数字,调用方法获取值
    },
    configurable: true, 
		enumerable : false, 
		writable: true
  })
})(this)
js
(function(global) {
  var global_isFinite = global.isFinite
  
  Object.defineProperty(Number, 'isFinite', {
    value: function isFinite(value) {
      return typeof value === 'number' && global_isFinite(value) // 判断是否是数字,调用方法获取值
    },
    configurable: true, 
		enumerable : false, 
		writable: true
  })
})(this)

由上可看出,这两个方法只能判断数值型,非数值型直接返回 falseisNaN() 方法在只有 isNaN 值才会返回 true ,其他值都会返回 false

Number.parselnt()、Number.parseFloat()

这两个方法在 ES5 是放在全局 window 上;在 ES6 则是放到 Number 上。这么做的好处是逐步减少全局性方法,使得语言逐步模块化。本质没有任何区别。

js
// ES5
parseInt('32.1') // 32
parseFloat('32.1') // 32.1

// ES6
Number.parseInt('32.1') // 32
Number.parseFloat('32.1') // 32.1

Number.islnteger()

Number.islnteger() 方法用于判断该值是否是整数。在 javascript 中,由于整数和浮点数是同样的存储方式,因此 2 和 2.0 会视为同一个值。

js
Number.isInteger(3) // true
Number.isInteger(3.0) // true
Number.isInteger('3') // false
Number.isInteger(1.1) // false
Number.isInteger(false) // false

手写这个方法的底层逻辑原理。

js
(function (global) {
  var floor = Math.floor, isFinite = global.isFinite
  
  Object.defineProperty(Number, 'isInteger', {
    value: function(value) {
      return typeof value === 'number' && isFinite(value) && floor(value) === value
    },
    configurable: true, 
		enumerable : false,
		writable: true 
  })
})(this)

由上可以看出该方法只能检测数值型,非数值型的值直接返回 false ;且只能检测有限的数值,无限数值也会返回 false 。其判断原理是该数值向下取整是否等于其本身。

Number.EPSILON

ES6 Number 对象上面新增一个极小的常量——Number.EPSILON。

这个常量的作用在于控制误差的可接受范围。众所周知浮点数的计算一直存在误差,如果这个误差小于 Number.EPSILON,则这个误差是可以接受的误差。

js
Number.EPSILON // 2.220446049250313e-16

0.1 + 0.2 // 0.30000000000000004

0.1 + 0.2 - 0.3 // 5.551115123125783e-17

5.551115123125783e-17 < Number.EPSILON // true

安全整数和 Number. isSafelnteger()

JavaScript 能够精确表示的整数范围在 -2^532^53 之间(不含两个端点),超出这个范围就无法精确表示。ES6引入了 Number.MAX_SAFE_INTEGERNumber.MIN_SAFE_INTEGER 两个常量来表示这个范围的上下限。同时,使用 Number.isSafeInteger() 函数可以判断一个整数是否落在这个范围之内。

需要注意的是,超出安全整数范围的整数在计算过程中可能会导致不准确的结果,因此在处理整数运算时需要谨慎验证每个参与运算的值是否是安全整数。

js
// 判断一个整数是否为安全整数
console.log(Number.isSafeInteger(9007199254740993)); // false
console.log(Number.isSafeInteger(990)); // true
console.log(Number.isSafeInteger(9007199254740993 - 990)); // true

// 使用trusty函数验证整数运算结果
function trusty(left, right, result) {
  if (
    Number.isSafeInteger(left) &&
    Number.isSafeInteger(right) &&
    Number.isSafeInteger(result)
  ) {
    return result;
  } else {
    throw new RangeError('Operation cannot be trusted!');
  }
}

// 示例使用trusty函数验证整数运算
try {
  console.log(trusty(9007199254740993, 990, 9007199254740993 - 990)); // 抛出 RangeError
} catch (e) {
  console.log(e.message);
}

Math 对象扩展

Math 对象上新增了 17 个与数学相关的方法。所有这些方法都是静态方法,只能在 Math 对象上调用。

Math.trunc()

Math.trunc() 方法用于获取一个数值的整数部分,去除小数部分。若传入一个非数值型的值,该函数会先转为数值型。若无法转为数值型,则返回 NaN

js
Math.trunc(1.1) // 1
Math.trunc(1.9) // 1
Math.trunc(-1.1) // -1
Math.trunc(-1.9) // -1
Math.trunc('1.1') // 1
Math.trunc(true) // NaN
Math.trunc('abc') // NaN

下面来对于无该方法的环境部署该方法。

js
Math.trunc = Math.trunc || function(num) {
  return x < 0 ? Math.ceil(x) : Math.floor(x)
}

可以看到,其原理是大于0的正数,向下取整;小于0的负数,向上取整。

Math.sign()

Math.sign() 用于判断一个数值是正数、负数还是零。对于非数值型的值,会先转为数值。其中会有以下五种情况:

  • 正数,返回 +1
  • 负数,返回 -1
  • 0,返回 0
  • -0,返回 -0
  • 其他值,返回 NaN
js
Math.sign(1.1) // +1
Math.sign(-2.9) // -1
Math.sign(-0) // -0
Math.sign(0) // 0
Math.sign('500') // 1
Math.sign('foo') // NaN
Math.sign(NaN) // NaN

下面来对于无该方法的环境部署该方法。

js
Math.sign = Math.sign || function(num) {
  x = +x
  if (x === 0 || isNaN(x)) {
    return x
  }
  return x > 0 ? +1 : -1
}

可以看到,其原理是先把值转为数值型,遇到 0 或 NaN ,直接返回;再判断数值是否大于0。

Math.cbrt()

Math.cbrt() 方法用于计算一个数的立方根,在处理非数值参数时会先将其转为数值。

js
Math.cbrt(1) // 1
Math.cbrt(-1) // -1
Math.cbrt(0) // 0
Math.cbrt('8') // 2
Math.cbrt('foo') // NaN

下面来给没有该方法的环境配置该方法。

js
Math.cbrt = Math.cbrt || function(num) {
  var y = Math.pow(Math.abs(num), 1/3); // 取绝对值后,计算其立方根
  return num < 0 ? -y : y;
}

Math.clz32()

JavaScript 中的 Math.clz32 方法用于返回一个32位无符号整数的二进制表示中前导的零的个数。即从左边开始连续的零的个数。

对于小数,Math.clz32 方法只考虑整数部分;对于空值或其他类型的值,会先将它们转为数值再进行计算。此外,还提到了 Math.clz32 方法与左移运算符(<<)的直接关联。

javascript
console.log(Math.clz32(0)); // 32
console.log(Math.clz32(1)); // 31
console.log(Math.clz32(1000)); // 22
console.log(Math.clz32(0b01000000000000000000000000000000)); // 1
console.log(Math.clz32(0b00100000000000000000000000000000)); // 2
console.log(Math.clz32(3.2)); // 30
console.log(Math.clz32(1 << 29)); // 2

console.log(Math.clz32(null)); // 32
console.log(Math.clz32('foo')); // 32
console.log(Math.clz32(NaN)); // 32
console.log(Math.clz32([])); // 32
console.log(Math.clz32({})); // 32
console.log(Math.clz32()); // 32

Math.imul()

JavaScript 中的 Math.imul 方法用于返回两个数以32位带符号整数形式相乘的结果,同样返回一个32位的带符号整数。

Math.imul 在数值处于 2的 35次方区间内与 x*y 的值相等,主要用于解决超过53位精度限制的问题,能够正确返回乘法运算的低位数值。

js
console.log(Math.imul(2, 4)); // 8
console.log(Math.imul(-1, 8)); // -8
console.log(Math.imul(-2, -2)); // 4
console.log(Math.imul(0x7fffffff, 0x7fffffff)); // 1

总结起来,Math.imul 方法用于执行两个数的32位带符号整数形式的乘法运算,返回结果也是一个32位带符号整数。对于超过53位精度限制的乘法运算,Math.imul 方法可以返回正确的低位数值,解决了 JavaScript 精度限制导致的计算错误。

Math.fround()

JavaScript 中的 Math.fround 方法用于将一个数转换为单精度浮点数形式。对于整数,Math.fround 方法的返回结果与原数相同;而对于无法精确表示的小数,Math.fround 方法会返回最接近该小数的单精度浮点数。

javascript
console.log(Math.fround(0)); // 0
console.log(Math.fround(1)); // 1
console.log(Math.fround(1.337)); // 1.3370000123977661
console.log(Math.fround(1.5)); // 1.5
console.log(Math.fround(NaN)); // NaN

// 模拟实现Math.fround方法
Math.fround = Math.fround || function(x) { return new Float32Array([x])[0]; };

下面在没有该方法的环境模拟部署该方法。

js
Math.fround = Math.fround || function(num) {
  return new Float32Array([x])[0]
}

Math.hypot()

Math.hypot 方法返回所有参数的平方和的平方根。同样的,如果参数不是数值型,则会先转为数值型。若有一个参数无法转为数值型,则返回 NaN

js
Math.hypot(3, 4) // 5。根号(3^2 + 4^2) = 根号(9 + 16) = 根号(25) = 5
Math.hypot(3, 4, 5) // 7.0710678118654755
Math.hypot() // 0
Math.hypot(3, '4') // 5
Math.hypot(NaN) // NaN
Math.hypot(3, 4, 'foo') // NaN

对数方法

ES6 新增了四个对数方法。

  • Math.expm1()

    Math.expm1(x) 返回一个 e^x - 1 的结果,相当于 Math.exp(x) - 1 。在无该方法的环境中可以部署该方法。

    js
    Math.expm1 = Math.expm1 || function(num) {
      return Math.exp(x) - 1
    }
  • Math.log1p()

    Math.log1p(x) 方法返回 ln(x + 1) ,即 Math.log(x + 1) 。若 x 小于 -1 则返回 NaN 。在无该方法的环境中可以部署该方法。

    js
    Math.log1p = Math.log1p || function(num) {
      return Math.log(x + 1)
    }
  • Math.log10()

    Math.log10(x) 方法返回以10为底的 x 的对数。若 x 小于 0 则返回 NaN 。在无该方法的环境中可以部署该方法。

    js
    Math.log10 = Math.log10 || function(num) {
      return Math.log(x) / Math.LN10
    }
  • Math.log2()

    Math.log2(x) 方法返回以2为底的 x 的对数。若 x 小于 0 则返回 NaN 。在无该方法的环境中可以部署该方法。

    js
    Math.log2 = Math.log2 || function(num) {
      return Math.log(x) / Math.LN2
    }

双曲函数方法

ES6 新增了六个双曲线函数方法(了解即可)

  • Math.sinh(x) 返回 的双曲正弦 (hyperbolic sine)
  • Math cosh(x) 返回 的双曲余弦 (hyperbolic cosine)
  • Math tanh(x) 返回 的双曲正切 (hyperbolic tangent)
  • Math.asinh(x) 返回 的反双曲正弦 (inverse hyperbolic sine)
  • Math acosh(x) 返回 的反双曲余弦 (inverse hyperbolic cosine)
  • Math.atanh(x) 返回 的反双曲正切 (inverse hyperbolic tangent)

Math.signbit()

Math.sign 方法用于判断一个数的正负,但在参数为 -0 时会返回 -0,这种情况下不太有用。由于 JavaScript 内部使用64位浮点数表示数值,IEEE 754标准规定第一位是符号位,表示正数或负数,因此存在+0和-0两种零。

针对这种情况,提案中引入了 Math.signbit 方法,用来判断一个数的符号位是否已经设置。该方法的算法如下:

  • 如果参数是 NaN ,返回 false
  • 如果参数是 -0,返回 true
  • 如果参数是负值,返回 true
  • 其他情况返回 false
javascript
console.log(Math.signbit(2)); // false
console.log(Math.signbit(-2)); // true
console.log(Math.signbit(0)); // false
console.log(Math.signbit(-0)); // true

指数运算符

指数运算符可以直接计算幂运算,还可以与等号结合形成新的赋值运算符 **= ,用于对变量进行幂运算赋值操作。

在V8引擎中,指数运算符与Math.pow 方法的实现存在细微差异,特别是在处理特别大的运算结果时。举例来说,当使用 Math.pow(99, 99)99 ** 99 进行运算时,由于计算精度的不同,两者得到的最后一位有效数字可能会有差异。

javascript
// 使用指数运算符和赋值运算符的示例
let a = 1.5;
a **= 2; // 等同于 a = a * a;
console.log(a); // 输出 2.25

let b = 4;
b **= 3; // 等同于 b = b * b * b;
console.log(b); // 输出 64

// 比较指数运算符和Math.pow在处理大数值时的差异
console.log(Math.pow(99, 99)); // 输出 1.1369729637649726e+197
console.log(99 ** 99); // 输出 1.1369729637649727e+197

Integer 数据类型

JavaScript 中数字都以64位浮点数形式保存,导致整数精确度仅限于53位。为了解决这一问题,一个提案引入了新的数据类型Integer(整数),用于精确表示任意位数的整数,不受位数限制。

为了区别于Number类型,Integer类型的数据需要使用后缀 "n" 来表示。二进制 八进 、十六进制的表示法都要加上后缀 "n"

js
// 使用Integer类型的示例
let a = 1n + 2n; // 3n
console.log(a);

// 不同进制下的Integer表示
console.log(0b11n); // 3n (二进制)
console.log(0o777n); // 511n (八进制)
console.log(0xFFn); // 255n (十六进制)

// 使用Integer对象生成Integer类型的数值
console.log(Integer(123)); // 123n
console.log(Integer('123')); // 123n
console.log(Integer(false)); // 0n
console.log(Integer(true)); // 1n

// 以下用法会报错
// new Integer() // TypeError
// Integer(undefined) // TypeError
// Integer(null) // TypeError
// Integer('123n') // SyntaxError
// Integer('abc') // SyntaxError

在数学运算方面,Integer类型的加、减、乘和幂运算符与Number类型的行为相似。但是除法运算符 / 将舍去小数部分,返回一个整数。

js
let a = 9n + 5n; // 14n (加法)
let b = 11n - 6n; // 5n (减法)

// 乘法运算符
let c = 6n * 7n; // 42n

// 指数运算符
let d = 2n ** 3n; // 8n

// 除法运算符
let e = 10n / 3n; // 3n (舍去小数部分)

// 不允许的操作
// let f = 7n >> 1; // 报错,不允许使用右移位运算符
// let g = +9n; // 报错,不允许一元求正运算符

// 不允许Integer类型与Number类型进行混合运算
// let h = 5n + 1; // 报错

// 相等运算符
// let i = 0n == 0; // 报错,不允许混合使用
// let j = 0n === 0; // true (精确相等运算符)

总结

本章节的内容主要涵盖了ES6中关于数值表示法、Math对象扩展、整数数据类型等方面的知识点。

其中,介绍了二进制与八进制表示法、Number.isFinite()、Number.isNaN()、Number.parseInt()、Number.parseFloat()、Number.isInteger()、Number.EPSILON、安全整数与Number.isSafeInteger()、Math对象扩展方法、指数运算符以及Integer数据类型的使用方法。

整个文本内容较为详细地介绍了这些知识点,并包含了示例和方法的手写实现,能够帮助读者更好地理解和掌握这些内容。