ES6 速通(上)

简介: ES6 速通(上)

1. 声明变量方法

var function let const import class

前面两个是es5的,后面是es6的,let const 的好处是块变量:只在声明所在的块级作用域内有效;

const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。

2. 取顶层对象

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

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

同一段代码为了能够在各种环境,都能取到顶层对象,现在一般是使用this变量,但是有局限性。

  • 全局环境中,this会返回顶层对象。但是,Node 模块和 ES6 模块中,this返回的是当前模块。
  • 函数里面的this,如果函数不是作为对象的方法运行,而是单纯作为函数运行,this会指向顶层对象。但是,严格模式下,这时this会返回undefined
  • 不管是严格模式,还是普通模式,new Function('return this')(),总是会返回全局对象。但是,如果浏览器用了 CSP(Content Security Policy,内容安全策略),那么evalnew Function这些方法都可能无法使用。

综上所述,很难找到一种方法,可以在所有情况下,都取到顶层对象。下面是两种勉强可以使用的方法。

  1. // 方法一
  2. (typeof window !== 'undefined'
  3. ? window
  4. : (typeof process === 'object' &&
`typeof require === 'function' &&`
`typeof global === 'object')`
`? global`
`: this);`
  1. // 方法二
  2. var getGlobal = function () {
  3. if (typeof self !== 'undefined') { return self; }
  4. if (typeof window !== 'undefined') { return window; }
  5. if (typeof global !== 'undefined') { return global; }
  6. throw new Error('unable to locate global object');
  7. };

es6采用的方式是globalThis,都可以从它拿到顶层对象

3. 变量解构

数组字符串解构按顺序

  1. let [a, b, c] = [1, 2, 3];
  2. const [a, b, c, d, e] = 'hello';

对象解构按对象名

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

数值和布尔值解构

  1. let {toString: s} = 123;
  2. s === Number.prototype.toString // true
  3. let {toString: s} = true;
  4. s === Boolean.prototype.toString // true
  5. let { prop: x } = undefined; // TypeError
  6. let { prop: y } = null; // TypeError

函数解构

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

结构默认值:

  1. var {x = 3} = {};
  2. x // 3
  3. var { message: msg = 'Something went wrong' } = {};
  4. msg // "Something went wrong"

这里要注意:

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

上面代码的写法会报错,因为 JavaScript 引擎会将{x}理解成一个代码块,从而发生语法错误。

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

不能使用圆括号

(1)变量声明语句

(2)函数参数

(3)赋值语句的模式

4. 字符串拓展

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

  1. $('#result').append(`
  2. There are ${basket.count} items
  3. in your basket, ${basket.onSale}
  4. are on sale!
  5. `);

模板字符串前后的空格和换行可以使用trim()来处理

` this is a template string `.trim()

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

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

5. 字符串新增方法

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

  1. String.fromCodePoint(0x20BB7)
  2. // "𠮷"
  3. String.fromCodePoint(0x78, 0x1f680, 0x79) === 'x\uD83D\uDE80y'
  4. // true

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

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

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

  1. '\u01D1'==='\u004F\u030C' //false
  2. '\u01D1'.length // 1
  3. '\u004F\u030C'.length // 2
  4. '\u01D1'.normalize() === '\u004F\u030C'.normalize()
  5. // true

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

  • includes():返回布尔值,表示是否找到了参数字符串。
  • startsWith():返回布尔值,表示参数字符串是否在原字符串的头部。
  • endsWith():返回布尔值,表示参数字符串是否在原字符串的尾部。
  1. let s = 'Hello world!';
  2. s.startsWith('world', 6) // true
  3. s.endsWith('Hello', 5) // true
  4. s.includes('Hello', 6) // false

repeat方法返回一个新字符串,表示将原字符串重复n次。小数会向下取整,负数或者Infinity会报错

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

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

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

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

  1. const s = ' abc ';
  2. s.trim() // "abc"
  3. s.trimStart() // "abc "
  4. s.trimEnd() // " abc"

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

6. 正则的拓展

感觉和现在的python差不多,这里要注意是exec进行调用

  1. const RE_OPT_A = /^(?a+)?$/;
  2. const matchObj = RE_OPT_A.exec('');
  3. matchObj.groups.as // undefined
  4. 'as' in matchObj.groups // true

字符串对象共有 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]

7. 数值的拓展

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

ES6 将全局方法parseInt()parseFloat(),移植到Number对象上面,行为完全保持不变。其功能是解析string转化为int或者float

  1. Number.parseInt('12.34') // 12
  2. Number.parseFloat('123.45#') // 123.45

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

  1. Number.isInteger(5E-324) // false
  2. Number.isInteger(5E-325) // true
    如果一个数值的绝对值小于Number.MIN_VALUE(5E-324)会被自动转为 0,上面代码中,5E-325由于值太小,会被自动转为0,因此返回true

ES6 在Number对象上面,新增一个极小的常量Number.EPSILON。根据规格,它表示 1 与大于 1 的最小浮点数之间的差。Number.EPSILON实际上是 JavaScript 能够表示的最小精度。误差如果小于这个值,就可以认为已经没有意义了,即不存在误差了。

  1. 0.1 + 0.2 === 0.3 // false

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

JavaScript 所有数字都保存成 64 位浮点数,这给数值的表示带来了两大限制。一是数值的精度只能到 53 个二进制位(相当于 16 个十进制位),大于这个范围的整数,JavaScript 是无法精确表示的,这使得 JavaScript 不适合进行科学和金融方面的精确计算。二是大于或等于2的1024次方的数值,JavaScript 无法表示,会返回Infinity

  1. // 超过 53 个二进制位的数值,无法保持精度
  2. Math.pow(2, 53) === Math.pow(2, 53) + 1 // true
  3. // 超过 2 的 1024 次方的数值,无法表示
  4. Math.pow(2, 1024) // Infinity

8. 函数的拓展

如果有默认值的参数都不是尾参数。这时,无法只省略该参数,而不省略它后面的参数,除非显式输入undefined。ES2017 允许函数的最后一个参数有尾逗号(trailing comma)。

  1. // 例一
  2. function f(x = 1, y) {
  3. return [x, y];
  4. }
  5. f() // [1, undefined]
  6. f(2) // [2, undefined]
  7. f(, 1) // 报错
  8. f(undefined, 1) // [1, 1]
  9. // 例二
  10. function f(x, y = 5, z) {
  11. return [x, y, z];
  12. }
  13. f() // [undefined, 5, undefined]
  14. f(1) // [1, 5, undefined]
  15. f(1, ,2) // 报错
  16. f(1, undefined, 2) // [1, 5, 2]

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

  1. (function (a) {}).length // 1
  2. (function (a = 5) {}).length // 0
  3. (function (a, b, c = 5) {}).length // 2

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

  1. (function (a = 0, b, c) {}).length // 0
  2. (function (a, b = 1, c) {}).length // 1

ES6 引入 rest 参数(形式为...变量名),用于获取函数的多余参数,这样就不需要使用arguments对象了。rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中。下面是一个 rest 参数代替arguments变量的例子。

  1. // arguments变量的写法
  2. function sortNumbers() {
  3. return Array.prototype.slice.call(arguments).sort();
  4. }
  5. // rest参数的写法
  6. const sortNumbers = (...numbers) => numbers.sort();

arguments对象不是数组,而是一个类似数组的对象。所以为了使用数组的方法,必须使用Array.prototype.slice.call先将其转为数组。rest 参数就不存在这个问题,它就是一个真正的数组,数组特有的方法都可以使用。下面是一个利用 rest 参数改写数组push方法的例子。

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

  1. function push(array, ...items) {
  2. items.forEach(function(item) {
  3. array.push(item);
  4. console.log(item);
  5. });
  6. }
  7. var a = [];
  8. push(a, 1, 2, 3)

ES2016 做了一点修改,规定只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式,否则会报错。这样规定的原因是,函数内部的严格模式,同时适用于函数体和函数参数。但是,函数执行的时候,先执行函数参数,然后再执行函数体。这样就有一个不合理的地方,只有从函数体之中,才能知道参数是否应该以严格模式执行,但是参数却应该先于函数体执行。

  1. function doSomething(a, b) {
  2. 'use strict';
  3. // code
  4. }

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

  1. const bar = function baz() {};
  2. bar.name // "baz"

箭头函数

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

  1. var f = v => v;
  2. // 等同于
  3. var f = function (v) {
  4. return v;
  5. };

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

  1. var f = () => 5;
  2. // 等同于
  3. var f = function () { return 5 };
  4. var sum = (num1, num2) => num1 + num2;
  5. // 等同于
  6. var sum = function(num1, num2) {
  7. return num1 + num2;
  8. };

ES6 的尾调用优化只在严格模式下开启,正常模式是无效的。递归本质上是一种循环操作。纯粹的函数式编程语言没有循环操作命令,所有的循环都用递归实现,这就是为什么尾递归对这些语言极其重要。对于其他支持“尾调用优化”的语言(比如 Lua,ES6),只需要知道循环可以用递归代替,而一旦使用递归,就最好使用尾递归。

9. 数组的拓展

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

  1. console.log(...[1, 2, 3])
  2. // 1 2 3
  3. console.log(1, ...[2, 3, 4], 5)
  4. // 1 2 3 4 5

复制数组

  1. const a1 = [1, 2];
  2. // 写法一
  3. const a2 = [...a1];
  4. // 写法二
  5. const [...a2] = a1;

合并数组

  1. const arr1 = ['a', 'b'];
  2. const arr2 = ['c'];
  3. const arr3 = ['d', 'e'];
  4. // ES5 的合并数组
  5. arr1.concat(arr2, arr3);
  6. // [ 'a', 'b', 'c', 'd', 'e' ]
  7. // ES6 的合并数组
  8. [...arr1, ...arr2, ...arr3]
  9. // [ 'a', 'b', 'c', 'd', 'e' ]

两种转化数组的方式,array.from() array.of()

Array.from方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括 ES6 新增的数据结构 Set 和 Map)。

