ES6

 

1 ECMAScript 6简介

  1.1.ESMAScript和JavaScript的关系

  1.2 ES6 与 ECMAScript 2015 的关系

  1.3 TC39委员会

  1.4 ECMAScript 的历史

  1.5 浏览器支持

2 let 和 const 命令

2.1 let 命令

2.2 块级作用域

2.3 globalthis(没搞懂)

3 变量的解构赋值

3.1 基本用法

3.2 对象的解构赋值

3.3 字符串的解构赋值

3.4 数值和布尔值的解构赋值

3.5 函数参数的解构赋值

3.6 圆括号

3.7 用途

4 字符串的扩展

5 字符串的新增方法

6 正则的扩展

7 数值的扩展

函数的扩展

数组的扩展

对象的扩展

对象的新增方法

Symbol

Set 和 Map 数据结构

Proxy

Reflect

Promise 对象

Iterator 和 for...of 循环

Generator 函数的语法

Generator 函数的异步应用

async 函数

Class 的基本语法

Class 的继承

Module 的语法

Module 的加载实现

编程风格

读懂规格

异步遍历器

ArrayBuffer

最新提案

Decorator

1. 简介

1.1 ESMAScript和JavaScript的关系

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

1.2 ES6 与 ECMAScript 2015 的关系

ES6 既是一个历史名词,也是一个泛指,含义是 5.1 版以后的 JavaScript 的下一代标准,涵盖了 ES2015、ES2016、ES2017 等等,而 ES2015 则是正式名称,特指该年发布的正式版本的语言标准。本书中提到 ES6 的地方,一般是指 ES2015 标准,但有时也是泛指“下一代 JavaScript 语言”。

1.3 任何人都可以向标准委员会(又称 TC39 委员会)提案,要求修改语言标准。ECMAScript 当前的所有提案,可以在 TC39 的官方网站GitHub.com/tc39/ecma262查看。

1.4 ECMAScript 的历史

ECMAScript 1.0 是 1997 年发布的,3.0 版是一个巨大的成功,在业界得到广泛支持,成为通行标准。2000 年,ECMAScript 4.0 开始酝酿。这个版本最后没有通过,但是它的大部分内容被 ES6 继承了。因此,ES6 制定的起点其实是 2000 年。2007 年 10 月,ECMAScript 4.0 版草案发布,本来预计次年 8 月发布正式版本。但是,各方对于是否通过这个标准,发生了严重分歧。2008 年 7 月,ECMA 开会决定,中止 ECMAScript 4.0 的开发,将其中涉及现有功能改善的一小部分,发布为 ECMAScript 3.1。会后不久,ECMAScript 3.1 就改名为 ECMAScript 5。2009 年 12 月,ECMAScript 5.0 版正式发布。Harmony 项目则一分为二,一些较为可行的设想定名为 JavaScript.next 继续开发,后来演变成 ECMAScript 6;一些不是很成熟的设想,则被视为 JavaScript.next.next。ES5 与 ES3 基本保持兼容,较大的语法修正和新功能加入,将由 JavaScript.next 完成。当时,JavaScript.next 指的是 ES6,第六版发布以后,就指 ES7。2011 年 6 月,ECMAScript 5.1 版发布,并且成为 ISO 国际标准(ISO/IEC 16262:2011)。2013 年 3 月,ECMAScript 6 草案冻结,不再添加新功能。新的功能设想将被放到 ECMAScript 7。2013 年 12 月,ECMAScript 6 草案发布。然后是 12 个月的讨论期,听取各方反馈。2015 年 6 月,ECMAScript 6 正式通过,成为国际标准。从 2000 年算起,这时已经过去了 15 年。

1.5 浏览器支持

各大浏览器的最新版本,对 ES6 的支持可以查看kangax.github.io/compat-table/es6/。添加带图片的dialog

2 let const

2.1 let 命令

不存在变量提升

暂时性死区

不允许重复声明

let声明的变量只在它所在的代码块有效。let声明的i只在本轮循环有效,所以每一次循环的i其实都是一个新的变量。而var声明的i,指向的是同一个i。for循环还有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域。

for循环的计数器,就很合适使用let命令。

var命令会发生“变量提升”现象,即变量可以在声明之前使用,值为undefined。let所声明的变量一定要在声明后使用,否则报错。

ES6 明确规定,如果区块中存在let和const命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。

如果一个变量根本没有被声明,使用typeof反而不会报错。使用let声明,在声明之前使用typeof会报错。

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

2.2 块级作用域

全局作用域、函数作用域、块级作用域(新增)

为什么需要块级作用域?第一种场景,内层变量可能会覆盖外层变量。第二种场景,用来计数的循环变量泄露为全局变量。

ES6 的块级作用域

允许块级作用域的任意嵌套。内层作用域可以定义外层作用域的同名变量。

块级作用域与函数声明

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

const foo = Object.freeze({});

声明变量的方法:var、function(ES5)、const、let、import、class(ES6)

2.3 globalthis(没搞懂)

3. 解构赋值

3.1 基本用法

可以从数组中提取值,按照对应位置,对变量赋值。如果解构不成功,变量的值就等于undefined。不完全解构依然可以成功。

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

解构赋值允许指定默认值。只有当一个数组成员严格(===)等于undefined,默认值才会生效。

如果默认值是一个表达式,那么这个表达式是惰性求值的,即只有在用到默认值的时候,才会求值。

默认值可以引用解构赋值的其他变量,但该变量必须已经声明。

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

3.2 对象的解构赋值

对象的解构与数组有一个重要的不同。数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。变量没有对应的同名属性,导致取不到值,解构失败,最后等于undefined。

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

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

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

foo是匹配的模式,baz才是变量。真正被赋值的是变量baz,而不是模式foo。

 与数组一样,解构也可以用于嵌套结构的对象。

如果解构模式是嵌套的对象,而且子对象所在的父属性不存在,那么将会报错。

注意,对象的解构赋值可以取到继承的属性。

对象的解构也可以指定默认值。默认值生效的条件是,对象的属性值严格等于undefined。

注意:如果要将一个已经声明的变量用于解构赋值(对象),将整个解构赋值语句,放在一个圆括号里面,就可以正确执行。

   解构赋值允许等号左边的模式之中,不放置任何变量名。

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

3.3 字符串的解构赋值

字符串也可以解构赋值。字符串被转换成类似数组的对象。

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

3.4 数值和布尔值的解构赋值

解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其转为对象,例如对象会有tostring属性。undefined和null无法转为对象,所以对它们进行解构赋值,都会报错。

3.5 函数参数的解构赋值

function move({x = 0, y = 0} = {})//为变量x、y指定默认值,当解构失败时,x、y等于默认值
function move({x, y} = { x: 0, y: 0 }) //为函数参数指定默认值,当没有传入函数参数时(例:move()),传入参数{0,0}

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

3.6 圆括号

以下三种解构赋值不得使用圆括号。

(1)变量声明语句

(2)函数参数:函数参数也属于变量声明,因此不能带有圆括号。

  (3)  赋值语句的模式

可以使用圆括号的情况只有一种:赋值语句的非模式部分,可以使用圆括号。

  1. [(b)] = [3]; // 正确
  2. ({ p: (d) } = {}); // 正确
  3. [(parseInt.prop)] = [3]; // 正确

首先它们都是赋值语句,而不是声明语句;其次它们的圆括号都不属于模式的一部分。第一行语句中,模式是取数组的第一个成员,跟圆括号无关;第二行语句中,模式是p,而不是d;第三行语句与第一行语句的性质一致。

3.7 用途

(1)交换变量的值

(2)从函数返回多个值

  函数只能返回一个值,如果要返回多个值,只能将它们放在数组或对象里返回。有了解构赋值,取出这些值就非常方便。

(3)函数参数的定义

  解构赋值可以方便地将一组参数与变量名对应起来。

(4)提取 JSON 数据

区别 JSON Javascript
含义 仅仅是一种数据格式 表示类的实例
传输 可以跨平台数据传输,速度快 不能传输
表现

1.键值对方式,键必须加双引号

2.值不能是方法函数,不能是undefined/NaN(这是最主要的区别)

1.键值对方式,键不加引号

2.值可以是函数、对象、字符串、数字、boolean 等

相互转换

Json转换Js对象

1.JSON.parse(JsonStr);(不兼容IE7)

2.eval("("+jsonStr+")");(兼容所有浏览器,但不安全,会执行json里面的表达式?)

js对象转换Json

JSON.stringify(jsObj);

其他

调用JSON官网的JS,实现parse和stringify在各个浏览器的兼容:总而言之你可以理解为JSON是严格的JS对象数据格式,是JavaScript原生格式,JSON属性名称必须有双引号,如果值是字符串,也必须是双引号;他从属于JS,并且在处理JSON数据时可直接使用JS内置API。

(5)函数参数的默认值

(6)遍历 Map 结构

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

