# ES6

# ES6 简介

​ ECMAScript 6.0(以下简称 ES6)是 JavaScript 语言的下一代标准,已经在 2015 年 6 月正式发布了。它的目标,是使得 JavaScript 语言可以用来编写复杂的大型应用程序,成为企业级开发语言。

​ ES6 既是一个历史名词,也是一个泛指,含义是 5.1 版以后的 JavaScript 的下一代标准,涵盖了 ES2015、ES2016、ES2017 等等,而 ES2015 则是正式名称,特指该年发布的正式版本的语言标准。

ECMAScript 和 JavaScript 的关系:前者是后者的规格,后者是前者的一种实现(另外的 ECMAScript 方言还有 JScript 和 ActionScript)。日常场合,这两个词是可以互换的。

Babel 是一个广泛使用的 ES6 转码器,可以将 ES6 代码转为 ES5 代码,从而在老版本的浏览器执行。

# let 与 const

# let 命令

ES6 新增了 let 命令,用来声明变量。它的用法类似于 var ,但是所声明的变量,只在 let 命令所在的代码块内有效

var a = [];
for (var i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6](); // 10

上面代码中,变量 ivar 命令声明的,在全局范围内都有效,所以全局只有一个变量 i 。每一次循环,变量 i 的值都会发生改变,而循环内被赋给数组 a 的函数内部的 console.log(i) ,里面的 i 指向的就是全局的 i 。也就是说,所有数组 a 的成员里面的 i ,指向的都是同一个 i ,导致运行时输出的是最后一轮的 i 的值,也就是 10。

如果使用 let ,声明的变量仅在块级作用域内有效,最后输出的是 6。

var a = [];
for (let i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6](); // 6

上面代码中,变量 ilet 声明的,当前的 i 只在本轮循环有效,所以每一次循环的 i 其实都是一个新的变量,所以最后输出的是 6 。你可能会问,如果每一轮循环的变量 i 都是重新声明的,那它怎么知道上一轮循环的值,从而计算出本轮循环的值?这是因为 JavaScript 引擎内部会记住上一轮循环的值,初始化本轮的变量 i 时,就在上一轮循环的基础上进行计算。

另外, for 循环还有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域

for (let i = 0; i < 3; i++) {
  let i = 'abc';
  console.log(i);
}
// abc
// abc
// abc

上面代码正确运行,输出了 3 次 abc 。这表明 for 函数内部的变量 i 与循环变量 i 不在同一个作用域,有各自单独的作用域(同一个作用域不可使用 let 重复声明同一个变量)。

暂时性死区

​ 只要块级作用域内存在 let 命令,它所声明的变量就 “绑定”(binding)这个区域,不再受外部的影响。

var tmp = 123;
if (true) {
  tmp = 'abc'; // ReferenceError
  let tmp;
}

“暂时性死区” 也意味着 typeof 不再是一个百分之百安全的操作。

typeof x; // ReferenceError
let x;

上面代码中,变量 x 使用 let 命令声明,所以在声明之前,都属于 x 的 “死区”,只要用到该变量就会报错。因此, typeof 运行时就会抛出一个 ReferenceError

作为比较,如果一个变量根本没有被声明,使用 typeof 反而不会报错。

typeof undeclared_variable // "undefined"

不允许重复声明

let 不允许在相同作用域内,重复声明同一个变量。

// 报错
function func() {
  let a = 10;
  var a = 1;
}
// 报错
function func() {
  let a = 10;
  let a = 1;
}

因此,不能在函数内部重新声明参数。

function func(arg) {
  let arg;
}
func() // 报错
function func(arg) {
  {
    let arg;
  }
}
func() // 不报错

块级作用域与函数声明

​ 在 ES6 中:

  • 允许在块级作用域内声明函数。
  • 函数声明类似于 var ,即会提升到全局作用域或函数作用域的头部。
  • 同时,函数声明还会提升到所在的块级作用域的头部。

不推荐在块级作用域内进行函数声明。

# const 命令

const 声明一个只读的常量。一旦声明,常量的值就不能改变。

const 声明的变量不得改变值,这意味着, const 一旦声明变量,就必须立即初始化,不能留到以后赋值。

const 的作用域与 let 命令相同:只在声明所在的块级作用域内有效。

const 命令声明的常量也是不提升,同样存在暂时性死区,只能在声明的位置后面使用。