Array.of方法用于将一组值,转换为数组。

数组实例的find方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为true的成员,然后返回该成员。如果没有符合条件的成员,则返回undefined

  1. [1, 4, -5, 10].find((n) => n < 0)
  2. // -5

上面代码找出数组中第一个小于 0 的成员。

  1. [1, 5, 10, 15].find(function(value, index, arr) {
  2. return value > 9;
  3. }) // 10

数组实例的findIndex方法的用法与find方法非常类似,返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1

  1. [1, 5, 10, 15].findIndex(function(value, index, arr) {
  2. return value > 9;
  3. }) // 2

fill方法使用给定值,填充一个数组。

  1. ['a', 'b', 'c'].fill(7)
  2. // [7, 7, 7]
  3. new Array(3).fill(7)
  4. // [7, 7, 7]

数组也是键值对组成的['a', 'b'] 相当于 {0: 'a', 1: 'b'},其键就是序号,其值是本身;

entries()keys()values()——用于遍历数组。它们都返回一个遍历器对象(详见《Iterator》一章),可以用for...of循环进行遍历,唯一的区别是keys()是对键名的遍历、values()是对键值的遍历,entries()是对键值对的遍历。

数组的成员有时还是数组,Array.prototype.flat()用于将嵌套的数组“拉平”,变成一维的数组。该方法返回一个新数组,对原数据没有影响。

  1. [1, 2, [3, [4, 5]]].flat()
  2. // [1, 2, 3, [4, 5]]
  3. [1, 2, [3, [4, 5]]].flat(2)
  4. // [1, 2, 3, 4, 5]