for (let [key, value] of map) {
  console.log(key + " is " + value);
}
// 获取键名
for (let [key] of map) {
  // ...
}

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

(7)输入模块的指定方法

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

4. 字符串的扩展

4.1 字符的 Unicode 表示法

ES6 加强了对 Unicode 的支持,允许采用\uxxxx形式表示一个字符,其中xxxx表示字符的 Unicode 码点。这种表示法只限于码点在\u0000~\uFFFF之间的字符。超出这个范围的字符,必须用两个双字节的形式表示。如果直接在\u后面跟上超过0xFFFF的数值(比如\u20BB7),JavaScr大括号表示法与四字节的 UTF-16 编码是等价的。ipt 会理解成\u20BB+7。由于\u20BB是一个不可打印字符,所以只会显示一个空格,后面跟着一个7。如果将超出范围的整个码点放入大括号,就能正确解读该字符。有了这种表示法之后,JavaScript 共有 6 种方法可以表示一个字符。

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

4.2 字符串的遍历器接口

ES6 为字符串添加了遍历器接口(详见《Iterator》一章),使得字符串可以被for...of循环遍历。这个遍历器最大的优点是可以识别大于0xFFFF(例:0x20BB7)的码点,传统的for循环无法识别这样的码点,会认为它包含两个字符(都不可打印),而for...of循环会正确识别出这一个字符。

4.3 直接输入u2028和u2029

但是,JavaScript 规定有5个字符,不能在字符串里面直接使用,只能使用转义形式。

  • U+005C:反斜杠(reverse solidus)
  • U+000D:回车(carriage return)
  • U+2028:行分隔符(line separator)
  • U+2029:段分隔符(paragraph separator)
  • U+000A:换行符(line feed)

举例来说,字符串里面不能直接包含反斜杠,一定要转义写成\\或者\\u005c。

麻烦在于 JSON 格式允许字符串里面直接使用 U+2028(行分隔符)和 U+2029(段分隔符)。这样一来,服务器输出的 JSON 被JSON.parse解析,就有可能直接报错。

ES2019 允许 JavaScript 字符串直接输入 U+2028(行分隔符)和 U+2029(段分隔符)。const PS = eval("'\u2029'");

4.4 JSON.stringify() 的改造 

根据标准,JSON 数据必须是 UTF-8 编码。UTF-8 标准规定,0xD800到0xDFFF之间的码点,不能单独使用,必须配对使用。这是为了表示码点大于0xFFFF的字符的一种变通方法。JSON.stringify()的问题在于,它可能返回0xD800到0xDFFF之间的单个码点。为了确保返回的是合法的 UTF-8 字符,ES2019 改变了JSON.stringify()的行为。如果遇到0xD800到0xDFFF之间的单个码点,或者不存在的配对形式,它会返回转义字符串,留给应用自己决定下一步的处理。

JSON.stringify('\u{D834}') // ""\\uD834""
JSON.stringify('\uDF06\uD834') // ""\\udf06\\ud834""

4.5 模板字符串

传统输出模板:

$('#result').append(
  'There are <b>' + basket.count + '</b> ' +
  'items in your basket, ' +
  '<em>' + basket.onSale +
  '</em> are on sale!'
);

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

$('#result').append(`
  There are <b>${basket.count}</b> items
   in your basket, <em>${basket.onSale}</em>
  are on sale!
`);

如果在模板字符串中需要使用反引号,则前面要用反斜杠转义。

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

如果使用模板字符串表示多行字符串,所有的空格和缩进都会被保留在输出之中。如果你不想要这个换行,可以使用trim方法消除它。

$('#list').html(`
<ul>
  <li>first</li>
  <li>second</li>
</ul>
`.trim());

模板字符串中嵌入变量,需要将变量名写在${}之中。大括号内部可以放入任意的 JavaScript 表达式,可以进行运算,以及引用对象属性。模板字符串之中还能调用函数。

// 字符串中嵌入变量
let name = "Bob", time = "today";
`Hello ${name}, how are you ${time}?`
function fn() {
  return "Hello World";
}
`foo ${fn()} bar` // foo Hello World bar
let x = 1, y = 2;
`${x} + ${y} = ${x + y}`// "1 + 2 = 3"

如果大括号中的值不是字符串,将按照一般的规则转为字符串。比如,大括号中是一个对象,将默认调用对象的toString方法。

如果模板字符串中的变量没有声明,将报错。

如果大括号内部是一个字符串,将会原样输出。

模板字符串甚至还能嵌套:

const tmpl = addrs => `
  <table>
  ${addrs.map(addr => `
    <tr><td>${addr.first}</td></tr>
    <tr><td>${addr.last}</td></tr>
  `).join('')}
  </table>
`;

上面代码中,模板字符串的变量之中,又嵌入了另一个模板字符串,使用方法如下。

const data = [
    { first: '<Jane>', last: 'Bond' },
    { first: 'Lars', last: '<Croft>' },
];

console.log(tmpl(data));
// <table>
//
//   <tr><td><Jane></td></tr>
//   <tr><td>Bond</td></tr>
//
//   <tr><td>Lars</td></tr>
//   <tr><td><Croft></td></tr>
//
// </table>

如果需要引用模板字符串本身,在需要时执行,可以写成函数。比如将模板字符串写成了一个函数的返回值。执行这个函数,就相当于执行这个模板字符串了。

4.6 模板编译

没有看懂

4.7 标签模板

标签指的是函数名,紧跟在后面的模板字符串是它的参数,该函数将被调用来处理这个模板字符串。例如alert`123`

函数tag依次会接收到多个参数。

function tag(stringArr, value1, value2){
  // ...
}

// 等同于

function tag(stringArr, ...values){
  // ...
}
let a = 5, b = 10;
tag`Hello ${ a + b } world ${ a * b }`;
// 等同于
tag(['Hello ', ' world ', ''], 15, 50);
第一个参数是literals数组,括号中整个参数称arguments,arguments[0] = literals

传入函数的参数是literals数组,arguments是js内置的对象。每次取literals和arguments里各一个参数,将各个参数按照原来的位置拼合回去。像是literals[0]+argumnts[1]+literals[1]+arguments[2].....

“标签模板”的一个重要应用,就是过滤 HTML 字符串,防止用户输入恶意内容。

模板处理函数的第一个参数(模板字符串数组),还有一个raw属性。(例:console.log`123`)console.log接受的参数,实际上是一个数组。该数组有一个raw属性,也指向一个数组。保存的是转义后的原字符串。字符串里面的斜杠都被转义了。输出参数字符串。

4.8 模板字符串的限制

模板字符串默认会将字符串转义,导致无法嵌入其他语言。模板字符串会将\u00FF和\u{42}当作 Unicode 字符进行转义,所以解析类似\unicode会报错。ES2018 放松了对标签模板里面的字符串转义的限制。如果遇到不合法的字符串转义,就返回undefined,而不是报错,并且从raw属性上面可以得到原始字符串。

注意,这种对字符串转义的放松,只在标签模板解析字符串时生效,不是标签模板的场合(普通模板赋值),依然会报错。

5. 字符串的新增方法

5.1 String.fromCodePoint() 

ES5 提供了String.fromCharCode()方法,用于从 Unicode 码点返回对应字符,但是这个方法不能识别码点大于0xFFFF的字符。会舍弃最高位返回。0x20BB7(舍弃最高位2)最后返回码点U+0BB7对应的字符

ES6 提供了String.fromCodePoint()方法,可以识别大于0xFFFF的字符。在作用上,正好与下面的codePointAt()方法相反。如果String.fromCodePoint方法有多个参数,则它们会被合并成一个字符串返回。

注意,fromCodePoint方法定义在String对象上,而codePointAt方法定义在字符串的实例对象上。

5.2 String.raw()

该方法返回一个斜杠都被转义(即斜杠前面再加一个斜杠)的字符串,往往用于模板字符串的处理方法。如果原字符串的斜杠已经转义,那么String.raw()会进行再次转义。

String.raw()本质上是一个正常的函数,只是专用于模板字符串的标签函数。如果写成正常函数的形式,它的第一个参数,应该是一个具有raw属性的对象,且raw属性的值应该是一个数组,对应模板字符串解析后的值。

// `foo${1 + 2}bar`
// 等同于
String.raw({ raw: ['foo', 'bar'] }, 1 + 2) // "foo3bar"

5.3 实例方法:codePointAt()

JavaScript 内部,字符以 UTF-16 的格式储存,每个字符固定为2个字节。对于那些需要4个字节储存的字符(Unicode 码点大于0xFFFF的字符),JavaScript 会认为它们是两个字符。

汉字“𠮷”(注意,这个字不是“吉祥”的“吉”)的码点是0x20BB7,对于这种4个字节的字符,JavaScript 不能正确处理,s.length字符串长度会误判为2,而且charAt()方法无法读取整个字符,charCodeAt()方法只能分别返回前两个字节和后两个字节的十进制值。(??有点问题,不也是返回两个字节的值吗?这两个字节的值和另外一个方法返回的有什么不一样)

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

