# 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 |
上面代码中,变量 i
是 var
命令声明的,在全局范围内都有效,所以全局只有一个变量 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 |
上面代码中,变量 i
是 let
声明的,当前的 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 除了添加 let
和 const
命令,后面章节还会提到,另外两种声明变量的方法: 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
都能取到值。
解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其转为对象。由于 undefined
和 null
无法转为对象,所以对它们进行解构赋值,都会报错。
let { prop: x } = undefined; // TypeError | |
let { prop: y } = null; // TypeError |
# 函数参与的解构赋值
函数的参数也可以使用解构赋值。
function add([x, y]){ | |
return x + y; | |
} | |
add([1, 2]); // 3 |
上面代码中,函数 add
的参数表面上是一个数组,但在传入参数的那一刻,数组参数就被解构成变量 x
和 y
。对于函数内部的代码来说,它们能感受到的参数就是 x
和 y
。
下面是另一个例子。
[[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
的参数是一个对象,通过对这个对象进行解构,得到变量 x
和 y
的值。如果解构失败, x
和 y
等于默认值。
注意,下面的写法会得到不一样的结果。
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
的参数指定默认值,而不是为变量 x
和 y
指定默认值,所以会得到与前一种写法不同的结果。
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); |
应用
- 过滤 HTML 字符串,防止用户输入恶意内容。
- 多语言转换。
- 嵌入其他语音。
# 字符串新增方法
# 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
),后面三个参数 p1
、 p2
、 p3
则依次为三个组匹配。
# at()
at()
方法接受一个整数作为参数,返回参数指定位置的字符,支持负索引(即倒数的位置)。
const str = 'hello'; | |
str.at(1) // "e" | |
str.at(-1) // "o" |
如果参数位置超出了字符串范围, at()
返回 undefined
。
# 正则的扩展
待补充正则的扩展 - ECMAScript 6 入门 (ruanyifeng.com)
# 数值的扩展
# 二进制和八进制表示法
ES6 提供了二进制和八进制数值的新的写法,分别用前缀 0b
(或 0B
)和 0o
(或 0O
)表示。
如果要将 0b
和 0o
前缀的字符串数值转为十进制,要使用 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)。
- 不能两个或两个以上的分隔符连在一起。
- 小数点的前后不能有分隔符。
- 科学计数法里面,表示指数的
e
或E
前后不能有分隔符。
数值分隔符只是一种书写便利,对于 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 |
如果参数类型不是 NaN
, Number.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^53
到 2^53
之间(不含两个端点),超过这个范围,无法精确表示这个值。ES6 引入了 Number.MAX_SAFE_INTEGER
和 Number.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()
函数必须有参数,而且参数必须可以正常转为数值,下面的用法都会报错。