const 声明的常量,也与 let 一样不可重复声明。

本质

const 实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。故如果真的想将对象冻结,应该使用 Object.freeze 方法。

var constantize = (obj) => {
  Object.freeze(obj);
  Object.keys(obj).forEach( (key, i) => {
    if ( typeof obj[key] === 'object' ) {
      constantize( obj[key] );
    }
  });
};

# ES6 声明变量的六种方法

ES5 只有两种声明变量的方法: var 命令和 function 命令。ES6 除了添加 letconst 命令,后面章节还会提到,另外两种声明变量的方法: import 命令和 class 命令。所以,ES6 一共有 6 种声明变量的方法。

顶层对象

JavaScript 语言存在一个顶层对象,它提供全局环境(即全局作用域),所有代码都是在这个环境中运行。但是,顶层对象在各种实现里面是不统一的。

  • 浏览器里面,顶层对象是 window ,但 Node 和 Web Worker 没有 window
  • 浏览器和 Web Worker 里面, self 也指向顶层对象,但是 Node 没有 self
  • Node 里面,顶层对象是 global ,但其他环境都不支持。

获取顶层对象的方法

var getGlobal = function () {
  if (typeof self !== 'undefined') { return self; }
  if (typeof window !== 'undefined') { return window; }
  if (typeof global !== 'undefined') { return global; }
  throw new Error('unable to locate global object');
};

​ 在 ES5 之中,顶层对象的属性与全局变量是等价的。ES6 为了改变这一点,一方面规定,为了保持兼容性, var 命令和 function 命令声明的全局变量,依旧是顶层对象的属性;另一方面规定, let 命令、 const 命令、 class 命令声明的全局变量,不属于顶层对象的属性。也就是说,从 ES6 开始,全局变量将逐步与顶层对象的属性脱钩。

# 变量的解构赋值

ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)。

# 数组的解构赋值

例子:

let [foo, [[bar], baz]] = [1, [[2], 3]];
foo // 1
bar // 2
baz // 3
let [ , , third] = ["foo", "bar", "baz"];
third // "baz"
let [x, , y] = [1, 2, 3];
x // 1
y // 3
let [head, ...tail] = [1, 2, 3, 4];
head // 1
tail // [2, 3, 4]
let [x, y, ...z] = ['a'];
x // "a"
y // undefined
z // []

如果解构不成功,变量的值就等于 undefined

let [foo] = [];
let [bar, foo] = [1];
foo //undefined

另一种情况是不完全解构,即等号左边的模式,只匹配一部分的等号右边的数组。这种情况下,解构依然可以成功。

let [x, y] = [1, 2, 3];
x // 1
y // 2
let [a, [b], d] = [1, [2, 3], 4];
a // 1
b // 2
d // 4

如果等号的右边不是数组(或者严格地说,不是可遍历的结构,参见《Iterator》一章),那么将会报错。

// 报错
let [foo] = 1;
let [foo] = false;
let [foo] = NaN;
let [foo] = undefined;
let [foo] = null;
let [foo] = {};

上面的语句都会报错,因为等号右边的值,要么转为对象以后不具备 Iterator 接口(前五个表达式),要么本身就不具备 Iterator 接口(最后一个表达式)。

# 对象的解构赋值

解构不仅可以用于数组,还可以用于对象。对象的解构与数组有一个重要的不同。数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。

let { bar, foo } = { foo: 'aaa', bar: 'bbb' };
foo // "aaa"
bar // "bbb"
let { baz } = { foo: 'aaa', bar: 'bbb' };
baz // undefined

对象的解构赋值,可以很方便地将现有对象的方法,赋值到某个变量。

// 例一
let { log, sin, cos } = Math;
// 例二
const { log } = console;
log('hello') // hello

如果变量名与属性名不一致,必须写成下面这样。

let { foo: baz } = { foo: 'aaa', bar: 'bbb' };
baz // "aaa"
let obj = { first: 'hello', last: 'world' };
let { first: f, last: l } = obj;
f // 'hello'
l // 'world'

这实际上说明,对象的解构赋值是下面形式的简写。

let { foo: foo, bar: bar } = { foo: 'aaa', bar: 'bbb' };

如果要将一个已经声明的变量用于解构赋值,必须非常小心。