总之,codePointAt()方法会正确返回 32 位的 UTF-16 字符的码点。对于那些两个字节储存的常规字符,它的返回结果与charCodeAt()方法相同。

codePointAt()方法返回的是码点的十进制值,如果想要十六进制的值,可以使用toString()方法转换一下。

let s = '𠮷a';

s.codePointAt(0) // 134071
s.codePointAt(1) // 57271
s.codePointAt(2) // 97
s.codePointAt(0).toString(16) // "20bb7"
s.codePointAt(2).toString(16) // "61"
codePointAt()方法的参数,仍然是不正确的。解决这个问题的一个办法是使用for...of循环,因为它会正确识别 32 位的 UTF-16 字符。
for (let ch of s) {
  console.log(ch.codePointAt(0).toString(16));
}
// 20bb7
// 61
另一种方法也可以,使用扩展运算符(...)将字符变为数组,利用forEach进行展开运算。
let arr = [...'𠮷a']; // arr.length === 2
arr.forEach(
  ch => console.log(ch.codePointAt(0).toString(16))
);
// 20bb7
// 61

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

function is32Bit(c) {
  return c.codePointAt(0) > 0xFFFF;
}

is32Bit("𠮷") // true
is32Bit("a") // false

5.4 实例方法:normalize()

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

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

这两种表示方法,在视觉和语义上都等价,但是 JavaScript 不能识别。

'\u01D1'==='\u004F\u030C' //false
'\u01D1'.normalize() === '\u004F\u030C'.normalize()
// true

 

normalize方法可以接受一个参数来指定normalize的方式,参数的四个可选值如下。

 

  • NFC,默认参数,表示“标准等价合成”,返回多个简单字符的合成字符。所谓“标准等价”指的是视觉和语义上的等价。
  • NFD,表示“标准等价分解”,即在标准等价的前提下,返回合成字符分解的多个简单字符。
  • NFKC,表示“兼容等价合成”,返回合成字符。所谓“兼容等价”指的是语义上存在等价,但视觉上不等价,比如“囍”和“喜喜”。(这只是用来举例,normalize方法不能识别中文。)
  • NFKD,表示“兼容等价分解”,即在兼容等价的前提下,返回合成字符分解的多个简单字符。

不过,normalize方法目前不能识别三个或三个以上字符的合成。这种情况下,还是只能使用正则表达式,通过 Unicode 编号区间判断。

5.5 实例方法:includes(), startsWith(), endsWith()

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

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

这三个方法都支持第二个参数,表示开始搜索的位置。使用第二个参数n时,endsWith的行为与其他两个方法有所不同。它针对前n个字符,而其他两个方法针对从第n个位置直到字符串结束。

5.6 实例方法:repeat() 

repeat方法返回一个新字符串,表示将原字符串重复n次。参数如果是小数,会被取整(2.9=3)。如果repeat的参数是负数或者Infinity,会报错。但是,如果参数是 0 到-1 之间的小数,则等同于 0,这是因为会先进行取整运算。0 到-1 之间的小数,取整以后等于-0,repeat视同为 0。参数NaN等同于 0。如果repeat的参数是字符串,则会先转换成数字。

5.7 实例方法:padStart(),padEnd() 

ES2017 引入了字符串补全长度的功能。如果某个字符串不够指定长度,会在头部或尾部补全。padStart()用于头部补全,padEnd()用于尾部补全。方法一共接受两个参数,第一个参数是字符串补全生效的最大长度,第二个参数是用来补全的字符串。如果省略第二个参数,默认使用空格补全长度。

如果原字符串的长度,等于或大于最大长度,则字符串补全不生效,返回原字符串。

如果用来补全的字符串与原字符串,两者的长度之和超过了最大长度,则会截去超出位数的补全字符串。

padStart()的常见用途是为数值补全指定位数。

'1'.padStart(10, '0') // "0000000001"
'12'.padStart(10, '0') // "0000000012"
'123456'.padStart(10, '0') // "0000123456"

另一个用途是提示字符串格式。

'12'.padStart(10, 'YYYY-MM-DD') // "YYYY-MM-12"
'09-12'.padStart(10, 'YYYY-MM-DD') // "YYYY-09-12"

5.8 实例方法:trimStart(),trimEnd()

ES2019 对字符串实例新增了trimStart()和trimEnd()这两个方法。它们的行为与trim()一致,trimStart()消除字符串头部的空格,trimEnd()消除尾部的空格。它们返回的都是新字符串,不会修改原始字符串。除了空格键,这两个方法对字符串头部(或尾部)的 tab 键、换行符等不可见的空白符号也有效。浏览器还部署了额外的两个方法,trimLeft()是trimStart()的别名,trimRight()是trimEnd()的别名。

5.9 实例方法:matchAll() 

matchAll()方法返回一个正则表达式在当前字符串的所有匹配。

6 正则的扩展

6.1 RegExp 构造函数

在 ES5 中,RegExp构造函数的参数有两种情况。

第一种情况是,参数是字符串,这时第二个参数表示正则表达式的修饰符(flag)。

var regex = new RegExp('xyz', 'i');

第二种情况是,参数是一个正则表示式,这时会返回一个原有正则表达式的拷贝。但是,ES5 不允许此时使用第二个参数添加修饰符,否则会报错。

var regex = new RegExp(/xyz/i);

ES6 改变了第二种情况。如果RegExp构造函数第一个参数是一个正则对象,也可以使用第二个参数指定修饰符。而且,返回的正则表达式会忽略原有的正则表达式的修饰符,只使用新指定的修饰符,即覆盖原有的修饰符。

6.2 字符串的正则方法

字符串对象共有 4 个方法,可以使用正则表达式:match()、replace()、search()和split()。

ES6 将这 4 个方法,在语言内部全部调用RegExp的实例方法,从而做到所有与正则相关的方法,全都定义在RegExp对象上。

  • String.prototype.match 调用 RegExp.prototype[Symbol.match]
  • String.prototype.replace 调用 RegExp.prototype[Symbol.replace]
  • String.prototype.search 调用 RegExp.prototype[Symbol.search]
  • String.prototype.split 调用 RegExp.prototype[Symbol.split]

 

6.3 u 修饰符

ES6 对正则表达式添加了u修饰符,含义为“Unicode 模式”,用来正确处理大于\uFFFF的 Unicode 字符。也就是说,会正确处理四个字节的 UTF-16 编码。

 

/^\uD83D/u.test('\uD83D\uDC2A') // false
/^\uD83D/.test('\uD83D\uDC2A') // true

ES5 不支持四个字节的 UTF-16 编码,会将其识别为两个字符,导致第二行代码结果为true。加了u修饰符以后,ES6 就会识别其为一个字符,所以第一行代码结果为false。

一旦加上u修饰符号,就会修改下面这些正则表达式的行为。

(1)点字符

点(.)字符在正则表达式中,含义是除了换行符以外的任意单个字符。对于码点大于0xFFFF的 Unicode 字符,点字符不能识别,必须加上u修饰符。

var s = '𠮷';

/^.$/.test(s) // false
/^.$/u.test(s) // true

上面代码表示,如果不添加u修饰符,正则表达式就会认为字符串为两个字符,从而匹配失败。

(2)Unicode 字符表示法

ES6 新增了使用大括号表示 Unicode 字符,这种表示法在正则表达式中必须加上u修饰符,才能识别当中的大括号,否则会被解读为量词。

/\u{61}/.test('a') // false
/\u{61}/u.test('a') // true
/\u{20BB7}/u.test('𠮷') // true

上面代码表示,如果不加u修饰符,正则表达式无法识别\u{61}这种表示法,只会认为这匹配 61 个连续的u。

3)量词

使用u修饰符后,所有量词都会正确识别码点大于0xFFFF的 Unicode 字符。

/a{2}/.test('aa') // true
/a{2}/u.test('aa') // true
/𠮷{2}/.test('𠮷𠮷') // false
/𠮷{2}/u.test('𠮷𠮷') // true

4)预定义模式

u修饰符也影响到预定义模式,能否正确识别码点大于0xFFFF的 Unicode 字符。

/^\S$/.test('𠮷') // false
/^\S$/u.test('𠮷') // true

上面代码的\S是预定义模式,匹配所有非空白字符。只有加了u修饰符,它才能正确匹配码点大于0xFFFF的 Unicode 字符。

利用这一点,可以写出一个正确返回字符串长度的函数。

function codePointLength(text) {
  var result = text.match(/[\s\S]/gu);
  return result ? result.length : 0;
}

var s = '𠮷𠮷';

s.length // 4
codePointLength(s) // 2

(5)i 修饰符(????这和i修饰符有什么关系)

有些 Unicode 字符的编码不同,但是字型很相近,比如,\u004B与\u212A都是大写的K。