flatMap()方法对原数组的每个成员执行一个函数(相当于执行Array.prototype.map()),然后对返回值组成的数组执行flat()方法。该方法返回一个新数组,不改变原数组。

  1. // 相当于 [[2, 4], [3, 6], [4, 8]].flat()
  2. [2, 3, 4].flatMap((x) => [x, x * 2])
  3. // [2, 4, 3, 6, 4, 8]

10. 对象的扩展

变量foo直接写在大括号里面。这时,属性名就是变量名, 属性值就是变量值。下面是另一个例子。

  1. let birth = '2000/01/01';
  2. const Person = {
  3. name: '张三',
  4. //等同于birth: birth
  5. birth,
  6. // 等同于hello: function ()...
  7. hello() { console.log('我的名字是', this.name); }
  8. };

ES6 允许字面量定义对象时,用方法二(表达式)作为对象的属性名,即把表达式放在方括号内。

  1. let lastWord = 'last word';
  2. const a = {
  3. 'first word': 'hello',
  4. [lastWord]: 'world'
  5. };
  6. a['first word'] // "hello"
  7. a[lastWord] // "world"
  8. a['last word'] // "world"

注意,属性名表达式如果是一个对象{},默认情况下会自动将对象转为字符串[object Object],这一点要特别小心。

我们知道,this关键字总是指向函数所在的当前对象,ES6 又新增了另一个类似的关键字super,指向当前对象的原型对象。

三种super的用法都会报错,因为对于 JavaScript 引擎来说,这里的super都没有用在对象的方法之中。第一种写法是super用在属性里面,第二种和第三种写法是super用在一个函数里面,然后赋值给foo属性。目前,只有对象方法的简写法可以让 JavaScript 引擎确认,定义的是对象的方法。

  1. // 报错
  2. const obj = {
  3. foo: super.foo
  4. }
  5. // 报错
  6. const obj = {
  7. foo: () => super.foo
  8. }
  9. // 报错
  10. const obj = {
  11. foo: function () {
  12. return super.foo
  13. }
  14. }

对象的拓展运算符

  1. let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
  2. let { x, ...y, ...z } = someObject; // 句法错误
  3. let { ...x, y, z } = someObject; // 句法错误
  4. let { x, ...y, ...z } = someObject; // 句法错误

注意,解构赋值的拷贝是浅拷贝,即如果一个键的值是复合类型的值(数组、对象、函数)、那么解构赋值拷贝的是这个值的引用,而不是这个值的副本。

  1. let obj = { a: { b: 1 } };
  2. let { ...x } = obj;
  3. obj.a.b = 2;
  4. x.a.b // 2

ES2020 引入了“链判断运算符”(optional chaining operator)?.

  1. const firstName = (message
  2. && message.body
  3. && message.body.user
  4. && message.body.user.firstName) || 'default';
  5. const firstName = message?.body?.user?.firstName || 'default';
  6. const fooValue = myForm.querySelector('input[name=foo]')?.value