// 错误的写法
let x;
{x} = {x: 1};
// SyntaxError: syntax error

上面代码的写法会报错,因为 JavaScript 引擎会将 {x} 理解成一个代码块,从而发生语法错误。只有不将大括号写在行首,避免 JavaScript 将其解释为代码块,才能解决这个问题。

// 正确的写法
let x;
({x} = {x: 1});

由于数组本质是特殊的对象,因此可以对数组进行对象属性的解构。

let arr = [1, 2, 3];
let {0 : first, [arr.length - 1] : last} = arr;
first // 1
last // 3

# 字符串的解构赋值

字符串也可以解构赋值。这是因为此时,字符串被转换成了一个类似数组的对象。

const [a, b, c, d, e] = 'hello';
a // "h"
b // "e"
c // "l"
d // "l"
e // "o"

类似数组的对象都有一个 length 属性,因此还可以对这个属性解构赋值。

let {length : len} = 'hello';
len // 5

# 数值与布尔值的解构赋值

解构赋值时,如果等号右边是数值和布尔值,则会先转为对象。

let {toString: s} = 123;
s === Number.prototype.toString // true
let {toString: s} = true;
s === Boolean.prototype.toString // true

上面代码中,数值和布尔值的包装对象都有 toString 属性,因此变量 s 都能取到值。

解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其转为对象。由于 undefinednull 无法转为对象,所以对它们进行解构赋值,都会报错。

let { prop: x } = undefined; // TypeError
let { prop: y } = null; // TypeError

# 函数参与的解构赋值

函数的参数也可以使用解构赋值。

function add([x, y]){
  return x + y;
}
add([1, 2]); // 3

上面代码中,函数 add 的参数表面上是一个数组,但在传入参数的那一刻,数组参数就被解构成变量 xy 。对于函数内部的代码来说,它们能感受到的参数就是 xy

下面是另一个例子。

[[1, 2], [3, 4]].map(([a, b]) => a + b);
// [ 3, 7 ]

函数参数的解构也可以使用默认值。

function move({x = 0, y = 0} = {}) {
  return [x, y];
}
move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, 0]
move({}); // [0, 0]
move(); // [0, 0]

上面代码中,函数 move 的参数是一个对象,通过对这个对象进行解构,得到变量 xy 的值。如果解构失败, xy 等于默认值。

注意,下面的写法会得到不一样的结果。

function move({x, y} = { x: 0, y: 0 }) {
  return [x, y];
}
move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, undefined]
move({}); // [undefined, undefined]
move(); // [0, 0]

上面代码是为函数 move 的参数指定默认值,而不是为变量 xy 指定默认值,所以会得到与前一种写法不同的结果。

undefined 就会触发函数参数的默认值。

[1, undefined, 3].map((x = 'yes') => x);
// [ 1, 'yes', 3 ]

# 用途

交换变量的值

let x = 1;
let y = 2;
[x, y] = [y, x];

从函数返回多个值

// 返回一个数组
function example() {
  return [1, 2, 3];
}
let [a, b, c] = example();
// 返回一个对象
function example() {
  return {
    foo: 1,
    bar: 2
  };
}
let { foo, bar } = example();

函数参数的定义

// 参数是一组有次序的值
function f([x, y, z]) { ... }
f([1, 2, 3]);
// 参数是一组无次序的值
function f({x, y, z}) { ... }
f({z: 3, y: 2, x: 1});

提取 JSON 数据

let jsonData = {
  id: 42,
  status: "OK",
  data: [867, 5309]
};
let { id, status, data: number } = jsonData;
console.log(id, status, number);
// 42, "OK", [867, 5309]

函数参数的默认值

jQuery.ajax = function (url, {
  async = true,
  beforeSend = function () {},
  cache = true,
  complete = function () {},
  crossDomain = false,
  global = true,
  // ... more config
} = {}) {
  // ... do stuff
};

指定参数的默认值,就避免了在函数体内部再写 var foo = config.foo || 'default foo'; 这样的语句。

遍历 Map 解构

任何部署了 Iterator 接口的对象,都可以用 for...of 循环遍历。Map 结构原生支持 Iterator 接口,配合变量的解构赋值,获取键名和键值就非常方便。

const map = new Map();
map.set('first', 'hello');
map.set('second', 'world');
for (let [key, value] of map) {
  console.log(key + " is " + value);
}
// first is hello
// second is world