/[a-z]/i.test('\u212A') // false
/[a-z]/iu.test('\u212A') // true

上面代码中,不加u修饰符,就无法识别非规范的K字符。

(6)转义

没有u修饰符的情况下,正则中没有定义的转义(如逗号的转义\,)无效,而在u模式会报错。

/\,/ // /\,/
/\,/u // 报错

上面代码中,没有u修饰符时,逗号前面的反斜杠是无效的,加了u修饰符就报错。

6.4 RegExp.prototype.unicode 属性

正则实例对象新增unicode属性,表示是否设置了u修饰符。

const r1 = /hello/;
const r2 = /hello/u;

r1.unicode // false
r2.unicode // true

上面代码中,正则表达式是否设置了u修饰符,可以从unicode属性看出来。

6.5 y 修饰符

除了u修饰符,ES6 还为正则表达式添加了y修饰符,叫做“粘连”(sticky)修饰符。y修饰符的作用与g修饰符类似,也是全局匹配,后一次匹配都从上一次匹配成功的下一个位置开始。不同之处在于,g修饰符只要剩余位置中存在匹配就可,而y修饰符确保匹配必须从剩余的第一个位置开始匹配。实际上,y修饰符号隐含了头部匹配的标志^。

单单一个y修饰符对match方法,只能返回第一个匹配,必须与g修饰符联用,才能返回所有匹配。y需要多次调用,g不需多次调用

'a1a2a3'.match(/a\d/y) // ["a1"]
'a1a2a3'.match(/a\d/gy) // ["a1", "a2", "a3"]

y修饰符的一个应用,是从字符串提取 token(词元),y修饰符确保了匹配之间不会有漏掉的字符。

const TOKEN_Y = /\s*(\+|[0-9]+)\s*/y;
const TOKEN_G  = /\s*(\+|[0-9]+)\s*/g;
tokenize(TOKEN_Y, '3x + 4')
// [ '3' ]
tokenize(TOKEN_G, '3x + 4')
// [ '3', '+', '4' ]

上面代码中,g修饰符会忽略非法字符,而y修饰符不会,这样就很容易发现错误。

6.6 RegExp.prototype.sticky 

与y修饰符相匹配,ES6 的正则实例对象多了sticky属性,表示是否设置了y修饰符。

var r = /hello\d/y;
r.sticky // true

6.7 RegExp.prototype.flags 属性

ES6 为正则表达式新增了flags属性,会返回正则表达式的修饰符。

// ES5 的 source 属性,返回正则表达式的正文
例:/abc/ig.source      // "abc"

// ES6 的 flags 属性,返回正则表达式的修饰符
例:/abc/ig.flags          // 'gi'

6.8 s 修饰符:dotAll 模式

正则表达式中,点(.)是一个特殊字符,代表任意的单个字符,但是有两个例外。一个是四个字节的 UTF-16 字符,这个可以用u修饰符解决;另一个是行终止符。

以下四个字符属于“行终止符”。

  • U+000A 换行符(\n)
  • U+000D 回车符(\r)
  • U+2028 行分隔符(line separator)
  • U+2029 段分隔符(paragraph separator)
/foo.bar/.test('foo\nbar')  // false 不匹配\n
/foo[^]bar/.test('foo\nbar')  // true 变换一下写法可以成功,但不符合直觉

ES2018 引入s修饰符,使得.可以匹配任意单个字符。

const re = /foo.bar/s;

re.test('foo\nbar') // true 这被称为dotAll模式,即点(dot)代表一切字符。

re.dotAll //true 正则表达式还引入了一个dotAll属性,返回一个布尔值,表示该正则表达式是否处在dotAll模式。
re.flags // 's'

/s修饰符和多行修饰符/m不冲突,两者一起使用的情况下,.匹配所有字符,而^和$匹配每一行的行首和行尾。

6.9 后行断言

JavaScript 语言的正则表达式,只支持先行断言和先行否定断言,不支持后行断言和后行否定断言。ES2018 引入后行断言,V8 引擎 4.9 版(Chrome 62)已经支持。

 

“先行断言”指的是,x只有在y前面才匹配,必须写成/x(?=y)/。比如,只匹配百分号之前的数字,要写成/\d+(?=%)/。

“先行否定断言”指的是,x只有不在y前面才匹配,必须写成/x(?!y)/。比如,只匹配不在百分号之前的数字,要写成/\d+(?!%)/。

“后行断言”指的是,x只有在y后面才匹配,必须写成/(?<=y)x/。比如,只匹配美元符号之后的数字,要写成/(?<=\$)\d+/。

“后行否定断言”指的是,x只有不在y后面才匹配,必须写成/(?<!y)x/。比如,只匹配不在美元符号后面的数字,要写成/(?<!\$)\d+/。

 

“后行断言”的实现,需要先匹配/(?<=y)x/的x,然后再回到左边,匹配y的部分。这种“先右后左”的执行顺序,与所有其他正则操作相反,导致了一些不符合预期的行为。

(?????没懂)其次,“后行断言”的反斜杠引用,也与通常的顺序相反,必须放在对应的那个括号之前。

/(?<=(o)d\1)r/.exec('hodor')  // null
/(?<=\1d(o))r/.exec('hodor')  // ["r", "o"]

上面代码中,如果后行断言的反斜杠引用(\1)放在括号的后面,就不会得到匹配结果,必须放在前面才可以。因为后行断言是先从左到右扫描,发现匹配以后再回过头,从右到左完成反斜杠引用。

6.10 Unicode 属性类 

ES2018 引入了一种新的类的写法\p{...}和\P{...},\p{…}允许正则表达式匹配符合 Unicode 某种属性的所有字符。\P{…}是\p{…}的反向匹配,即匹配不满足条件的字符。

这两种类只对 Unicode 有效,所以使用的时候一定要加上u修饰符。如果不加u修饰符,正则表达式使用\p和\P会报错

Unicode 属性类要指定属性名和属性值。

\p{UnicodePropertyName=UnicodePropertyValue}

对于某些属性,可以只写属性名,或者只写属性值。

\p{UnicodePropertyName}
\p{UnicodePropertyValue}
const regexGreekSymbol = /\p{Script=Greek}/u;
regexGreekSymbol.test('π') // true

上面代码中,\p{Script=Greek}指定匹配一个希腊文字母,所以匹配π成功。 

Unicode 的各种属性非常多,所以这种新的类的表达能力非常强。

const regex = /^\p{Decimal_Number}+$/u;  指定匹配所有十进制字符,可以看到各种字型的十进制字符都会匹配成功
regex.test('𝟏𝟐𝟑𝟜𝟝𝟞𝟩𝟪𝟫𝟬𝟭𝟮𝟯𝟺𝟻𝟼') // true
'²³¹¼½¾' '㉛㉜㉝' 'ⅠⅡⅢⅣⅤⅥⅦⅧⅨⅩⅪⅫ'        \p{Number}甚至能匹配罗马数字。

上面代码中,属性类指定匹配所有十进制字符,可以看到各种字型的十进制字符都会匹配成功。

6.11 具名组匹配

正则表达式使用圆括号进行组匹配。

const RE_DATE = /(\d{4})-(\d{2})-(\d{2})/;

上面代码中,正则表达式里面有三组圆括号。使用exec方法,就可以将这三组匹配结果提取出来。

组匹配的一个问题是,每一组的匹配含义不容易看出来,而且只能用数字序号(比如matchObj[1])引用,要是组的顺序变了,引用的时候就必须修改序号。

ES2018 引入了具名组匹配,允许为每一个组匹配指定一个名字,既便于阅读代码,又便于引用。

const RE_DATE = /(\d{4})-(\d{2})-(\d{2})/;  //使用具名组匹配之前的regex
const RE_DATE = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;  //具名组匹配在圆括号内部,模式的头部添加“问号 + 尖括号 + 组名”
const matchObj = RE_DATE.exec('1999-12-31');
const year = matchObj.groups.year; // 1999
const month = matchObj.groups.month; // 12
const day = matchObj.groups.day; // 31

这样就可以在exec方法返回结果的groups属性上引用该组名,如果组的顺序变了,也不用改变匹配后的处理代码。同时,数字序号(matchObj[1])依然有效。

如果具名组as没有匹配,那么对应的groups对象属性会是undefined。并且as这个键名在groups是始终存在的。

6.12 解构赋值和替换

有了具名组匹配以后,可以使用解构赋值直接从匹配结果上为变量赋值。

let {groups: {one, two}} = /^(?<one>.*):(?<two>.*)$/u.exec('foo:bar');   //对象替换
let re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u;  //字符串替换时,使用$<组名>引用具名组。

'2015-01-02'.replace(re, '$<day>/$<month>/$<year>') // '02/01/2015',replace方法的第二个参数是字符串,也可以是函数

replace方法的第二个参数也可以是函数,该函数的参数序列如下。