相等运算符(==)和严格相等运算符(===)ES6 提出“Same-value equality”(同值相等)算法,用来解决这个问题。Object.is就是部署这个算法的新方法。它用来比较两个值是否严格相等,与严格比较运算符(===)的行为基本一致。

  1. Object.is('foo', 'foo')
  2. // true
  3. Object.is({}, {})
  4. // false
  5. +0 === -0 //true
  6. NaN === NaN // false
  7. Object.is(+0, -0) // false
  8. Object.is(NaN, NaN) // true

Object.assign方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。

Object.assign方法实行的是浅拷贝,而不是深拷贝。也就是说,如果源对象某个属性的值是对象,那么目标对象拷贝得到的是这个对象的引用。

  1. const obj1 = {a: {b: 1}};
  2. const obj2 = Object.assign({}, obj1);
  3. obj1.a.b = 2;
  4. obj2.a.b // 2

对于这种嵌套的对象,一旦遇到同名属性,Object.assign的处理方法是替换,而不是添加。

  1. const target = { a: { b: 'c', d: 'e' } }
  2. const source = { a: { b: 'hello' } }
  3. Object.assign(target, source)
  4. // { a: { b: 'hello' } }

Object.assign可以用来处理数组,但是会把数组视为对象。

Object.assign只能进行值的复制,如果要复制的值是一个取值函数,那么将求值后再复制。

  1. const source = {
  2. get foo() { return 1 }
  3. };
  4. const target = {};
  5. Object.assign(target, source)
  6. // { foo: 1 }

ES2017 引入了Object.getOwnPropertyDescriptors()方法,返回指定对象所有自身属性(非继承属性)的描述对象。

  1. const obj = {
  2. foo: 123,
  3. get bar() { return 'abc' }
  4. };
  5. Object.getOwnPropertyDescriptors(obj)
  6. // { foo:
  7. // { value: 123,
  8. // writable: true,
  9. // enumerable: true,
  10. // configurable: true },
  11. // bar:
  12. // { get: [Function: get bar],
  13. // set: undefined,
  14. // enumerable: true,
  15. // configurable: true } }

__proto__属性(前后各两个下划线),用来读取或设置当前对象的原型对象(prototype)。目前,所有浏览器(包括 IE11)都部署了这个属性。但是不推荐使用,实现上,__proto__调用的是Object.prototype.__proto__,具体实现如下。

Object.setPrototypeOf方法的作用与__proto__相同,用来设置一个对象的原型对象(prototype),返回参数对象本身。它是 ES6 正式推荐的设置原型对象的方法。

Object.getPrototypeOf()该方法与Object.setPrototypeOf方法配套,用于读取一个对象的原型对象。

Object.keys(),Object.values(),Object.entries() 作为遍历一个对象的补充手段,供for...of循环使用。

Object.fromEntries()方法是Object.entries()的逆操作,用于将一个键值对数组转为对象。

12. Symbol

ES6 引入了一种新的原始数据类型Symbol,表示独一无二的值。它是 JavaScript 语言的第七种数据类型,前六种是:undefinednull、布尔值(Boolean)、字符串(String)、数值(Number)、对象(Object)。

Symbol 作为属性名,遍历对象的时候,该属性不会出现在for...infor...of循环中,也不会被Object.keys()Object.getOwnPropertyNames()JSON.stringify()返回。

但是,它也不是私有属性,有一个Object.getOwnPropertySymbols()方法,可以获取指定对象的所有 Symbol 属性名。该方法返回一个数组,成员是当前对象的所有用作属性名的 Symbol 值。

  1. const obj = {};
  2. const foo = Symbol('foo');
  3. obj[foo] = 'bar';
  4. for (let i in obj) {
  5. console.log(i); // 无输出
  6. }
  7. Object.getOwnPropertyNames(obj) // []
  8. Object.getOwnPropertySymbols(obj) // [Symbol(foo)]

13. Set 和 Map 数据结构

Set 结构的实例有以下属性。

  • Set.prototype.constructor:构造函数,默认就是Set函数。
  • Set.prototype.size:返回Set实例的成员总数。

Set 实例的方法分为两大类:操作方法(用于操作数据)和遍历方法(用于遍历成员)。下面先介绍四个操作方法。

  • Set.prototype.add(value):添加某个值,返回 Set 结构本身。
  • Set.prototype.delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
  • Set.prototype.has(value):返回一个布尔值,表示该值是否为Set的成员。
  • Set.prototype.clear():清除所有成员,没有返回值。

keys方法、values方法、entries方法返回的都是遍历器对象(详见《Iterator 对象》一章)。由于 Set 结构没有键名,只有键值(或者说键名和键值是同一个值),所以keys方法和values方法的行为完全一致。

WeakSet 结构与 Set 类似,也是不重复的值的集合。但是,它与 Set 有两个区别。首先,WeakSet 的成员只能是对象,而不能是其他类型的值。其次,WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet 之中。

