码迷,mamicode.com
首页 > 其他好文 > 详细

ES6

时间:2020-01-03 19:50:26      阅读:1656      评论:0      收藏:0      [点我收藏+]

标签:忽略   imp   cto   回车符   参与   reflect   新版本   不容易   async   

 

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 ESMAScriptJavaScript的关系

前者是后者的规格,后者是前者的一种实现(另外的 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命令会发生“变量提升”现象,即变量可以在声明之前使用,值为undefinedlet所声明的变量一定要在声明后使用,否则报错。

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

如果一个变量根本没有被声明,使用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属性。undefinednull无法转为对象,所以对它们进行解构赋值,都会报错。

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

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 之间的小数,取整以后等于-0repeat视同为 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表示。

如果要将0b0o前缀的字符串数值转为十进制,要使用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()对于非数值一律返回falseNumber.isNaN()只有对于NaN才返回trueNaN一律返回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.20.3得到的结果是false

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

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

7.6 安全整数和 Number.isSafeInteger()

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

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

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

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

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

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

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

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

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

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

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

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

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,以下三个变量在箭头函数之中也是不存在的,指向外层函数的对应变量:argumentssupernew.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运行结束,将结果返回到AB的调用帧才会消失。如果函数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  //上面代码中,a3a4是用两种不同方法合并而成的新数组,但是它们的成员都是对原数组成员的引用,这就是浅拷贝。如果修改了原数组的成员,会同步反映到新数组。

(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); 
//同名属性:后面的属性会覆盖前面的属性。
//只有一个参数会返回该参数,若该参数不是对象会被转为对象。由于undefinednull无法转成对象,所以如果它们作为参数,就会报错。但如果它们不在首位,会被跳过。
//字符串会以
(以字符数组的形式)合入目标对象,数值和布尔值都会被忽略。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构造函数的参数。这就是说,SetMap都可以用来生成新的 Map。

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

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

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

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

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

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

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

数组、对象、json转map

WeakMap

WeakMapMap的区别有两点。

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

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

proxy

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

ES6

标签:忽略   imp   cto   回车符   参与   reflect   新版本   不容易   async   

原文地址:https://www.cnblogs.com/icctuan/p/12023722.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!