'2015-01-02'.replace(re, (
   matched, // 整个匹配结果 2015-01-02
   capture1, // 第一个组匹配 2015
   capture2, // 第二个组匹配 01
   capture3, // 第三个组匹配 02
   position, // 匹配开始的位置 0
   S, // 原字符串 2015-01-02
   groups // 具名组构成的一个对象 {year, month, day}
 ) => {
 let {day, month, year} = groups;
 return `${day}/${month}/${year}`;
});

具名组匹配在原来的基础上,新增了最后一个函数参数:具名组构成的一个对象。函数内部可以直接对这个对象进行解构赋值。

如果要在正则表达式内部引用某个“具名组匹配”,可以使用\k<组名>的写法,类似反向引用。

const RE_TWICE = /^(?<word>[a-z]+)!\k<word>$/;  //用法类似 \k<> = \1
const RE_TWICE = /^(?<word>[a-z]+)!\1$/;
const RE_TWICE = /^(?<word>[a-z]+)!\k<word>!\1$/; //可以同时使用

6.12 String.prototype.matchAll

如果一个正则表达式在字符串里面有多个匹配,现在一般使用g修饰符或y修饰符,在循环里面逐一取出。

var regex = /t(e)(st(\d?))/g;
var string = 'test1test2test3';

var matches = [];
var match;
while (match = regex.exec(string)) {
  matches.push(match);
}

matches
// [
//   ["test1", "e", "st1", "1", index: 0, input: "test1test2test3"],
//   ["test2", "e", "st2", "2", index: 5, input: "test1test2test3"],
//   ["test3", "e", "st3", "3", index: 10, input: "test1test2test3"]
// ]

上面代码中,while循环取出每一轮的正则匹配,一共三轮。

目前有一个提案,增加了String.prototype.matchAll方法,可以一次性取出所有匹配。不过,它返回的是一个遍历器(Iterator),而不是数组。

const string = 'test1test2test3';

// g 修饰符加不加都可以
const regex = /t(e)(st(\d?))/g;

for (const match of string.matchAll(regex)) {
  console.log(match);
}
// ["test1", "e", "st1", "1", index: 0, input: "test1test2test3"]
// ["test2", "e", "st2", "2", index: 5, input: "test1test2test3"]
// ["test3", "e", "st3", "3", index: 10, input: "test1test2test3"]

上面代码中,由于string.matchAll(regex)返回的是遍历器,所以可以用for...of循环取出。相对于返回数组,返回遍历器的好处在于,如果匹配结果是一个很大的数组,那么遍历器比较节省资源。

遍历器转为数组是非常简单的,使用...运算符和Array.from方法就可以了。

// 转为数组方法一
[...string.matchAll(regex)]

// 转为数组方法二
Array.from(string.matchAll(regex));

7 数值的扩展

7.1 二进制和八进制表示法

ES6 提供了二进制和八进制数值的新的写法,分别用前缀0b(或0B)和0o(或0O)表示。从 ES5 开始,在严格模式之中,八进制就不再允许使用前缀0表示,ES6 进一步明确,要使用前缀0o表示。

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

Number('0b111')  // 7
Number('0o10')  // 8

7.2 Number.isFinite(), Number.isNaN()

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

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

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

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

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

7.3 Number.parseInt(), Number.parseFloat()

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

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

Number.parseInt === parseInt // true
Number.parseFloat === parseFloat // true
Number.parseFloat('123.45#') === parseFloat('123.45#') // 123.45

7.4 Number.isInteger()

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

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

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

注意,由于 JavaScript 采用 IEEE 754 标准,数值存储为64位双精度格式,数值精度最多可以达到 53 个二进制位(1 个隐藏位与 52 个有效位)。如果数值的精度超过这个限度,第54位及后面的位就会被丢弃,这种情况下,Number.isInteger可能会误判。如果一个数值的绝对值小于Number.MIN_VALUE(5E-324),即小于 JavaScript 能够分辨的最小值,会被自动转为 0。这时,Number.isInteger也会误判。

总之,如果对数据精度的要求较高,不建议使用Number.isInteger()判断一个数值是否为整数。

7.5 Number.EPSILON

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

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

Number.EPSILON === Math.pow(2, -52)   // true

引入一个这么小的量的目的,在于为浮点数计算,设置一个误差范围。我们知道浮点数计算是不精确的。0.1 + 0.2与0.3得到的结果是false。

Number.EPSILON可以用来设置“能够接受的误差范围”。比如,误差范围设为 2 的-50 次方(即Number.EPSILON * Math.pow(2, 2)),即如果两个浮点数的差小于这个值,我们就认为这两个浮点数相等。

因此,Number.EPSILON的实质是一个可以接受的最小误差范围。

7.6 安全整数和 Number.isSafeInteger()

JavaScript 能够准确表示的整数范围在-2^53到2^53之间(不含两个端点),超过这个范围,无法精确表示这个值。

Math.pow(2, 53) === Math.pow(2, 53) + 1
Math.pow(2, 53) // 9007199254740992
 // true 

上面代码中,超出 2 的 53 次方之后,一个数就不精确了。

ES6 引入了Number.MAX_SAFE_INTEGER和Number.MIN_SAFE_INTEGER这两个常量,用来表示这个范围的上下限。

Number.MAX_SAFE_INTEGER === Math.pow(2, 53) - 1
Number.MIN_SAFE_INTEGER === Math.pow(-2, 53) - 1

Number.isSafeInteger()则是用来判断一个整数是否落在这个范围之内。这个函数的实现很简单,就是跟安全整数的两个边界值比较一下。

实际使用这个函数时,需要注意。验证运算结果是否落在安全整数的范围内,不要只验证运算结果,而要同时验证参与运算的每个值。只要运算的数超出了精度范围,导致在计算机内部,以9007199254740992的形式储存。即使结果在安全整数范围内,也是错误的结果。

7.7 Math 对象的扩展

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

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

对于非数值,Math.trunc内部使用Number方法将其先转为数值(例如true为1,false为0,null为0)。

对于空值和无法截取整数的值,返回NaN(例如NaN、字符串、undefined)。

对于没有部署这个方法的环境,可以用下面的代码模拟。

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

Math.sign()方法用来判断一个数到底是正数、负数、还是零。对于非数值,会先将其转换为数值。对于那些无法转为数值的值,会返回NaN。

它会返回五种值。

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

Math.cbrt()方法用于计算一个数的立方根。对于非数值,Math.cbrt方法内部也是先使用Number方法将其转为数值。

Math.cbrt(-1) // -1
Math.cbrt(8)  // 2
Math.cbrt(2)  // 1.2599210498948734

Math.clz32()方法将参数转为 32 位无符号整数的形式,然后返回这个 32 位值里面有多少个前导 0。

Math.clz32(0) // 32  0 的二进制形式全为 0,所以有 32 个前导 0
Math.clz32(1) // 31  1 的二进制形式是0b1,只占 1 位,所以 32 位之中有 31 个前导 0;
Math.clz32(1000) // 22  1000 的二进制形式是0b1111101000,一共有 10 位,所以 32 位之中有 22 个前导 0。
Math.clz32(0b01000000000000000000000000000000) // 1
Math.clz32(0b00100000000000000000000000000000) // 2

左移运算符(<<)与Math.clz32方法直接相关。

Math.clz32(1) // 31
Math.clz32(1 << 1) // 30
Math.clz32(1 << 29) // 2

对于小数,Math.clz32方法只考虑整数部分。

对于空值或其他类型的值,Math.clz32方法会将它们先转为数值0(true为1),然后再计算。

Math.imul()方法返回两个数以 32 位带符号整数形式相乘的结果,返回的也是一个 32 位的带符号整数。

Math.imul(2, 4)   // 8
Math.imul(-1, 8)  // -8
Math.imul(-2, -2) // 4

如果只考虑最后 32 位,大多数情况下,Math.imul(a, b)与a * b的结果是相同的,即该方法等同于(a * b)|0的效果(超过 32 位的部分溢出)。之所以需要部署这个方法,是因为 JavaScript 有精度限制,超过 2 的 53 次方的值无法精确表示。这就是说,对于那些很大的数的乘法,低位数值往往都是不精确的,Math.imul方法可以返回正确的低位数值。

(0x7fffffff * 0x7fffffff)|0 // 0

上面这个乘法算式,返回结果为 0。但是由于这两个二进制数的最低位都是 1,所以这个结果肯定是不正确的,因为根据二进制乘法,计算结果的二进制最低位应该也是 1。这个错误就是因为它们的乘积超过了 2 的 53 次方,JavaScript 无法保存额外的精度,就把低位的值都变成了 0。Math.imul方法可以返回正确的值 1。

Math.imul(0x7fffffff, 0x7fffffff) // 1

Math.fround()方法返回一个数的32位单精度浮点数形式。主要作用是将64位双精度浮点数转为32位单精度浮点数。如果小数的精度超过24个二进制位,返回值就会不同于原值,否则返回值不变(即与64位双精度值一致)。对于 NaN 和 Infinity,此方法返回原值。对于其它类型的非数值,Math.fround 方法会先将其转为数值,再返回单精度浮点数。