Map是JavaScript 的对象(Object),本质上是键值对的集合(Hash 结构),但是传统上只能用字符串当作键。这给它的使用带来了很大的限制。

Map 结构的实例有以下属性和操作方法。

size属性返回 Map 结构的成员总数。

set方法设置键名key对应的键值为value,然后返回整个 Map 结构。如果key已经有值,则键值会被更新,否则就新生成该键。

set方法返回的是当前的Map对象,因此可以采用链式写法。

get方法读取key对应的键值,如果找不到key,返回undefined

has方法返回一个布尔值,表示某个键是否在当前 Map 对象之中。

delete方法删除某个键,返回true。如果删除失败,返回false

clear方法清除所有成员,没有返回值。

  • Map.prototype.keys():返回键名的遍历器。
  • Map.prototype.values():返回键值的遍历器。
  • Map.prototype.entries():返回所有成员的遍历器。
  • Map.prototype.forEach():遍历 Map 的所有成员。

(1)Map 转为数组

前面已经提过,Map 转为数组最方便的方法,就是使用扩展运算符(...)。

  1. const myMap = new Map()
  2. .set(true, 7)
  3. .set({foo: 3}, ['abc']);
  4. [...myMap]
  5. // [ [ true, 7 ], [ { foo: 3 }, [ 'abc' ] ] ]

(2)数组 转为 Map

将数组传入 Map 构造函数,就可以转为 Map。

  1. new Map([
  2. [true, 7],
  3. [{foo: 3}, ['abc']]
  4. ])
  5. // Map {
  6. // true => 7,
  7. // Object {foo: 3} => ['abc']
  8. // }

(3)Map 转为对象

如果所有 Map 的键都是字符串,它可以无损地转为对象。

  1. function strMapToObj(strMap) {
  2. let obj = Object.create(null);
  3. for (let [k,v] of strMap) {
`obj[k] = v;`
  1. }
  2. return obj;
  3. }
  4. const myMap = new Map()
  5. .set('yes', true)
  6. .set('no', false);
  7. strMapToObj(myMap)
  8. // { yes: true, no: false }

如果有非字符串的键名,那么这个键名会被转成字符串,再作为对象的键名。

(4)对象转为 Map

对象转为 Map 可以通过Object.entries()

  1. let obj = {"a":1, "b":2};
  2. let map = new Map(Object.entries(obj));

此外,也可以自己实现一个转换函数。

  1. function objToStrMap(obj) {
  2. let strMap = new Map();
  3. for (let k of Object.keys(obj)) {
`strMap.set(k, obj[k]);`
  1. }
  2. return strMap;
  3. }
  4. objToStrMap({yes: true, no: false})
  5. // Map {"yes" => true, "no" => false}

(5)Map 转为 JSON

Map 转为 JSON 要区分两种情况。一种情况是,Map 的键名都是字符串,这时可以选择转为对象 JSON。

  1. function strMapToJson(strMap) {
  2. return JSON.stringify(strMapToObj(strMap));
  3. }
  4. let myMap = new Map().set('yes', true).set('no', false);
  5. strMapToJson(myMap)
  6. // '{"yes":true,"no":false}'

另一种情况是,Map 的键名有非字符串,这时可以选择转为数组 JSON。

  1. function mapToArrayJson(map) {
  2. return JSON.stringify([...map]);
  3. }
  4. let myMap = new Map().set(true, 7).set({foo: 3}, ['abc']);
  5. mapToArrayJson(myMap)
  6. // '[[true,7],[{"foo":3},["abc"]]]'

(6)JSON 转为 Map

JSON 转为 Map,正常情况下,所有键名都是字符串。

  1. function jsonToStrMap(jsonStr) {
  2. return objToStrMap(JSON.parse(jsonStr));
  3. }
  4. jsonToStrMap('{"yes": true, "no": false}')
  5. // Map {'yes' => true, 'no' => false}

但是,有一种特殊情况,整个 JSON 就是一个数组,且每个数组成员本身,又是一个有两个成员的数组。这时,它可以一一对应地转为 Map。这往往是 Map 转为数组 JSON 的逆操作。

  1. function jsonToMap(jsonStr) {
  2. return new Map(JSON.parse(jsonStr));
  3. }
  4. jsonToMap('[[true,7],[{"foo":3},["abc"]]]')
  5. // Map {true => 7, Object {foo: 3} => ['abc']}

WeakMap结构与Map结构类似,也是用于生成键值对的集合。WeakMapMap的区别有两点。首先,WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键名。其次,WeakMap的键名所指向的对象,不计入垃圾回收机制。

和weakset一样

14. Proxy

Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。