如果只想获取键名,或者只想获取键值,可以写成下面这样。

// 获取键名
for (let [key] of map) {
  // ...
}
// 获取键值
for (let [,value] of map) {
  // ...
}

输入模块的指定方法

const { SourceMapConsumer, SourceNode } = require("source-map");

# 字符串扩展

# 字符的 Unicode 表示法

JavaScript 共有 6 种方法可以表示一个字符。

'\z' === 'z'  // true
'\172' === 'z' // true
'\x7A' === 'z' // true
'\u007A' === 'z' // true
'\u{7A}' === 'z' // true

# 字符串的遍历器接口

for (let codePoint of 'foo') {
  console.log(codePoint)
}
// "f"
// "o"
// "o"

# 模板字符串

模板字符串(template string)是增强版的字符串,用反引号(`)标识。它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量。

// 普通字符串
`In JavaScript '\n' is a line-feed.`
// 多行字符串
`In JavaScript this is
 not legal.`
console.log(`string text line 1
string text line 2`);
// 字符串中嵌入变量
let name = "Bob", time = "today";
`Hello ${name}, how are you ${time}?`

上面代码中的模板字符串,都是用反引号表示。如果在模板字符串中需要使用反引号,则前面要用反斜杠转义。

let greeting = `\`Yo\` World!`;

大括号内部可以放入任意的 JavaScript 表达式,可以进行运算,以及引用对象属性。模板字符串之中还能调用函数。

标签模板

模板字符串可以紧跟在一个函数名后面,该函数将被调用来处理这个模板字符串。这被称为 “标签模板” 功能(tagged template)。

alert`hello`
// 等同于
alert(['hello'])

标签模板其实不是模板,而是函数调用的一种特殊形式。“标签” 指的就是函数,紧跟在后面的模板字符串就是它的参数。

但是,如果模板字符里面有变量,就不是简单的调用了,而是会将模板字符串先处理成多个参数,再调用函数。

let a = 5;
let b = 10;
tag`Hello ${ a + b } world ${ a * b }`;
// 等同于
tag(['Hello ', ' world ', ''], 15, 50);

应用

  1. 过滤 HTML 字符串,防止用户输入恶意内容。
  2. 多语言转换。
  3. 嵌入其他语音。

# 字符串新增方法

# String.fromCodePoint()

ES5 提供 String.fromCharCode() 方法,用于从 Unicode 码点返回对应字符,但是这个方法不能识别码点大于 0xFFFF 的字符。

ES6 提供了 String.fromCodePoint() 方法,可以识别大于 0xFFFF 的字符,弥补了 String.fromCharCode() 方法的不足。在作用上,正好与下面的 codePointAt() 方法相反。

# String.raw()

ES6 还为原生的 String 对象,提供了一个 raw() 方法。该方法返回一个斜杠都被转义(即斜杠前面再加一个斜杠)的字符串,往往用于模板字符串的处理方法。

如果原字符串的斜杠已经转义,那么 String.raw() 会进行再次转义。

# codePointAt()

var s = "𠮷";
s.length // 2
s.charAt(0) // ''
s.charAt(1) // ''
s.charCodeAt(0) // 55362
s.charCodeAt(1) // 57271

上面代码中,汉字 “𠮷”(注意,这个字不是 “吉祥” 的 “吉”)的码点是 0x20BB7 ,UTF-16 编码为 0xD842 0xDFB7 (十进制为 55362 57271 ),需要 4 个字节储存。对于这种 4 个字节的字符,JavaScript 不能正确处理,字符串长度会误判为 2 ,而且 charAt() 方法无法读取整个字符, charCodeAt() 方法只能分别返回前两个字节和后两个字节的值。

ES6 提供了 codePointAt() 方法,能够正确处理 4 个字节储存的字符,返回一个字符的码点。

let s = '𠮷a';
s.codePointAt(0) // 134071
s.codePointAt(1) // 57271
s.codePointAt(2) // 97

codePointAt() 方法的参数,是字符在字符串中的位置(从 0 开始)。上面代码中,JavaScript 将 “𠮷a” 视为三个字符,codePointAt 方法在第一个字符上,正确地识别了 “𠮷”,返回了它的十进制码点 134071(即十六进制的 20BB7 )。在第二个字符(即 “𠮷” 的后两个字节)和第三个字符 “a” 上, codePointAt() 方法的结果与 charCodeAt() 方法相同。

你可能注意到了, codePointAt() 方法的参数,仍然是不正确的。比如,上面代码中,字符 a 在字符串 s 的正确位置序号应该是 1,但是必须向 codePointAt() 方法传入 2。解决这个问题的一个办法是使用 for...of 循环,因为它会正确识别 32 位的 UTF-16 字符。

let s = '𠮷a';
for (let ch of s) {
  console.log(ch.codePointAt(0).toString(16));
}
// 20bb7
// 61

codePointAt() 方法是测试一个字符由两个字节还是由四个字节组成的最简单方法。

function is32Bit(c) {
  return c.codePointAt(0) > 0xFFFF;
}
is32Bit("𠮷") // true
is32Bit("a") // false

# normalize()

许多欧洲语言有语调符号和重音符号。为了表示它们,Unicode 提供了两种方法。一种是直接提供带重音符号的字符,比如 Ǒ (\u01D1)。另一种是提供合成符号(combining character),即原字符与重音符号的合成,两个字符合成一个字符,比如 O (\u004F)和 ˇ (\u030C)合成 Ǒ (\u004F\u030C)。

ES6 提供字符串实例的 normalize() 方法,用来将字符的不同表示方法统一为同样的形式,这称为 Unicode 正规化。

# includes(),startsWith(),endsWith()

传统上,JavaScript 只有 indexOf 方法,可以用来确定一个字符串是否包含在另一个字符串中。ES6 又提供了三种新方法。

  • includes():返回布尔值,表示是否找到了参数字符串。
  • startsWith():返回布尔值,表示参数字符串是否在原字符串的头部。
  • endsWith():返回布尔值,表示参数字符串是否在原字符串的尾部。

# repeat()

repeat 方法返回一个新字符串,表示将原字符串重复 n 次。

'x'.repeat(3) // "xxx"
'hello'.repeat(2) // "hellohello"
'na'.repeat(0) // ""

参数如果是小数,会被取整。如果是负数或者 Infinity,会报错。(先取整,再判断报错)

# padStart(),padEnd()

ES2017 引入了字符串补全长度的功能。如果某个字符串不够指定长度,会在头部或尾部补全。 padStart() 用于头部补全, padEnd() 用于尾部补全。

'x'.padStart(5, 'ab') // 'ababx'
'x'.padStart(4, 'ab') // 'abax'
'x'.padEnd(5, 'ab') // 'xabab'
'x'.padEnd(4, 'ab') // 'xaba'

上面代码中, padStart()padEnd() 一共接受两个参数,第一个参数是字符串补全生效的最大长度,第二个参数是用来补全的字符串。

如果原字符串的长度,等于或大于最大长度,则字符串补全不生效,返回原字符串。如果省略第二个参数,默认使用空格补全长度。

# trimStart(),trimEnd()

ES2019 对字符串实例新增了 trimStart()trimEnd() 这两个方法。它们的行为与 trim() 一致, trimStart() 消除字符串头部的空格, trimEnd() 消除尾部的空格。它们返回的都是新字符串,不会修改原始字符串。

const s = '  abc  ';
s.trim() // "abc"
s.trimStart() // "abc  "
s.trimEnd() // "  abc"

上面代码中, trimStart() 只消除头部的空格,保留尾部的空格。 trimEnd() 也是类似行为。

除了空格键,这两个方法对字符串头部(或尾部)的 tab 键、换行符等不可见的空白符号也有效。

浏览器还部署了额外的两个方法, trimLeft()trimStart() 的别名, trimRight()trimEnd() 的别名。

# matchAll()

matchAll() 方法返回一个正则表达式在当前字符串的所有匹配,详见《正则的扩展》的一章。

# replaceAll()

它的用法与 replace() 相同,返回一个新字符串,不会改变原字符串。

String.prototype.replaceAll(searchValue, replacement)

上面代码中, searchValue 是搜索模式,可以是一个字符串,也可以是一个全局的正则表达式(带有 g 修饰符)。

如果 searchValue 是一个不带有 g 修饰符的正则表达式, replaceAll() 会报错。这一点跟 replace() 不同。

replaceAll() 的第二个参数 replacement 是一个字符串,表示替换的文本,其中可以使用一些特殊字符串。

  • $& :匹配的字符串。
  • $ `:匹配结果前面的文本。
  • $' :匹配结果后面的文本。
  • $n :匹配成功的第 n 组内容, n 是从 1 开始的自然数。这个参数生效的前提是,第一个参数必须是正则表达式。
  • $$ :指代美元符号 $

下面是一些例子。

// $& 表示匹配的字符串,即 `b` 本身
// 所以返回结果与原字符串一致
'abbc'.replaceAll('b', '$&')
// 'abbc'
// $` 表示匹配结果之前的字符串
// 对于第一个 `b`,$`指代`a`
// 对于第二个 `b`,$`指代`ab`
'abbc'.replaceAll('b', '$`')
// 'aaabc'
// $' 表示匹配结果之后的字符串
// 对于第一个 `b`,$' 指代 `bc`
// 对于第二个 `b`,$' 指代 `c`
'abbc'.replaceAll('b', `$'`)
// 'abccc'
// $1 表示正则表达式的第一个组匹配,指代 `ab`
// $2 表示正则表达式的第二个组匹配,指代 `bc`
'abbc'.replaceAll(/(ab)(bc)/g, '$2$1')
// 'bcab'
// $$ 指代 $
'abc'.replaceAll('b', '$$')
// 'a$c'

replaceAll() 的第二个参数 replacement 除了为字符串,也可以是一个函数,该函数的返回值将替换掉第一个参数 searchValue 匹配的文本。

这个替换函数可以接受多个参数。第一个参数是捕捉到的匹配内容,第二个参数捕捉到是组匹配(有多少个组匹配,就有多少个对应的参数)。此外,最后还可以添加两个参数,倒数第二个参数是捕捉到的内容在整个字符串中的位置,最后一个参数是原字符串。

const str = '123abc456';
const regex = /(\d+)([a-z]+)(\d+)/g;
function replacer(match, p1, p2, p3, offset, string) {
  return [p1, p2, p3].join(' - ');
}
str.replaceAll(regex, replacer)
// 123 - abc - 456

上面例子中,正则表达式有三个组匹配,所以 replacer() 函数的第一个参数 match 是捕捉到的匹配内容(即字符串 123abc456 ),后面三个参数 p1p2p3 则依次为三个组匹配。

# at()

at() 方法接受一个整数作为参数,返回参数指定位置的字符,支持负索引(即倒数的位置)。

const str = 'hello';
str.at(1) // "e"
str.at(-1) // "o"

如果参数位置超出了字符串范围, at() 返回 undefined

# 正则的扩展

待补充正则的扩展 - ECMAScript 6 入门 (ruanyifeng.com)

# 数值的扩展

# 二进制和八进制表示法

ES6 提供了二进制和八进制数值的新的写法,分别用前缀 0b (或 0B )和 0o (或 0O )表示。

如果要将 0b0o 前缀的字符串数值转为十进制,要使用 Number 方法。

# 数值分隔符

ES2021,允许 JavaScript 的数值使用下划线( _ )作为分隔符。

let budget = 1_000_000_000_000;
budget === 10 ** 12 // true

这个数值分隔符没有指定间隔的位数,也就是说,可以每三位添加一个分隔符,也可以每一位、每两位、每四位添加一个。

123_00 === 12_300 // true
12345_00 === 123_4500 // true
12345_00 === 1_234_500 // true

小数和科学计数法也可以使用数值分隔符。

// 小数
0.000_001
// 科学计数法
1e10_000

数值分隔符有几个使用注意点。

  • 不能放在数值的最前面(leading)或最后面(trailing)。
  • 不能两个或两个以上的分隔符连在一起。
  • 小数点的前后不能有分隔符。
  • 科学计数法里面,表示指数的 eE 前后不能有分隔符。

数值分隔符只是一种书写便利,对于 JavaScript 内部数值的存储和输出,并没有影响。

let num = 12_345;
num // 12345
num.toString() // 12345

# Number.isFinite(),Number.isNaN()

ES6 在 Number 对象上,新提供了 Number.isFinite()Number.isNaN() 两个方法。

Number.isFinite() 用来检查一个数值是否为有限的(finite),即不是 Infinity

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.isFinite 一律返回 false

Number.isNaN() 用来检查一个值是否为 NaN

Number.isNaN(NaN) // true
Number.isNaN(15) // false
Number.isNaN('15') // false
Number.isNaN(true) // false
Number.isNaN(9/NaN) // true
Number.isNaN('true' / 0) // true
Number.isNaN('true' / 'true') // true

如果参数类型不是 NaNNumber.isNaN 一律返回 false

它们与传统的全局方法 isFinite()isNaN() 的区别在于,传统方法先调用 Number() 将非数值的值转为数值,再进行判断,而这两个新方法只对数值有效, Number.isFinite() 对于非数值一律返回 false , Number.isNaN() 只有对于 NaN 才返回 true ,非 NaN 一律返回 false

isFinite(25) // true
isFinite("25") // true
Number.isFinite(25) // true
Number.isFinite("25") // false
isNaN(NaN) // true
isNaN("NaN") // true
Number.isNaN(NaN) // true
Number.isNaN("NaN") // false
Number.isNaN(1) // false

# Number.parseInt(),Number.parseFloat()

ES6 将全局方法 parseInt()parseFloat() ,移植到 Number 对象上面,行为完全保持不变。

// ES5 的写法
parseInt('12.34') // 12
parseFloat('123.45#') // 123.45
// ES6 的写法
Number.parseInt('12.34') // 12
Number.parseFloat('123.45#') // 123.45

这样做的目的,是逐步减少全局性方法,使得语言逐步模块化。

# Number.isInteger()

Number.isInteger() 用来判断一个数值是否为整数。

Number.isInteger(25) // true
Number.isInteger(25.1) // false

JavaScript 内部,整数和浮点数采用的是同样的储存方法,所以 25 和 25.0 被视为同一个值。

Number.isInteger(25) // true
Number.isInteger(25.0) // true

如果参数不是数值, Number.isInteger 返回 false

# Number.EPSILON

ES6 在 Number 对象上面,新增一个极小的常量 Number.EPSILON 。根据规格,它表示 1 与大于 1 的最小浮点数之间的差。

对于 64 位浮点数来说,大于 1 的最小浮点数相当于二进制的 1.00..001 ,小数点后面有连续 51 个零。这个值减去 1 之后,就等于 2 的 -52 次方。

Number.EPSILON === Math.pow(2, -52)
// true
Number.EPSILON
// 2.220446049250313e-16
Number.EPSILON.toFixed(20)
// "0.00000000000000022204"

Number.EPSILON 实际上是 JavaScript 能够表示的最小精度。误差如果小于这个值,就可以认为已经没有意义了,即不存在误差了。

# 安全整数和 Number.isSafeInteger ()

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

# Math 对象的扩展

Math.trunc()

Math.trunc 方法用于去除一个数的小数部分,返回整数部分。

对于非数值, Math.trunc 内部使用 Number 方法将其先转为数值。

对于空值和无法截取整数的值,返回 NaN

Math.sign()

Math.sign 方法用来判断一个数到底是正数、负数、还是零。对于非数值,会先将其转换为数值。

它会返回五种值。

  • 参数为正数,返回 +1
  • 参数为负数,返回 -1
  • 参数为 0,返回 0
  • 参数为 - 0,返回 -0 ;
  • 其他值,返回 NaN

# BigInt 数据类型

ES2020 引入了一种新的数据类型 BigInt(大整数),这是 ECMAScript 的第八种数据类型。BigInt 只用来表示整数,没有位数的限制,任何位数的整数都可以精确表示。为了与 Number 类型区别,BigInt 类型的数据必须添加后缀 n

BigInt 同样可以使用各种进制表示,都要加上后缀 n

BigInt 与普通整数是两种值,它们之间并不相等。

typeof 运算符对于 BigInt 类型的数据返回 bigint

BigInt 可以使用负号( - ),但是不能使用正号( + ),因为会与 asm.js 冲突。

JavaScript 原生提供 BigInt 函数,可以用它生成 BigInt 类型的数值。转换规则基本与 Number() 一致,将其他类型的值转为 BigInt。

BigInt(123) // 123n
BigInt('123') // 123n
BigInt(false) // 0n
BigInt(true) // 1n

BigInt() 函数必须有参数,而且参数必须可以正常转为数值,下面的用法都会报错。

更新于

请我喝[茶]~( ̄▽ ̄)~*

页川木子 微信支付

微信支付

页川木子 支付宝

支付宝

页川木子 贝宝

贝宝