Math.hypot()方法返回所有参数的平方和的平方根。

Math.hypot(3, 4);        // 5
Math.hypot(3, 4, 5);     // 7.0710678118654755
Math.hypot(3, 4, '5');   // 7.0710678118654755
Math.hypot(-3);          // 3

上面代码中,3 的平方加上 4 的平方,等于 5 的平方。

如果参数不是数值,Math.hypot方法会将其转为数值。空值返回0,只要有一个参数无法转为数值,就会返回 NaN。

7.8 对数方法

ES6 新增了 4 个对数相关方法。

(1) Math.expm1(x)返回 ex - 1,即Math.exp(x) - 1。

(2)Math.log1p(x)方法返回1 + x的自然对数,即Math.log(1 + x)。如果x小于-1,返回NaN。1+x为0,则返回-Infinity

(3)Math.log10(x)返回以 10 为底的x的对数。如果x小于 0,则返回 NaN。

(4)Math.log2(x)返回以 2 为底的x的对数。如果x小于 0,则返回 NaN。

7.9 双曲函数方法

ES6 新增了 6 个双曲函数方法。

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

7.10 指数运算符

ES2016 新增了一个指数运算符(**)。

这个运算符的一个特点是右结合,而不是常见的左结合。多个指数运算符连用时,是从最右边开始计算的。

// 相当于 2 ** (3 ** 2)
2 ** 3 ** 2
// 512

指数运算符可以与等号结合,形成一个新的赋值运算符(**=)。

a **= 2;
// 等同于 a = a * a;
b **= 3;
// 等同于 b = b * b * b;

注意,V8 引擎的指数运算符与Math.pow的实现不相同,对于特别大的运算结果,两者会有细微的差异。

Math.pow(99, 99)
// 3.697296376497263e+197

99 ** 99
// 3.697296376497268e+197

上面代码中,两个运算结果的最后一位有效数字是有差异的。

8 函数的扩展

8.1 函数参数的默认值基本用法

ES6 之前,不能直接为函数的参数指定默认值,只能采用变通的方法。

y = y || 'World';   //缺点是如果参数y赋值了,但是对应的布尔值为false,则该赋值不起作用

为了避免这个问题,通常需要先判断一下参数y是否被赋值(typeof y === 'undifined'),如果没有,再等于默认值。

ES6 允许为函数的参数设置默认值,即直接写在参数定义的后面。

function log(x, y = 'World')

除了简洁,ES6 的写法还有两个好处:首先,阅读代码的人,可以立刻意识到哪些参数是可以省略的,不用查看函数体或文档;其次,有利于将来的代码优化,即使未来的版本在对外接口中,彻底拿掉这个参数,也不会导致以前的代码无法运行。

参数变量是默认声明的,所以不能用let或const在函数体中再次声明。

使用参数默认值时,函数参数中不能有同名参数,会报错,没有定义默认值时使用不报错。

参数默认值是惰性求值的。每次调用函数,都会重新计算参数的值。

8.2 与解构赋值默认值结合使用

!要注意结合的概念,分辨到底有没有函数参数的默认值,还是只有对象的解构赋值默认值,使用对象的解构赋值,在传入参数但没有定义对象值时会起作用,如果使用了函数参数默认值,即便没有传参数,也会使用解构赋值默认值。

参数默认值可以与解构赋值的默认值,结合起来使用。

function foo({x, y = 5}) 
foo({}) // undefined 5
foo() // TypeError: Cannot read property 'x' of undefined

上面代码只使用了对象的解构赋值默认值,没有使用函数参数的默认值。只有当函数foo的参数是一个对象时,变量x和y才会通过解构赋值生成。如果函数foo调用时没提供参数,变量x和y就不会生成,从而报错。通过提供函数参数的默认值,就可以避免这种情况。

function foo({x, y = 5} = {}) 
foo() // undefined 5

上面代码指定,如果没有提供参数,函数foo的参数默认为一个空对象。

8.3 参数默认值的位置

通常情况下,定义了默认值的参数,应该是函数的尾参数。因为这样比较容易看出来,到底省略了哪些参数。如果非尾部的参数设置默认值,实际上这个参数是没法省略的。无法只省略该参数,而不省略它后面的参数,除非显式输入undefined。如果传入undefined,将触发该参数等于默认值,null则没有这个效果。

8.4 函数的 length 属性

指定了默认值以后,函数的length属性,将返回没有指定默认值的参数个数。也就是说,指定了默认值后,length属性将失真。后文的 rest 参数也不会计入length属性。

(function(...args) {}).length  // 0

如果设置了默认值的参数不是尾参数,即默认值参数放在前面,那么length属性也不再计入这后面的参数了。

8.5 作用域

一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域,在函数中可用。等到初始化结束,这个作用域就会消失。这种语法行为,在不设置参数默认值时,是不会出现的。

例如function(x,y=x),这个时候的y的值,和传入的x参数相等,即便有全局变量x,不会指向全局变量。而function(y=x),这个时候y的值会受到全局变量的影响,全局变量不存在则会报错。即便函数体里面重新定义了x,y也不会改变。

var x = 1;

function foo(x = x)

参数x = x形成一个单独作用域。实际执行的是let x = x,由于暂时性死区的原因,这行代码会报错”x 未定义“。

如果参数的默认值是一个函数,该函数的作用域也遵守这个规则。

总结就是,有参数默认值,则存在全局作用域、函数内部的局部作用域,函数参数之间的单独作用域。

8.6 应用

8.6.1

利用参数默认值,可以指定某一个参数不得省略,如果省略就抛出一个错误。

function throwIfMissing() {
  throw new Error('Missing parameter');
}

function foo(mustBeProvided = throwIfMissing()) {
  return mustBeProvided;
}

foo()
// Error: Missing parameter

上面代码的foo函数,如果调用的时候没有参数,就会调用默认值throwIfMissing函数,从而抛出一个错误。

从上面代码还可以看到,参数mustBeProvided的默认值等于throwIfMissing函数的运行结果(注意函数名throwIfMissing之后有一对圆括号),这表明参数的默认值不是在定义时执行,而是在运行时执行。如果参数已经赋值,默认值中的函数就不会运行。

另外,可以将参数默认值设为undefined,表明这个参数是可以省略的。

function foo(optional = undefined) { ··· }

8.6.2 rest 参数

ES6 引入 rest 参数(形式为...变量名),用于获取函数的多余参数,这样就不需要使用arguments对象了。rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中。arguments对象不是数组,而是一个类似数组的对象。所以为了使用数组的方法,必须使用Array.prototype.slice.call先将其转为数组。rest 参数就不存在这个问题,它就是一个真正的数组,数组特有的方法都可以使用。

// arguments变量的写法
function sortNumbers() {
  return Array.prototype.slice.call(arguments).sort();
}

// rest参数的写法
const sortNumbers = (...numbers) => numbers.sort();

注意,rest 参数之后不能再有其他参数(即只能是最后一个参数),否则会报错。

函数的length属性,不包括 rest 参数。

(function(a) {}).length  // 1

8.6.3 严格模式

从 ES5 开始,函数内部可以设定为严格模式。

ES2016 做了一点修改,规定只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式,否则会报错。

这样规定的原因是,函数内部的严格模式,同时适用于函数体和函数参数。但是,函数执行的时候,先执行函数参数,然后再执行函数体。这样就有一个不合理的地方,只有从函数体之中,才能知道参数是否应该以严格模式执行,但是参数却应该先于函数体执行。虽然可以先解析函数体代码,再执行参数代码,但是这样无疑就增加了复杂性。因此,标准索性禁止了这种用法,只要参数使用了默认值、解构赋值、或者扩展运算符,就不能显式指定严格模式。

两种方法可以规避这种限制。第一种是设定全局性的严格模式,这是合法的。

第二种是把函数包在一个无参数的立即执行函数里面。

8.6.4 name 属性

函数的name属性,返回该函数的函数名。

需要注意的是,ES6 对这个属性的行为做出了一些修改。如果将一个匿名函数赋值给一个变量,ES5 的name属性,会返回空字符串,而 ES6 的name属性会返回实际的函数名。

如果将一个具名函数赋值给一个变量,则 ES5 和 ES6 的name属性都返回这个具名函数原本的名字。

Function构造函数返回的函数实例,name属性的值为anonymous。

(new Function).name // "anonymous"

bind返回的函数,name属性值会加上bound前缀。

function foo() {};
foo.bind({}).name // "bound foo"

(function(){}).bind({}).name // "bound "

8.6.5 箭头函数

ES6 允许使用“箭头”(=>)定义函数。

如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分。

如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用return语句返回。

由于大括号会被引擎解释为代码块,所以如果箭头函数直接返回一个对象,必须在返回的对象外面加上括号,否则会报错。