下面是 Proxy 支持的拦截操作一览,一共 13 种。

  • get(target, propKey, receiver):拦截对象属性的读取,比如proxy.fooproxy['foo']
  • set(target, propKey, value, receiver):拦截对象属性的设置,比如proxy.foo = vproxy['foo'] = v,返回一个布尔值。
  • has(target, propKey):拦截propKey in proxy的操作,返回一个布尔值。
  • deleteProperty(target, propKey):拦截delete proxy[propKey]的操作,返回一个布尔值。
  • ownKeys(target):拦截Object.getOwnPropertyNames(proxy)Object.getOwnPropertySymbols(proxy)Object.keys(proxy)for...in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。
  • getOwnPropertyDescriptor(target, propKey):拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。
  • defineProperty(target, propKey, propDesc):拦截Object.defineProperty(proxy, propKey, propDesc)Object.defineProperties(proxy, propDescs),返回一个布尔值。
  • preventExtensions(target):拦截Object.preventExtensions(proxy),返回一个布尔值。
  • getPrototypeOf(target):拦截Object.getPrototypeOf(proxy),返回一个对象。
  • isExtensible(target):拦截Object.isExtensible(proxy),返回一个布尔值。
  • setPrototypeOf(target, proto):拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。
  • apply(target, object, args):拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)proxy.call(object, ...args)proxy.apply(...)
  • construct(target, args):拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(...args)
  1. var person = {
  2. name: "张三"
  3. };
  4. var proxy = new Proxy(person, {
  5. get: function(target, propKey) {
  6. if (propKey in target) {
  7. return target[propKey];
  8. } else {
  9. throw new ReferenceError("Prop name \"" + propKey + "\" does not exist.");
  10. }
  11. }
  12. });
  13. proxy.name // "张三"
  14. proxy.age // 抛出一个错误

Proxy.revocable()方法返回一个可取消的 Proxy 实例。

Proxy.revocable()方法返回一个对象,该对象的proxy属性是Proxy实例,revoke属性是一个函数,可以取消Proxy实例。上面代码中,当执行revoke函数之后,再访问Proxy实例,就会抛出一个错误。

Proxy.revocable()的一个使用场景是,目标对象不允许直接访问,必须通过代理访问,一旦访问结束,就收回代理权,不允许再次访问。

15. Reflect

Reflect对象与Proxy对象一样,也是 ES6 为了操作对象而提供的新 API。Reflect对象的设计目的有这样几个。

(1) 将Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上。现阶段,某些方法同时在ObjectReflect对象上部署,未来的新方法将只部署在Reflect对象上。也就是说,从Reflect对象上可以拿到语言内部的方法。

(2) 修改某些Object方法的返回结果,让其变得更合理。比如,Object.defineProperty(obj, name, desc)在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj, name, desc)则会返回false

(3) 让Object操作都变成函数行为。某些Object操作是命令式,比如name in objdelete obj[name],而Reflect.has(obj, name)Reflect.deleteProperty(obj, name)让它们变成了函数行为。

(4)Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。这就让Proxy对象可以方便地调用对应的Reflect方法,完成默认行为,作为修改行为的基础。也就是说,不管Proxy怎么修改默认行为,你总可以在Reflect上获取默认行为。

Reflect对象一共有 13 个静态方法。

  • Reflect.apply(target, thisArg, args)
  • Reflect.construct(target, args)
  • Reflect.get(target, name, receiver)
  • Reflect.set(target, name, value, receiver)
  • Reflect.defineProperty(target, name, desc)
  • Reflect.deleteProperty(target, name)
  • Reflect.has(target, name)
  • Reflect.ownKeys(target)
  • Reflect.isExtensible(target)
  • Reflect.preventExtensions(target)
  • Reflect.getOwnPropertyDescriptor(target, name)
  • Reflect.getPrototypeOf(target)
  • Reflect.setPrototypeOf(target, prototype)

上面这些方法的作用,大部分与Object对象的同名方法的作用都是相同的,而且它与Proxy对象的方法是一一对应的。下面是对它们的解释。

16. Promise

Promise对象有以下两个特点。

(1)对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。

(2)一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。如果改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。

我们可以将图片的加载写成一个Promise,一旦加载完成,Promise的状态就发生变化。

  1. const preloadImage = function (path) {
  2. return new Promise(function (resolve, reject) {
  3. const image = new Image();
  4. image.onload = resolve;
  5. image.onerror = reject;
  6. image.src = path;
  7. });
  8. };

同时要注意:

  1. setTimeout(function () {
  2. console.log('three');
  3. }, 0);
  4. Promise.resolve().then(function () {
  5. console.log('two');
  6. });
  7. console.log('one');
  8. // one
  9. // two
  10. // three

上面代码中,setTimeout(fn, 0)在下一轮“事件循环”开始时执行,Promise.resolve()在本轮“事件循环”结束时执行,console.log('one')则是立即执行,因此最先输出。

实际开发中,经常遇到一种情况:不知道或者不想区分,函数f是同步函数还是异步操作,但是想用 Promise 来处理它。因为这样就可以不管f是否包含异步操作,都用then方法指定下一步流程,用catch方法处理f抛出的错误。一般就会采用下面的写法。