如果箭头函数只有一行语句,且不需要返回值,可以采用下面的写法,就不用写大括号了。

let fn = () => void doesNotReturn();

箭头函数可以与变量解构结合使用。

const full = ({ first, last }) => first + ' ' + last;

箭头函数使得表达更加简洁,简化回调函数。

使用注意点:

箭头函数有几个使用注意点。

(1)函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。箭头函数导致this总是指向函数定义生效时所在的对象。

(2)不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。

(3)不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。

(4)不可以使用yield命令,因此箭头函数不能用作 Generator 函数。

上面四点中,第一点尤其值得注意。this对象的指向是可变的,但是在箭头函数中,它是固定的。

箭头函数可以让this指向固定化,这种特性很有利于封装回调函数。

this指向的固定化,并不是因为箭头函数内部有绑定this的机制,实际原因是箭头函数根本没有自己的this,导致内部的this就是外层代码块的this。正是因为它没有this,所以也就不能用作构造函数。

除了this,以下三个变量在箭头函数之中也是不存在的,指向外层函数的对应变量:arguments、super、new.target。

由于箭头函数没有自己的this,所以当然也就不能用call()、apply()、bind()这些方法去改变this的指向。内部的this指向外部的this。

不适用场合:

由于箭头函数使得this从“动态”变成“静态”,下面两个场合不应该使用箭头函数。

第一个场合是定义对象的方法,且该方法内部包括this。obj.a是一个箭头函数定义的方法,里面用到this,这会使得this指向全局对象,这是因为对象不构成单独的作用域,导致a箭头函数定义时的作用域就是全局作用域。

第二个场合是需要动态this的时候,也不应使用箭头函数。比如动态获取dom元素定义的click函数如果是箭头函数,里面的this就是全局对象。如果改成普通函数,this就会动态指向被点击的按钮对象。

另外,如果函数体很复杂,有许多行,或者函数内部有大量的读写操作,不单纯是为了计算值,这时也不应该使用箭头函数,而是要使用普通函数,这样可以提高代码可读性。

嵌套的箭头函数,箭头函数内部,还可以再使用箭头函数。

8.6 尾调用优化

尾调用是函数式编程的一个重要概念,就是指某个函数的最后一步是只调用另一个函数,不进行任何赋值操作,一定要返回这个函数。尾调用不一定出现在函数尾部,只要是最后一步操作即可。因此一个函数内部可以有多个尾调用(if else)。

尾调用优化

尾调用之所以与其他调用不同,就在于它的特殊的调用位置。

我们知道,函数调用会在内存形成一个“调用记录”,又称“调用帧”(call frame),保存调用位置和内部变量等信息。如果在函数A的内部调用函数B,那么在A的调用帧上方,还会形成一个B的调用帧。等到B运行结束,将结果返回到A,B的调用帧才会消失。如果函数B内部还调用函数C,那就还有一个C的调用帧,以此类推。所有的调用帧,就形成一个“调用栈”(call stack)。

尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用帧,取代外层函数的调用帧就可以了。(???)

尾调用优化即只保留内层函数的调用帧,在函数结束时调用下一个函数,会只保留下一个函数的调用帧。如果所有函数都是尾调用,那么完全可以做到每次执行时,调用帧只有一项,这将大大节省内存。这就是“尾调用优化”的意义。

注意,只有不再用到外层函数的内部变量,内层函数的调用帧才会取代外层函数的调用帧,否则就无法进行“尾调用优化”。

注意,目前只有 Safari 浏览器支持尾调用优化,Chrome 和 Firefox 都不支持。

尾递归

函数调用自身,称为递归。如果尾调用自身,就称为尾递归。

递归非常耗费内存,因为需要同时保存成千上百个调用帧,很容易发生“栈溢出”错误(stack overflow)。但对于尾递归来说,由于只存在一个调用帧,所以永远不会发生“栈溢出”错误。

function factorial(n) {
  if (n === 1) return 1;
  return n * factorial(n - 1);
}

factorial(5) // 120

上面代码是一个阶乘函数,计算n的阶乘,最多需要保存n个调用记录,复杂度 O(n) 。

如果改写成尾递归,只保留一个调用记录,复杂度 O(1) 。

function factorial(n, total) {
  if (n === 1) return total;
  return factorial(n - 1, n * total);
}

factorial(5, 1) // 120

还有一个比较著名的例子,就是计算 Fibonacci 数列,也能充分说明尾递归优化的重要性。

非尾递归的 Fibonacci 数列实现如下。

function Fibonacci (n) {
  if ( n <= 1 ) {return 1};

  return Fibonacci(n - 1) + Fibonacci(n - 2);
}

Fibonacci(10) // 89
Fibonacci(100) // 超时
Fibonacci(500) // 超时

由此可见,“尾调用优化”对递归操作意义重大,所以一些函数式编程语言将其写入了语言规格。ES6 亦是如此,第一次明确规定,所有 ECMAScript 的实现,都必须部署“尾调用优化”。这就是说,ES6 中只要使用尾递归,就不会发生栈溢出(或者层层递归造成的超时),相对节省内存。

递归函数的改写

尾递归的实现,往往需要改写递归函数,确保最后一步只调用自身。做到这一点的方法,就是把所有用到的内部变量改写成函数的参数。

这样做的缺点就是不太直观,第一眼很难看出来。

两个方法可以解决这个问题。方法一是在尾递归函数之外,再提供一个正常形式的函数。

函数式编程有一个概念,叫做柯里化(currying),意思是将多参数的函数转换成单参数的形式。这里也可以使用柯里化。

严格模式

ES6 的尾调用优化只在严格模式下开启,正常模式是无效的。

这是因为在正常模式下,函数内部有两个变量,可以跟踪函数的调用栈。

  • func.arguments:返回调用时函数的参数。
  • func.caller:返回调用当前函数的那个函数。

尾调用优化发生时,函数的调用栈会改写,因此上面两个变量就会失真。严格模式禁用这两个变量,所以尾调用模式仅在严格模式下生效。 

尾递归优化的实现

自己实现尾递归优化。它的原理非常简单。尾递归之所以需要优化,原因是调用栈太多,造成溢出,那么只要减少调用栈,就不会溢出。怎么做可以减少调用栈呢?就是采用“循环”换掉“递归”。

下面是一个正常的递归函数。

function sum(x, y) {
  if (y > 0) {
    return sum(x + 1, y - 1);
  } else {
    return x;
  }
}
sum(1, 100000)
// Uncaught RangeError: Maximum call stack size exceeded(…)

上面代码中,sum是一个递归函数,参数x是需要累加的值,参数y控制递归次数。一旦指定sum递归 100000 次,就会报错,提示超出调用栈的最大次数。

蹦床函数(trampoline)可以将递归执行转为循环执行。

function trampoline(f) {
  while (f && f instanceof Function) {
    f = f();
  }
  return f;
}

上面就是蹦床函数的一个实现,它接受一个函数f作为参数。只要f执行后返回一个函数,就继续执行。注意,这里是返回一个函数,然后执行该函数,而不是函数里面调用函数,这样就避免了递归执行,从而就消除了调用栈过大的问题。

然后,要做的就是将原来的递归函数,改写为每一步返回另一个函数。

function sum(x, y) {
  if (y > 0) {
    return sum.bind(null, x + 1, y - 1);
  } else {
    return x;
  }
}

上面代码中,sum函数的每次执行,都会返回自身的另一个版本。

现在,使用蹦床函数执行sum,就不会发生调用栈溢出。

trampoline(sum(1, 100000))
// 100001

蹦床函数并不是真正的尾递归优化,下面的实现才是。

function tco(f) {
  var value;
  var active = false;
  var accumulated = [];

  return function accumulator() {
    accumulated.push(arguments);
    if (!active) {
      active = true;
      while (accumulated.length) {
        value = f.apply(this, accumulated.shift());
      }
      active = false;
      return value;
    }
  };
}

var sum = tco(function(x, y) {
  if (y > 0) {
    return sum(x + 1, y - 1)
  }
  else {
    return x
  }
});

sum(1, 100000)
// 100001

上面代码中,tco函数是尾递归优化的实现,它的奥妙就在于状态变量active。默认情况下,这个变量是不激活的。一旦进入尾递归优化的过程,这个变量就激活了。然后,每一轮递归sum返回的都是undefined,所以就避免了递归执行;而accumulated数组存放每一轮sum执行的参数,总是有值的,这就保证了accumulator函数内部的while循环总是会执行。这样就很巧妙地将“递归”改成了“循环”,而后一轮的参数会取代前一轮的参数,保证了调用栈只有一层。

函数参数的尾逗号

ES2017 允许函数的最后一个参数有尾逗号(trailing comma)。

此前,函数定义和调用时,都不允许最后一个参数后面出现逗号。

如果像上面这样,将参数写成多行(即每个参数占据一行),以后修改代码的时候,想为函数clownsEverywhere添加第三个参数,或者调整参数的次序,就势必要在原来最后一个参数后面添加一个逗号。这对于版本管理系统来说,就会显示添加逗号的那一行也发生了变动。这看上去有点冗余,因此新的语法允许定义和调用时,尾部直接有一个逗号。

这样的规定也使得,函数参数与数组和对象的尾逗号规则,保持一致了。

Function.prototype.toString()

ES2019 对函数实例的toString()方法做出了修改。

toString()方法返回函数代码本身,以前会省略注释和空格。

原始代码包含注释,函数名和圆括号之间有空格,但是toString()方法都把它们省略了。

修改后的toString()方法,明确要求返回一模一样的原始代码。

catch 命令的参数省略

JavaScript 语言的try...catch结构,以前明确要求catch命令后面必须跟参数,接受try代码块抛出的错误对象。

} catch (err) { // 处理错误}     //catch命令后面带有参数err。

很多时候,catch代码块可能用不到这个参数。但是,为了保证语法正确,还是必须写。ES2019 做出了改变,允许catch语句省略参数。

} catch { // ...}

9 扩展运算符

扩展运算符(spread)是三个点(...)。它好比 rest 参数的逆运算,将一个数组转为用逗号分隔的参数序列。rest是将多余的参数放到一个数组里。

扩展运算符与正常的函数参数可以结合使用,非常灵活。

function f(v, w, x, y, z) { }
const args = [0, 1];
f(-1, ...args, 2, ...[3]);

扩展运算符后面还可以放置三元表达式  ...(x > 0 ? ['a'] : []),

如果扩展运算符后面是一个空数组,则不产生任何效果。

注意,只有函数调用时,扩展运算符才可以放在圆括号中,否则会报错。

替代函数的 apply 方法

由于扩展运算符可以展开数组,所以不再需要apply方法,将数组转为函数的参数了。

由于 JavaScript 不提供求数组最大元素的函数,所以只能套用Math.max函数,将数组转为一个参数序列,然后求最大值。有了扩展运算符以后,就可以直接用Math.max了。

ES5 写法中,push方法的参数不能是数组,所以只好通过apply方法变通使用push方法。有了扩展运算符,就可以直接将数组传入push方法。

扩展运算符的应用

(1)复制数组

数组是复合的数据类型,直接复制的话,只是复制了指向底层数据结构的指针,而不是克隆一个全新的数组。ES5 只能用变通方法来复制数组,a1会返回原数组的克隆,再修改a2就不会对a1产生影响。

const a2 = a1.concat();

扩展运算符提供了复制数组的简便写法。

const a2 = [...a1];

(2)合并数组

扩展运算符提供了数组合并的新写法。

arr1.concat(arr2, arr3);

不过,这两种方法都是浅拷贝,使用的时候需要注意。

const a1 = [{ foo: 1 }];
const a2 = [{ bar: 2 }];

const a3 = a1.concat(a2);
const a4 = [...a1, ...a2];

a3[0] === a1[0] // true
a4[0] === a1[0] // true  //上面代码中,a3和a4是用两种不同方法合并而成的新数组,但是它们的成员都是对原数组成员的引用,这就是浅拷贝。如果修改了原数组的成员,会同步反映到新数组。

(3)与解构赋值结合

扩展运算符可以与解构赋值结合起来,用于生成数组。

如果将扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报错。const [..start,end] = [1,2,3,4]  

 (4)字符串

扩展运算符还可以将字符串转为真正的数组,能够正确识别四个字节的 Unicode 字符。[...'hello']// [ "h", "e", "l", "l", "o" ]

凡是涉及到操作四个字节的 Unicode 字符的函数,都有问题。因此,最好都用扩展运算符改写。例如字符串的reverse操作。

(5)实现了 Iterator 接口的对象

任何定义了遍历器(Iterator)接口的对象,都可以用扩展运算符转为真正的数组。

querySelectorAll方法返回的是一个对象。它不是数组,而是一个类似数组的对象。这时,扩展运算符可以将其转为真正的数组,原因就在于这个对象实现了 Iterator 。

那些没有部署 Iterator 接口的类似数组的对象,扩展运算符就无法将其转为真正的数组。这时,可以改为使用Array.from方法将arrayLike转为真正的数组。

(6)Map 和 Set 结构,Generator 函数

扩展运算符内部调用的是数据结构的 Iterator 接口,因此只要具有 Iterator 接口的对象,都可以使用扩展运算符,比如 Map 结构。

10 对象的新增方法

Object.is(NaN, NaN) // true
Object.assign(target, source1, source2); 
//同名属性:后面的属性会覆盖前面的属性。
//只有一个参数会返回该参数,若该参数不是对象会被转为对象。由于undefined和null无法转成对象,所以如果它们作为参数,就会报错。但如果它们不在首位,会被跳过。
//字符串会以
(以字符数组的形式)合入目标对象,数值和布尔值都会被忽略。object.assign({},'abc') = {'0':'a','1':'b','2':'c'}
//Object.assign拷贝的属性是有限制的,只拷贝源对象的自身属性(不拷贝继承属性),也不拷贝不可枚举的属性(enumerable: false)。
//Object.assign方法实行的是浅拷贝,而不是深拷贝。也就是说,如果源对象某个属性的值是对象,那么目标对象拷贝得到的是这个对象的引用。这个对象的任何变化,都会反映到目标对象上面。
Object.defineProperty({}, key, value)

常见用途(1)为对象添加属性为对象添加方法为属性指定默认值

Object.getOwnPropertyDescriptors() 

 

Set

new Set()

过滤数组重复值、参数为字符串时将字符串拆为字符数组,认为NaN与自身相等,每个对象都不一样{} {}

Set实例属性:Set.prototype.constructor、Set.prototype.size

Set实例方法:add、delete、has、clear

Array.from将Set结构转为数组,可用于去除重复数组元素

Set 的遍历顺序就是插入顺序,调用时就能保证按照添加顺序调用

Set 有4个遍历方法:keys、values、entries、forEach

Set 结构没有键名,只有键值(或者说键名和键值是同一个值),所以keys方法和values方法的行为完全一致。 

Set 可用for...of遍历,可用forEach(函数,this)

扩展运算符和set结合可用于去除重复数组元素,如此转为数组后可使用map、filter方法

使用 Set 可以很容易地实现并集、交集和差集

在遍历操作中同步改变set结构:

set = new Set([...set].map(val => val * 2));
set = new Set(Array.from(set, val => val * 2));

WeakSet

成员只能是对象。

WeakSet 里面的引用,都不计入垃圾回收机制。因此,WeakSet 适合临时存放一组对象,以及存放跟对象绑定的信息。只要这些对象在外部消失,它在 WeakSet 里面的引用就会自动消失。

WeakSet 内部有多少个成员,取决于垃圾回收机制有没有运行,因此不可预测,所以不可遍历。

const arr = new WeakSet([3,4])  //报错:参数的成员不是对象,是数字。

方法:add、delete、has

Map

JavaScript 的对象(Object),本质上是键值对的集合(Hash 结构),但是传统上只能用字符串当作键。Map 数据结构类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。

方法:set、get、has、delete、size、clear(无返回值)

遍历方法:keys、values、entries、forEach

不仅仅是数组,任何具有 Iterator 接口、且每个成员都是一个双元素的数组的数据结构(详见《Iterator》一章)都可以当作Map构造函数的参数。这就是说,Set和Map都可以用来生成新的 Map。

如果对同一个键多次赋值,后面的值将覆盖前面的值。

如果读取一个未知的键,则返回undefined。

Map 的键实际上是跟内存地址绑定的,只要内存地址不一样,就视为两个键。这就解决了同名属性碰撞(***)的问题,我们扩展别人的库的时候,如果使用对象作为键名,就不用担心自己的属性与原作者的属性同名。

如果 Map 的键是一个简单类型的值(数字、字符串、布尔值),则只要两个值严格相等,Map 将其视为一个键,比如0和-0就是一个键,布尔值true和字符串true则是两个不同的键。另外,undefined和null也是两个不同的键。虽然NaN不严格相等于自身,但 Map 将其视为同一个键。

Map 的遍历顺序就是插入顺序。

Map 结构转为数组结构,比较快速的方法是使用扩展运算符(...)。

结合数组的map方法、filter方法,可以实现 Map 的遍历和过滤(Map 本身没有map和filter方法):将Map转为数组使用map、filter方法再传给新Map的参数

数组、对象、json转map

WeakMap

WeakMap与Map的区别有两点。

首先,WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键名。

其次,WeakMap的键名所指向的对象,不计入垃圾回收机制。

proxy

get方法用于拦截某个属性的读取操作,可以接受三个参数,依次为目标对象、属性名和 proxy 实例本身(严格地说,是操作行为所针对的对象

posted @ 2020-01-03 18:28  icctuan  阅读(397)  评论(0)    收藏  举报