由于Promise.try为所有操作提供了统一的处理机制,所以如果想用then方法管理流程,最好都用Promise.try包装一下,其中一点就是可以更好地管理异常。

  1. Promise.try(() => database.users.get({id: userId}))
  2. .then(...)
  3. .catch(...)

基本操作

ES6 规定,Promise对象是一个构造函数,用来生成Promise实例。

下面代码创造了一个Promise实例。

  1. const promise = new Promise(function(resolve, reject) {
  2. // ... some code
  3. if (/* 异步操作成功 */){
`resolve(value);`
  1. } else {
`reject(error);`
  1. }
  2. });

Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolvereject。它们是两个函数,由 JavaScript 引擎提供,不用自己部署。

resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;reject函数的作用是,将Promise对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。

Promise实例生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数。

  1. promise.then(function(value) {
  2. // success
  3. }, function(error) {
  4. // failure
  5. });

then方法可以接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为resolved时调用,第二个回调函数是Promise对象的状态变为rejected时调用。其中,第二个函数是可选的,不一定要提供。这两个函数都接受Promise对象传出的值作为参数。

  1. getJSON("/post/1.json").then(
  2. post => getJSON(post.commentURL)
  3. ).then(
  4. comments => console.log("resolved: ", comments),
  5. err => console.log("rejected: ", err)
  6. );

Promise.prototype.catch()方法是.then(null, rejection).then(undefined, rejection)的别名,用于指定发生错误时的回调函数。

  1. getJSON('/posts.json').then(function(posts) {
  2. // ...
  3. }).catch(function(error) {
  4. // 处理 getJSON 和 前一个回调函数运行时发生的错误
  5. console.log('发生错误!', error);
  6. });

上面代码中,getJSON()方法返回一个 Promise 对象,如果该对象状态变为resolved,则会调用then()方法指定的回调函数;如果异步操作抛出错误,状态就会变为rejected,就会调用catch()方法指定的回调函数,处理这个错误。另外,then()方法指定的回调函数,如果运行中抛出错误,也会被catch()方法捕获。

  1. p.then((val) => console.log('fulfilled:', val))
  2. .catch((err) => console.log('rejected', err));
  3. // 等同于
  4. p.then((val) => console.log('fulfilled:', val))
  5. .then(null, (err) => console.log("rejected:", err));

finally()方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。

  1. promise
  2. .then(result => {···})
  3. .catch(error => {···})
  4. .finally(() => {···});

上面代码中,不管promise最后的状态,在执行完thencatch指定的回调函数以后,都会执行finally方法指定的回调函数。

下面是一个例子,服务器使用 Promise 处理请求,然后使用finally方法关掉服务器。

  1. server.listen(port)
  2. .then(function () {
`// ...`
  1. })
  2. .finally(server.stop);

finally方法的回调函数不接受任何参数,这意味着没有办法知道,前面的 Promise 状态到底是fulfilled还是rejected。这表明,finally方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果。

Promise.all()方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。

  1. const p = Promise.all([p1, p2, p3]);

上面代码中,Promise.all()方法接受一个数组作为参数,p1p2p3都是 Promise 实例,如果不是,就会先调用下面讲到的Promise.resolve方法,将参数转为 Promise 实例,再进一步处理。另外,Promise.all()方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例。

p的状态由p1p2p3决定,分成两种情况。

(1)只有p1p2p3的状态都变成fulfilledp的状态才会变成fulfilled,此时p1p2p3的返回值组成一个数组,传递给p的回调函数。

(2)只要p1p2p3之中有一个被rejectedp的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。

Promise.race()方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。

复制代码

  1. const p = Promise.race([p1, p2, p3]);

上面代码中,只要p1p2p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。

Promise.race()方法的参数与Promise.all()方法一样,如果不是 Promise 实例,就会先调用下面讲到的Promise.resolve()方法,将参数转为 Promise 实例,再进一步处理。

下面是一个例子,如果指定时间内没有获得结果,就将 Promise 的状态变为reject,否则变为resolve

  1. const p = Promise.race([
  2. fetch('/resource-that-may-take-a-while'),
  3. new Promise(function (resolve, reject) {
`setTimeout(() => reject(new Error('request timeout')), 5000)`
  1. })
  2. ]);
  3. p
  4. .then(console.log)
  5. .catch(console.error);

上面代码中,如果 5 秒之内fetch方法无法返回结果,变量p的状态就会变为rejected,从而触发catch方法指定的回调函数。

Promise.allSettled()

Promise.allSettled()方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例。只有等到所有这些参数实例都返回结果,不管是fulfilled还是rejected,包装实例才会结束。该方法由 ES2020 引入。

  1. const promises = [
  2. fetch('/api-1'),
  3. fetch('/api-2'),
  4. fetch('/api-3'),
  5. ];
  6. await Promise.allSettled(promises);
  7. removeLoadingIndicator();

上面代码对服务器发出三个请求,等到三个请求都结束,不管请求成功还是失败,加载的滚动图标就会消失。

该方法返回的新的 Promise 实例,一旦结束,状态总是fulfilled,不会变成rejected。状态变成fulfilled后,Promise 的监听函数接收到的参数是一个数组,每个成员对应一个传入Promise.allSettled()的 Promise 实例。

  1. const resolved = Promise.resolve(42);
  2. const rejected = Promise.reject(-1);
  3. const allSettledPromise = Promise.allSettled([resolved, rejected]);
  4. allSettledPromise.then(function (results) {
  5. console.log(results);
  6. });
  7. // [
  8. // { status: 'fulfilled', value: 42 },
  9. // { status: 'rejected', reason: -1 }
  10. // ]

上面代码中,Promise.allSettled()的返回值allSettledPromise,状态只可能变成fulfilled。它的监听函数接收到的参数是数组results。该数组的每个成员都是一个对象,对应传入Promise.allSettled()的两个 Promise 实例。每个对象都有status属性,该属性的值只可能是字符串fulfilled或字符串rejectedfulfilled时,对象有value属性,rejected时有reason属性,对应两种状态的返回值。

下面是返回值用法的例子。

  1. const promises = [ fetch('index.html'), fetch('https://does-not-exist/') ];
  2. const results = await Promise.allSettled(promises);
  3. // 过滤出成功的请求
  4. const successfulPromises = results.filter(p => p.status === 'fulfilled');
  5. // 过滤出失败的请求,并输出原因
  6. const errors = results
  7. .filter(p => p.status === 'rejected')
  8. .map(p => p.reason);

有时候,我们不关心异步操作的结果,只关心这些操作有没有结束。这时,Promise.allSettled()方法就很有用。如果没有这个方法,想要确保所有操作都结束,就很麻烦。Promise.all()方法无法做到这一点。

  1. const urls = [ /* ... */ ];
  2. const requests = urls.map(x => fetch(x));
  3. try {
  4. await Promise.all(requests);
  5. console.log('所有请求都成功。');
  6. } catch {
  7. console.log('至少一个请求失败,其他请求可能还没结束。');
  8. }

上面代码中,Promise.all()无法确定所有请求都结束。想要达到这个目的,写起来很麻烦,有了Promise.allSettled(),这就很容易了。

Promise.any()方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例。只要参数实例有一个变成fulfilled状态,包装实例就会变成fulfilled状态;如果所有参数实例都变成rejected状态,包装实例就会变成rejected状态。该方法目前是一个第三阶段的提案

Promise.any()Promise.race()方法很像,只有一点不同,就是不会因为某个 Promise 变成rejected状态而结束。

  1. var resolved = Promise.resolve(42);
  2. var rejected = Promise.reject(-1);
  3. var alsoRejected = Promise.reject(Infinity);
  4. Promise.any([resolved, rejected, alsoRejected]).then(function (result) {
  5. console.log(result); // 42
  6. });
  7. Promise.any([rejected, alsoRejected]).catch(function (results) {
  8. console.log(results); // [-1, Infinity]
  9. });

ES6 速通(中)https://developer.aliyun.com/article/1504139?spm=a2c6h.13148508.setting.19.36834f0eMJOehx

目录
相关文章
|
6月前
|
编解码 JavaScript 前端开发
ES6 速通(下)
ES6 速通(下)
52 1
|
6月前
|
JSON 前端开发 JavaScript
ES6 速通(中)
ES6 速通(中)
35 1
|
6月前
|
前端开发 JavaScript
CommonJS 和 ES6 Module:一场模块规范的对决(下)
CommonJS 和 ES6 Module:一场模块规范的对决(下)
CommonJS 和 ES6 Module:一场模块规范的对决(下)
|
6月前
|
JavaScript 前端开发 开发者
CommonJS 和 ES6 Module:一场模块规范的对决(上)
CommonJS 和 ES6 Module:一场模块规范的对决(上)
|
前端开发 JavaScript Java
【编程指南】ES2016到ES2023新特性解析一网打尽(二)
【编程指南】ES2016到ES2023新特性解析一网打尽(二)
147 0
|
JSON JavaScript 前端开发
【编程指南】ES2016到ES2023新特性解析一网打尽(一)
【编程指南】ES2016到ES2023新特性解析一网打尽(一)
98 0
|
缓存 JavaScript 算法
每天3分钟,重学ES6-ES12(十八) CJS
每天3分钟,重学ES6-ES12(十八) CJS
88 0
|
前端开发 JavaScript
每天3分钟,重学ES6-ES12系列文章汇总
每天3分钟,重学ES6-ES12系列文章汇总
65 0
|
Web App开发 自然语言处理 JavaScript
ES6之路之模块详解
一个模块只不过是一个写在文件中的 JavaScript 代码块。
123 0
好客租房162-css modules在项目中的应用
好客租房162-css modules在项目中的应用
143 0
好客租房162-css modules在项目中的应用