阅读 9174

【复习资料】ES6/ES7/ES8/ES9资料整理(个人整理)

一、介绍

现在的网络上已经有各样关于 ECMAScript 规范介绍和分析的文章,而我自己重新学习一遍这些规范,整理出这么一份笔记,比较精简,主要内容涵盖ES6ES7ES8ES9,后续会增加面试题框架入门等笔记,欢迎吐槽交流。
这份资料的ES6部分将会参考阮一峰老师的 ECMAScript6入门 ,精简和整理出快速实用的内容。
另外ES7/ES8/ES9则会从网络综合参考和整理。

ES全称ECMAScript:
目前JavaScript使用的ECMAScript版本为ECMAScript-262

ECMAScript版本 发布时间 新增特性
ECMAScript 2009(ES5) 2009年11月 扩展了Object、Array、Function的功能等
ECMAScript 2015(ES6) 2015年6月 类,模块化,箭头函数,函数参数默认值等
ECMAScript 2016(ES7) 2016年3月 includes,指数操作符
ECMAScript 2017(ES8) 2017年6月 async/await,Object.values(),Object.entries(),St123 ; b['myfun'] => 'hi'ring padding等

本文博客 CuteECMAScript
本文开源地址 CuteECMAScript
个人博客 ping'anの博客

二、正文

1. ES6

1.1 let 和 const命令

在ES6中,我们通常实用 let 表示变量const 表示常量,并且 letconst 都是块级作用域,且在当前作用域有效不能重复声明。

1.1.1 let 命令

let 命令的用法和 var 相似,但是 let 只在所在代码块内有效。
基础用法

{
    let a = 1;
    let b = 2;
}
复制代码

并且 let 有以下特点:

  • 不存在变量提升:
    在ES6之前,我们 var 声明一个变量一个函数,都会伴随着变量提升的问题,导致实际开发过程经常出现一些逻辑上的疑惑,按照一般思维习惯,变量都是需要先声明后使用。
// var 
console.log(v1); // undefined
var v1 = 2;
// 由于变量提升 代码实际如下
var v1;
console.log(v1)
v1 = 2;

// let 
console.log(v2); // ReferenceError
let v2 = 2;
复制代码
  • 不允许重复声明:
    letconst相同作用域下,都不能重复声明同一变量,并且不能在函数内重新声明参数
// 1. 不能重复声明同一变量
// 报错
function f1 (){
    let a = 1;
    var a = 2;
}
// 报错
function f2 (){
    let a = 1;
    let a = 2;
}

// 2. 不能在函数内重新声明参数
// 报错
function f3 (a1){
    let a1; 
}
// 不报错
function f4 (a2){
    {
        let a2
    }
}
复制代码

1.1.2 const 命令

const 声明一个只读常量
基础用法

const PI = 3.1415926;
console.log(PI);  // 3.1415926

复制代码

注意点

  • const 声明后,无法修改值;
const PI = 3.1415926;
PI = 3; 
// TypeError: Assignment to constant variable.
复制代码
  • const 声明时,必须赋值;
const a ; 
// SyntaxError: Missing initializer in const declaration.
复制代码
  • const 声明的常量,let 不能重复声明;
const PI = 3.1415926;
let PI = 0;  
// Uncaught SyntaxError: Identifier 'PI' has already been declared
复制代码

1.2 变量的解构赋值

解构赋值概念:在ES6中,直接从数组和对象中取值,按照对应位置,赋值给变量的操作。

1.2.1 数组

基础用法

// ES6 之前
let a = 1;
let b = 2;

// ES6 之后
let [a, b] = [1, 2];
复制代码

本质上,只要等号两边模式一致,左边变量即可获取右边对应位置的值,更多用法:

let [a, [[b], c]] = [1, [[2], 3]];
console.log(a, b, c); // 1, 2, 3

let [ , , c] = [1, 2, 3];
console.log(c);       // 3

let [a, , c] = [1, 2, 3];
console.log(a,c);     // 1, 3

let [a, ...b] = [1, 2, 3];
console.log(a,b);     // 1, [2,3]

let [a, b, ..c.] = [1];
console.log(a, b, c); // 1, undefined, []
复制代码

注意点

  • 如果解构不成功,变量的值就等于undefined
let [a] = [];     // a => undefined
let [a, b] = [1]; // a => 1 , b => undefined
复制代码
  • 当左边模式多于右边,也可以解构成功。
let [a, b] = [1, 2, 3];
console.log(a, b); // 1, 2
复制代码
  • 两边模式不同,报错。
let [a] = 1;
let [a] = false;
let [a] = NaN;
let [a] = undefined;
let [a] = null;
let [a] = {};
复制代码

指定解构的默认值
基础用法

let [a = 1] = [];      // a => 1
let [a, b = 2] = [a];  // a => 1 , b => 2
复制代码

特殊情况:

let [a = 1] = [undefined]; // a => 1
let [a = 1] = [null];      // a => null
复制代码

右边模式对应的值,必须严格等于undefined,默认值才能生效,而null不严格等于undefined

1.2.2 对象的解构赋值

与数组解构不同的是,对象解构不需要严格按照顺序取值,而只要按照变量名去取对应属性名的值,若取不到对应属性名的值,则为undefined

基础用法

let {a, b} = {a:1, b:2};  // a => 1 , b => 2
let {a, b} = {a:2, b:1};  // a => 2 , b => 1
let {a} = {a:3, b:2, c:1};// a => 3
let {a} = {b:2, c:1};     // a => undefined
复制代码

注意点

  • 变量名属性名不一致,则需要修改名称。
let {a:b} = {a:1, c:2}; 
// error: a is not defined
// b => 1
复制代码

对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。真正被赋值的是后者,而不是前者。
上面代码中,a 是匹配的模式,b才是变量。真正被赋值的是变量b,而不是模式a

  • 对象解构也支持嵌套解构
let obj = {
    a:[ 1, { b: 2}]
};
let {a, a: [c, {b}]} = obj;
// a=>[1, {b: 2}], b => 2, c => 1
复制代码

指定解构的默认值

let {a=1} = {};        // a => 1
let {a, b=1} = {a:2};  // a => 2, b => 1

let {a:b=3} = {};      // b => 3
let {a:b=3} = {a:4};   // b = >4
// a是模式,b是变量 牢记

let {a=1} = {a:undefined};  // a => 1
let {a=1} = {a:null};   // a => null
// 因为null与undefined不严格相等,所以赋值有效
// 导致默认值1不会生效。
复制代码

1.2.3 字符串的解构赋值

字符串的解构赋值中,字符串被转换成了一个类似数组的对象基础用法

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

let {length:len} = 'hello';// len => 5
复制代码

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

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

// 数值和布尔值的包装对象都有toString属性
let {toString: s} = 123;
s === Number.prototype.toString // true
let {toString: s} = true;
s === Boolean.prototype.toString // true

let { prop: x } = undefined; // TypeError
let { prop: y } = null;      // TypeError
复制代码

1.2.5 函数参数的解构赋值

基础用法

function fun ([a, b]){
    return a + b;
}
fun ([1, 2]); // 3
复制代码

指定默认值的解构:

function fun ({a=0, b=0} = {}){
    return [a, b];
}
fun ({a:1, b:2}); // [1, 2]
fun ({a:1});      // [1, 0]
fun ({});         // [0, 0]
fun ();           // [0, 0]

function fun ({a, b} = {a:0, b:0}){
    return [a, b];
}
fun ({a:1, b:2}); // [1, 2]
fun ({a:1});      // [1, undefined]
fun ({});         // [undefined, undefined]
fun ();           // [0, 0]
复制代码

1.2.6 应用

  • 交换变量的值:
let a = 1,b = 2;
[a, b] = [b, a]; // a =>2 , b => 1 
复制代码
  • 函数返回多个值:
// 返回一个数组
function f (){
    return [1, 2, 3];
}
let [a, b, c] = f(); // a=>1, b=>2, c=>3

// 返回一个对象
function f (){
    return {a:1, b:2};
}
let {a, b} = f();    // a=>1, b=>2
复制代码
  • 快速对应参数: 快速的将一组参数与变量名对应。
function f([a, b, c]) {...}
f([1, 2, 3]);

function f({a, b, c}) {...}
f({b:2, c:3, a:1});
复制代码
  • 提取JSON数据
let json = {
    name : 'leo',
    age: 18
}
let {name, age} = json;
console.log(name,age); // leo, 18
复制代码
  • 遍历Map结构:
const m = new Map();
m.set('a',1);
m.set('b',2);
for (let [k, v] of m){
    console.log(k + ' : ' + v);
}
// 获取键名
for (let [k] of m){...}
// 获取键值
for (let [,k] of m){...}
复制代码
  • 输入模块的指定方法: 用于按需加载模块中需要用到的方法。
const {log, sin, cos} = require('math');
复制代码

1.3 字符串的拓展

1.3.1 includes(),startsWith(),endsWith()

在我们判断字符串是否包含另一个字符串时,ES6之前,我们只有typeof方法,ES6之后我们又多了三种方法:

  • includes():返回布尔值,表示是否找到参数字符串
  • startsWith():返回布尔值,表示参数字符串是否在原字符串的头部
  • endsWith():返回布尔值,表示参数字符串是否在原字符串的尾部
let a = 'hello leo';
a.startsWith('leo');   // false
a.endsWith('o');       // true
a.includes('lo');      // true
复制代码

并且这三个方法都支持第二个参数,表示起始搜索的位置。

let a = 'hello leo';
a.startsWith('leo',1);   // false
a.endsWith('o',5);       // true
a.includes('lo',6);      // false
复制代码

endsWith 是针对前 n 个字符,而其他两个是针对从第n个位置直到结束。

1.3.2 repeat()

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

'ab'.repeat(3);        // 'ababab'
'ab'.repeat(0);        // ''
复制代码

特殊用法:

  • 参数为小数,则取整
'ab'.repeat(2.3);      // 'abab'
复制代码
  • 参数为负数Infinity,则报错
'ab'.repeat(-1);       // RangeError
'ab'.repeat(Infinity); // RangeError
复制代码
  • 参数为0到-1的小数NaN,则取0
'ab'.repeat(-0.5);     // ''
'ab'.repeat(NaN);      // ''
复制代码
  • 参数为字符串,则转成数字
'ab'.repeat('ab');     // ''
'ab'.repeat('3');      // 'ababab'
复制代码

1.3.3 padStart(),padEnd()

用于将字符串头部尾部补全长度,padStart()头部补全padEnd()尾部补全
这两个方法接收2个参数,第一个指定字符串最小长度,第二个用于补全的字符串
基础用法

'x'.padStart(5, 'ab');   // 'ababx'
'x'.padEnd(5, 'ab');     // 'xabab'
复制代码

特殊用法:

  • 原字符串长度,大于或等于指定最小长度,则返回原字符串。
'xyzabc'.padStart(5, 'ab'); // 'xyzabc'
复制代码
  • 用来补全的字符串长度和原字符串长度之和,超过指定最小长度,则截去超出部分的补全字符串。
'ab'.padStart(5,'012345'); // "012ab"
复制代码
  • 省略第二个参数,则用空格补全。
'x'.padStart(4);           // '    x'
'x'.padEnd(4);           // 'x    '
复制代码

1.3.4 模版字符串

用于拼接字符串,ES6之前:

let a = 'abc' + 
    'def' + 
    'ghi';
复制代码

ES6之后:

let a = `
    abc
    def
    ghi
`
复制代码

拼接变量: 在**反引号(`)**中使用${}包裹变量或方法。

// ES6之前
let a = 'abc' + v1 + 'def';

// ES6之后
let a = `abc${v1}def`
复制代码

1.4 正则的拓展

1.4.1 介绍

在ES5中有两种情况。

  • 参数是字符串,则第二个参数为正则表达式的修饰符。
let a = new RegExp('abc', 'i');
// 等价于
let a = /abx/i;
复制代码
  • 参数是正则表达式,返回一个原表达式的拷贝,且不能有第二个参数,否则报错。
let a = new RegExp(/abc/i);
//等价于
let a = /abx/i;

let a = new RegExp(/abc/, 'i');
//  Uncaught TypeError
复制代码

ES6中使用:
第一个参数是正则对象,第二个是指定修饰符,如果第一个参数已经有修饰符,则会被第二个参数覆盖。

new RegExp(/abc/ig, 'i');
复制代码

1.4.2 字符串的正则方法

常用的四种方法:match()replace()search()split()

1.4.3 u修饰符

添加u修饰符,是为了处理大于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 a = "𠮷";
/^.$/.test(a);  // false
/^.$/u.test(a); // true
复制代码
  • (2)Unicode字符表示法 使用ES6新增的大括号表示Unicode字符时,必须在表达式添加u修饰符,才能识别大括号。
/\u{61}/.test('a');      // false
/\u{61}/u.test('a');     // true
/\u{20BB7}/u.test('𠮷'); // true
复制代码
  • (3)量词 使用u修饰符后,所有量词都会正确识别码点大于0xFFFF的 Unicode 字符。
/a{2}/.test('aa');    // true
/a{2}/u.test('aa');   // true
/𠮷{2}/.test('𠮷𠮷');  // false
/𠮷{2}/u.test('𠮷𠮷'); // true
复制代码
  • (4)i修饰符 不加u修饰符,就无法识别非规范的K字符。
/[a-z]/i.test('\u212A') // false
/[a-z]/iu.test('\u212A') // true
复制代码

检查是否设置u修饰符: 使用unicode属性。

const a = /hello/;
const b = /hello/u;

a.unicode // false
b.unicode // true
复制代码

1.4.4 y修饰符

y修饰符与g修饰符类似,也是全局匹配,后一次匹配都是从上一次匹配成功的下一个位置开始。区别在于,g修饰符只要剩余位置中存在匹配即可,而y修饰符是必须从剩余第一个开始。

var s = 'aaa_aa_a';
var r1 = /a+/g;
var r2 = /a+/y;

r1.exec(s) // ["aaa"]
r2.exec(s) // ["aaa"]

r1.exec(s) // ["aa"]  剩余 '_aa_a'
r2.exec(s) // null
复制代码

lastIndex属性: 指定匹配的开始位置:

const a = /a/y;
a.lastIndex = 2;  // 从2号位置开始匹配
a.exec('wahaha'); // null
a.lastIndex = 3;  // 从3号位置开始匹配
let c = a.exec('wahaha');
c.index;          // 3
a.lastIndex;      // 4
复制代码

返回多个匹配
一个y修饰符对match方法只能返回第一个匹配,与g修饰符搭配能返回所有匹配。

'a1a2a3'.match(/a\d/y);  // ["a1"]
'a1a2a3'.match(/a\d/gy); // ["a1", "a2", "a3"]
复制代码

检查是否使用y修饰符
使用sticky属性检查。

const a = /hello\d/y;
a.sticky;     // true
复制代码

1.4.5 flags属性

flags属性返回所有正则表达式的修饰符。

/abc/ig.flags;  // 'gi'
复制代码

1.5 数值的拓展

1.5.1 Number.isFinite(), Number.isNaN()

Number.isFinite() 用于检查一个数值是否是有限的,即不是Infinity,若参数不是Number类型,则一律返回false

Number.isFinite(10);            // true
Number.isFinite(0.5);           // true
Number.isFinite(NaN);           // false
Number.isFinite(Infinity);      // false
Number.isFinite(-Infinity);     // false
Number.isFinite('leo');         // false
Number.isFinite('15');          // false
Number.isFinite(true);          // false
Number.isFinite(Math.random()); // true
复制代码

Number.isNaN()用于检查是否是NaN,若参数不是NaN,则一律返回false

Number.isNaN(NaN);      // true
Number.isNaN(10);       // false
Number.isNaN('10');     // false
Number.isNaN(true);     // false
Number.isNaN(5/NaN);    // true
Number.isNaN('true' / 0);      // true
Number.isNaN('true' / 'true'); // true
复制代码

区别
与传统全局的isFinite()isNaN()方法的区别,传统的这两个方法,是先将参数转换成数值,再判断。
而ES6新增的这两个方法则只对数值有效, Number.isFinite()对于非数值一律返回false,Number.isNaN()只有对于NaN才返回true,其他一律返回false

isFinite(25);          // true
isFinite("25");        // true
Number.isFinite(25);   // true
Number.isFinite("25"); // false

isNaN(NaN);            // true
isNaN("NaN");          // true
Number.isNaN(NaN);     // true
Number.isNaN("NaN");   // false
复制代码

1.5.2 Number.parseInt(), Number.parseFloat()

这两个方法与全局方法parseInt()parseFloat()一致,目的是逐步减少全局性的方法,让语言更模块化

parseInt('12.34');     // 12
parseFloat('123.45#'); // 123.45

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

Number.parseInt === parseInt;     // true
Number.parseFloat === parseFloat; // true
复制代码

1.5.3 Number.isInteger()

用来判断一个数值是否是整数,若参数不是数值,则返回false

Number.isInteger(10);   // true
Number.isInteger(10.0); // true
Number.isInteger(10.1); // false
复制代码

1.5.4 Math对象的拓展

ES6新增17个数学相关的静态方法,只能在Math对象上调用。

  • Math.trunc:
    用来去除小数的小数部分,返回整数部分
    若参数为非数值,则先转为数值
    若参数为空值无法截取整数的值,则返回NaN
// 正常使用
Math.trunc(1.1);     // 1
Math.trunc(1.9);     // 1
Math.trunc(-1.1);    // -1
Math.trunc(-1.9);    // -1
Math.trunc(-0.1234); // -0

// 参数为非数值
Math.trunc('11.22'); // 11
Math.trunc(true);    // 1
Math.trunc(false);   // 0
Math.trunc(null);    // 0

// 参数为空和无法取整
Math.trunc(NaN);       // NaN
Math.trunc('leo');     // NaN
Math.trunc();          // NaN
Math.trunc(undefined); // NaN
复制代码

ES5实现

Math.trunc = Math.trunc || function(x){
    return x < 0 ? Math.ceil(x) : Math.floor(x);
}
复制代码
  • Math.sign():
    判断一个数是正数负数是零,对于非数值,会先转成数值
    返回值:
    • 参数为正数, 返回 +1
    • 参数为负数, 返回 -1
    • 参数为0, 返回 0
    • 参数为-0, 返回 -0
    • 参数为其他值, 返回 NaN
Math.sign(-1);   // -1
Math.sign(1);    // +1
Math.sign(0);    // 0
Math.sign(-0);   // -0
Math.sign(NaN);  // NaN

Math.sign('');   // 0
Math.sign(true); // +1
Math.sign(false);// 0
Math.sign(null); // 0
Math.sign('9');  // +1
Math.sign('leo');// NaN
Math.sign();     // NaN
Math.sign(undefined); // NaN
复制代码

ES5实现

Math.sign = Math.sign || function (x){
    x = +x;
    if (x === 0 || isNaN(x)){
        return x;
    }
    return x > 0 ? 1: -1;
}
复制代码
  • Math.cbrt():
    用来计算一个数的立方根,若参数为非数值则先转成数值。
Math.cbrt(-1); // -1
Math.cbrt(0);  // 0
Math.cbrt(1);  // 1
Math.cbrt(2);  // 1.2599210498

Math.cbrt('1');   // 1
Math.cbrt('leo'); // NaN
复制代码

ES5实现

Math.cbrt = Math.cbrt || function (x){
    var a = Math.pow(Math.abs(x), 1/3);
    return x < 0 ? -y : y;
}
复制代码
  • Math.clz32():
    用于返回一个数的 32 位无符号整数形式有多少个前导 0。
Math.clz32(0) // 32
Math.clz32(1) // 31
Math.clz32(1000) // 22
Math.clz32(0b01000000000000000000000000000000) // 1
Math.clz32(0b00100000000000000000000000000000) // 2
复制代码
  • Math.imul():
    用于返回两个数以 32 位带符号整数形式相乘的结果,返回的也是一个 32 位的带符号整数。
Math.imul(2, 4)   // 8
Math.imul(-1, 8)  // -8
Math.imul(-2, -2) // 4
复制代码
  • Math.fround():
    用来返回一个数的2位单精度浮点数形式。
Math.fround(0)   // 0
Math.fround(1)   // 1
Math.fround(2 ** 24 - 1)   // 16777215
复制代码
  • Math.hypot():
    用来返回所有参数的平方和的平方根
Math.hypot(3, 4);        // 5
Math.hypot(3, 4, 5);     // 7.0710678118654755
Math.hypot();            // 0
Math.hypot(NaN);         // NaN
Math.hypot(3, 4, 'foo'); // NaN
Math.hypot(3, 4, '5');   // 7.0710678118654755
Math.hypot(-3);          // 3
复制代码
  • Math.expm1():
    用来返回ex - 1,即Math.exp(x) - 1
Math.expm1(-1) // -0.6321205588285577
Math.expm1(0)  // 0
Math.expm1(1)  // 1.718281828459045
复制代码

ES5实现

Math.expm1 = Math.expm1 || function(x) {
  return Math.exp(x) - 1;
};
复制代码
  • Math.log1p():
    用来返回1 + x的自然对数,即Math.log(1 + x)。如果x小于-1,返回NaN
Math.log1p(1)  // 0.6931471805599453
Math.log1p(0)  // 0
Math.log1p(-1) // -Infinity
Math.log1p(-2) // NaN
复制代码

ES5实现

Math.log1p = Math.log1p || function(x) {
  return Math.log(1 + x);
};
复制代码
  • Math.log10():
    用来返回以 10为底的x的对数。如果x小于 0,则返回 NaN
Math.log10(2)      // 0.3010299956639812
Math.log10(1)      // 0
Math.log10(0)      // -Infinity
Math.log10(-2)     // NaN
Math.log10(100000) // 5
复制代码

ES5实现

Math.log10 = Math.log10 || function(x) {
  return Math.log(x) / Math.LN10;
};
复制代码
  • Math.log2():
    用来返回以 2 为底的x的对数。如果x小于0,则返回 NaN
Math.log2(3)       // 1.584962500721156
Math.log2(2)       // 1
Math.log2(1)       // 0
Math.log2(0)       // -Infinity
Math.log2(-2)      // NaN
Math.log2(1024)    // 10
Math.log2(1 << 29) // 29
复制代码

ES5实现

Math.log2 = Math.log2 || function(x) {
  return Math.log(x) / Math.LN2;
};
复制代码
  • 双曲函数方法:
    • 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)

1.5.5 指数运算符

新增的指数运算符(**):

2 ** 2; // 4
2 ** 3; // 8 

2 ** 3 ** 2; // 相当于 2 ** (3 ** 2); 返回 512
复制代码

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

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

99 ** 99
// 3.697296376497268e+197
复制代码

1.6 函数的拓展

1.6.1 参数默认值

// ES6 之前
function f(a, b){
    b = b || 'leo';
    console.log(a, b);
}

// ES6 之后
function f(a, b='leo'){
    console.log(a, b);
}

f('hi');          // hi leo
f('hi', 'jack');  // hi jack
f('hi', '');      // hi leo
复制代码

注意:

  • 参数变量是默认声明的,不能用letconst再次声明:
function f (a = 1){
    let a = 2; // error
}
复制代码
  • 使用参数默认值时,参数名不能相同:
function f (a, a, b){ ... };     // 不报错
function f (a, a, b = 1){ ... }; // 报错
复制代码

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

function f ({a, b=1}){
    console.log(a,b)
};
f({});         // undefined 1
f({a:2});      // 2 1
f({a:2, b:3}); // 2 3
f();           // 报错

function f ({a, b = 1} = {}){
    console.log(a, b)
}
f();  // undefined 1
复制代码

尾参数定义默认值:
通常在尾参数定义默认值,便于观察参数,并且非尾参数无法省略。

function f (a=1,b){
    return [a, b];
}
f();    // [1, undefined]
f(2);   // [2, undefined]
f(,2);  // 报错

f(undefined, 2);  // [1, 2]

function f (a, b=1, c){
    return [a, b, c];
}
f();        // [undefined, 1, undefined]
f(1);       // [1,1,undefined]
f(1, ,2);   // 报错
f(1,undefined,2); // [1,1,2]
复制代码

在给参数传递默认值时,传入undefined会触发默认值,传入null不会触发。

function f (a = 1, b = 2){
    console.log(a, b);
}
f(undefined, null); // 1 null
复制代码

函数的length属性:
length属性将返回,没有指定默认值的参数数量,并且rest参数不计入length属性。

function f1 (a){...};
function f2 (a=1){...};
function f3 (a, b=2){...};
function f4 (...a){...};
function f5 (a,b,...c){...};

f1.length; // 1
f2.length; // 0
f3.length; // 1
f4.length; // 0
f5.length; // 2
复制代码

1.6.2 rest 参数

rest参数形式为(...变量名),其值为一个数组,用于获取函数多余参数。

function f (a, ...b){
    console.log(a, b);
}
f(1,2,3,4); // 1 [2, 3, 4]
复制代码

注意

  • rest参数只能放在最后一个,否则报错:
function f(a, ...b, c){...}; // 报错 
复制代码
  • 函数的length属性不包含rest参数。
function f1 (a){...};
function f2 (a,...b){...};
f1(1);   // 1
f2(1,2); // 1
复制代码

1.6.3 name 属性

用于返回该函数的函数名。

function f (){...};
f.name;    // f

const f = function g(){...};
f.name;    // g
复制代码

1.6.4 箭头函数

使用“箭头”(=>)定义函数。
基础使用

// 有1个参数
let f = v => v;
// 等同于
let f = function (v){return v};

// 有多个参数
let f = (v, i) => {return v + i};
// 等同于
let f = function (v, i){return v + i};

// 没参数
let f = () => 1;
// 等同于
let f = function (){return 1};
复制代码

箭头函数与变量结构结合使用

// 正常函数写法
function f (p) {
    return p.a + ':' + p.b;
}

// 箭头函数写法
let f = ({a, b}) => a + ':' + b;
复制代码

简化回调函数

// 正常函数写法
[1, 2, 3].map(function (x){
    return x * x;
})


// 箭头函数写法
[1, 2, 3].map(x => x * x);
复制代码

箭头函数与rest参数结合

let f = (...n) => n;
f(1, 2, 3); // [1, 2, 3]
复制代码

注意点

  • 1.箭头函数内的this总是指向定义时所在的对象,而不是调用时。
  • 2.箭头函数不能当做构造函数,即不能用new命令,否则报错。
  • 3.箭头函数不存在arguments对象,即不能使用,可以使用rest参数代替。
  • 4.箭头函数不能使用yield命令,即不能用作Generator函数。

不适用场景

  • 1.在定义函数方法,且该方法内部包含this
const obj = {
    a:9,
    b: () => {
        this.a --;
    }
}
复制代码

上述b如果是普通函数,函数内部的this指向obj,但是如果是箭头函数,则this会指向全局,不是预期结果。

  • 2.需要动态this时。
let b = document.getElementById('myID');
b.addEventListener('click', ()=>{
    this.classList.toggle('on');
})
复制代码

上诉按钮点击会报错,因为b监听的箭头函数中,this是全局对象,若改成普通函数this就会指向被点击的按钮对象。

1.6.5 双冒号运算符

双冒号暂时是一个提案,用于解决一些不适用的场合,取代callapplybind调用。
双冒号运算符(::)的左边是一个对象,右边是一个函数。该运算符会自动将左边的对象,作为上下文环境(即this对象),绑定到右边函数上。

f::b;
// 等同于
b.bind(f);

f::b(...arguments);
// 等同于
b.apply(f, arguments);
复制代码

若双冒号左边为空,右边是一个对象的方法,则等于将该方法绑定到该对象上。

let f = a::a.b;
// 等同于
let f = ::a.b;
复制代码

1.7 数组的拓展

1.7.1 拓展运算符

拓展运算符使用(...),类似rest参数的逆运算,将数组转为用(,)分隔的参数序列。

console.log(...[1, 2, 3]);   // 1 2 3 
console.log(1, ...[2,3], 4); // 1 2 3 4
复制代码

拓展运算符主要使用在函数调用。

function f (a, b){
    console.log(a, b);
}
f(...[1, 2]); // 1 2

function g (a, b, c, d, e){
    console.log(a, b, c, d, e);
}
g(0, ...[1, 2], 3, ...[4]); // 0 1 2 3 4
复制代码

若拓展运算符后面是个空数组,则不产生效果

[...[], 1]; // [1]
复制代码

替代apply方法

// ES6之前
function f(a, b, c){...};
var a = [1, 2, 3];
f.apply(null, a);

// ES6之后
function f(a, b, c){...};
let a = [1, 2, 3];
f(...a);

// ES6之前
Math.max.apply(null, [3,2,6]);

// ES6之后
Math.max(...[3,2,6]);
复制代码

拓展运算符的运用

  • (1)复制数组
    通常我们直接复制数组时,只是浅拷贝,如果要实现深拷贝,可以使用拓展运算符。
// 通常情况 浅拷贝
let a1 = [1, 2];
let a2 = a1; 
a2[0] = 3;
console.log(a1,a2); // [3,2] [3,2]

// 拓展运算符 深拷贝
let a1 = [1, 2];
let a2 = [...a1];
// let [...a2] = a1; // 作用相同
a2[0] = 3;
console.log(a1,a2); // [1,2] [3,2]
复制代码
  • (2)合并数组
    注意,这里合并数组,只是浅拷贝。
let a1 = [1,2];
let a2 = [3];
let a3 = [4,5];

// ES5 
let a4 = a1.concat(a2, a3);

// ES6
let a5 = [...a1, ...a2, ...a3];

a4[0] === a1[0]; // true
a5[0] === a1[0]; // true
复制代码
  • (3)与解构赋值结合
    与解构赋值结合生成数组,但是使用拓展运算符需要放到参数最后一个,否则报错。
let [a, ...b] = [1, 2, 3, 4]; 
// a => 1  b => [2,3,4]

let [a, ...b] = [];
// a => undefined b => []

let [a, ...b] = ["abc"];
// a => "abc"  b => []
复制代码

1.7.2 Array.from()

类数组对象可遍历的对象,转换成真正的数组。

// 类数组对象
let a = {
    '0':'a',
    '1':'b',
    length:2
}
let arr = Array.from(a);

// 可遍历的对象
let a = Array.from([1,2,3]);
let b = Array.from({length: 3});
let c = Array.from([1,2,3]).map(x => x * x);
let d = Array.from([1,2,3].map(x => x * x));
复制代码

1.7.3 Array.of()

将一组数值,转换成数组,弥补Array方法参数不同导致的差异。

Array.of(1,2,3);    // [1,2,3]
Array.of(1).length; // 1

Array();       // []
Array(2);      // [,] 1个参数时,为指定数组长度
Array(1,2,3);  // [1,2,3] 多于2个参数,组成新数组
复制代码

1.7.4 find()和findIndex()

find()方法用于找出第一个符合条件的数组成员,参数为一个回调函数,所有成员依次执行该回调函数,返回第一个返回值为true的成员,如果没有一个符合则返回undefined

[1,2,3,4,5].find( a => a < 3 ); // 1
复制代码

回调函数接收三个参数,当前值、当前位置和原数组。

[1,2,3,4,5].find((value, index, arr) => {
    // ...
});
复制代码

findIndex()方法与find()类似,返回第一个符合条件的数组成员的位置,如果都不符合则返回-1

[1,2,3,4].findIndex((v,i,a)=>{
    return v>2;
}); // 2
复制代码

1.7.5 fill()

用于用指定值填充一个数组,通常用来初始化空数组,并抹去数组中已有的元素。

new Array(3).fill('a');   // ['a','a','a']
[1,2,3].fill('a');        // ['a','a','a']
复制代码

并且fill()的第二个和第三个参数指定填充的起始位置结束位置

[1,2,3].fill('a',1,2); //  [1, "a", 3]
复制代码

1.7.6 entries(),keys(),values()

主要用于遍历数组,entries()对键值对遍历,keys()对键名遍历,values()对键值遍历。

for (let i of ['a', 'b'].keys()){
    console.log(i)
}
// 0
// 1

for (let e of ['a', 'b'].values()){
    console.log(e)
}
// 'a'
// 'b'

for (let e of ['a', 'b'].entries()){
    console.log(e)
}
// [0, "a"] 
// [1, "b"]
复制代码

1.7.7 includes()

用于表示数组是否包含给定的值,与字符串的includes方法类似。

[1,2,3].includes(2);     // true
[1,2,3].includes(4);     // false
[1,2,NaN].includes(NaN); // true
复制代码

第二个参数为起始位置,默认为0,如果负数,则表示倒数的位置,如果大于数组长度,则重置为0开始。

[1,2,3].includes(3,3);    // false
[1,2,3].includes(3,4);    // false
[1,2,3].includes(3,-1);   // true
[1,2,3].includes(3,-4);   // true
复制代码

1.7.8 flat(),flatMap()

flat()用于将数组一维化,返回一个新数组,不影响原数组。
默认一次只一维化一层数组,若需多层,则传入一个整数参数指定层数。
若要一维化所有层的数组,则传入Infinity作为参数。

[1, 2, [2,3]].flat();        // [1,2,2,3]
[1,2,[3,[4,[5,6]]]].flat(3); // [1,2,3,4,5,6]
[1,2,[3,[4,[5,6]]]].flat('Infinity'); // [1,2,3,4,5,6]
复制代码

flatMap()是将原数组每个对象先执行一个函数,在对返回值组成的数组执行flat()方法,返回一个新数组,不改变原数组。
flatMap()只能展开一层。

[2, 3, 4].flatMap((x) => [x, x * 2]); 
// [2, 4, 3, 6, 4, 8] 
复制代码

1.8 对象的拓展

1.8.1 属性的简洁表示

let a = 'a1';
let b = { a };  // b => { a : 'a1' }
// 等同于
let b = { a : a };

function f(a, b){
    return {a, b}; 
}
// 等同于
function f (a, b){
    return {a:a ,b:b};
}

let a = {
    fun () {
        return 'leo';
    }
}
// 等同于
let a = {
    fun : function(){
        return 'leo';
    }
}
复制代码

1.8.2 属性名表达式

JavaScript提供2种方法定义对象的属性

// 方法1 标识符作为属性名
a.f = true;

// 方法2 字符串作为属性名
a['f' + 'un'] = true;
复制代码

延伸出来的还有:

let a = 'hi leo';
let b = {
    [a]: true,
    ['a'+'bc']: 123,
    ['my' + 'fun'] (){
        return 'hi';
    }
};
// b.a => undefined ; b.abc => 123 ; b.myfun() => 'hi'
// b[a] => true ; b['abc'] => 123 ; b['myfun'] => ƒ ['my' + 'fun'] (){ return 'hi'; }
复制代码

注意
属性名表达式不能与简洁表示法同时使用,否则报错。

// 报错
let a1 = 'aa';
let a2 = 'bb';
let b1 = {[a1]};

// 正确
let a1 = 'aa';
let b1 = { [a1] : 'bb'};
复制代码

1.8.3 Object.is()

Object.is() 用于比较两个值是否严格相等,在ES5时候只要使用相等运算符(==)和严格相等运算符(===)就可以做比较,但是它们都有缺点,前者会自动转换数据类型,后者的NaN不等于自身,以及+0等于-0

Object.is('a','a');   // true
Object.is({}, {});    // false

// ES5
+0 === -0 ;           // true
NaN === NaN;          // false

// ES6
Object.is(+0,-0);     // false
Object.is(NaN,NaN);   // true
复制代码

1.8.4 Object.assign()

Object.assign()方法用于对象的合并,将原对象的所有可枚举属性复制到目标对象。
基础用法
第一个参数是目标对象,后面参数都是源对象

let a = {a:1};
let b = {b:2};
Object.assign(a,b);  // a=> {a:1,b:2}
复制代码

注意

  • 若目标对象与源对象有同名属性,则后面属性会覆盖前面属性。
let a = {a:1, b:2};
let b = {b:3, c:4};
Object.assign(a, b); // a => {a:1, b:3, c:4}
复制代码
  • 若只有一个参数,则返回该参数。
let a = {a:1};
Object.assign(a) === a;  // true
复制代码
  • 若参数不是对象,则先转成对象后返回。
typeof Object.assign(2); // 'object'
复制代码
  • 由于undefinedNaN无法转成对象,所以做为参数会报错。
Object.assign(undefined) // 报错
Object.assign(NaN);      // 报错
复制代码
  • Object.assign()实现的是浅拷贝。

Object.assign()拷贝得到的是这个对象的引用。这个对象的任何变化,都会反映到目标对象上面。

let a = {a: {b:1}};
let b = Object.assign({},a);
a.a.b = 2;
console.log(b.a.b);  // 2
复制代码
  • 将数组当做对象处理,键名为数组下标,键值为数组下标对应的值。
Object.assign([1, 2, 3], [4, 5]); // [4, 5, 3]
复制代码

1.9 Symbol

1.9.1 介绍

ES6引入Symbol作为一种新的原始数据类型,表示独一无二的值,主要是为了防止属性名冲突
ES6之后,JavaScript一共有其中数据类型:SymbolundefinednullBooleanStringNumberObject
简单实用:

let a = Symbol();
typeof a; // "symbol"
复制代码

注意:

  • Symbol函数不能用new,会报错。由于Symbol是一个原始类型,不是对象,所以不能添加属性,它是类似于字符串的数据类型。
  • Symbol都是不相等的,即使参数相同。
// 没有参数
let a1 = Symbol();
let a2 = Symbol();
a1 === a2; // false 

// 有参数
let a1 = Symbol('abc');
let a2 = Symbol('abc');
a1 === a2; // false 
复制代码
  • Symbol不能与其他类型的值计算,会报错。
let a = Symbol('hello');
a + " world!";  // 报错
`${a} world!`;  // 报错
复制代码

Symbol可以显式转换为字符串:

let a1 = Symbol('hello');

String(a1);    // "Symbol(hello)"
a1.toString(); // "Symbol(hello)"
复制代码

Symbol可以转换为布尔值,但不能转为数值:

let a1 = Symbol();
Boolean(a1);
!a1;        // false

Number(a1); // TypeError
a1 + 1 ;    // TypeError
复制代码

1.9.2 Symbol作为属性名

好处:防止同名属性,还有防止键被改写或覆盖。

let a1 = Symbol();

// 写法1
let b = {};
b[a1] = 'hello';

// 写法2
let b = {
    [a1] : 'hello'
} 

// 写法3
let b = {};
Object.defineProperty(b, a1, {value : 'hello' });

// 3种写法 结果相同
b[a1]; // 'hello'
复制代码

需要注意: Symbol作为对象属性名时,不能用点运算符,并且必须放在方括号内。

let a = Symbol();
let b = {};

// 不能用点运算
b.a = 'hello';
b[a] ; // undefined
b['a'] ; // 'hello'

// 必须放在方括号内
let c = {
    [a] : function (text){
        console.log(text);
    }
}
c[a]('leo'); // 'leo'

// 上面等价于 更简洁
let c = {
    [a](text){
        console.log(text);
    }
}
复制代码

常常还用于创建一组常量,保证所有值不相等:

let a = {};
a.a1 = {
    AAA: Symbol('aaa'),
    BBB: Symbol('bbb'),
    CCC: Symbol('ccc')
}
复制代码

1.9.3 应用:消除魔术字符串

魔术字符串:指代码中多次出现,强耦合的字符串或数值,应该避免,而使用含义清晰的变量代替。

function f(a){
    if(a == 'leo') {
        console.log('hello');
    }
}
f('leo');   // 'leo' 为魔术字符串
复制代码

常使用变量,消除魔术字符串:

let obj = {
    name: 'leo'
};
function f (a){
    if(a == obj.name){
        console.log('hello');
    }
}
f(obj.name); // 'leo'
复制代码

使用Symbol消除强耦合,使得不需关系具体的值:

let obj = {
    name: Symbol()
};
function f (a){
    if(a == obj.name){
        console.log('hello');
    }
}
f(obj.name);
复制代码

1.9.4 属性名遍历

Symbol作为属性名遍历,不出现在for...infor...of循环,也不被Object.keys()Object.getOwnPropertyNames()JSON.stringify()返回。

let a = Symbol('aa'),b= Symbol('bb');
let obj = {
    [a]:'11', [b]:'22'
}
for(let k of Object.values(obj)){console.log(k)}
// 无输出

let obj = {};
let aa = Symbol('leo');
Object.defineProperty(obj, aa, {value: 'hi'});

for(let k in obj){
    console.log(k); // 无输出
}

Object.getOwnPropertyNames(obj);   // []
Object.getOwnPropertySymbols(obj); // [Symbol(leo)]
复制代码

Object.getOwnPropertySymbols方法返回一个数组,包含当前对象所有用做属性名的Symbol值。

let a = {};
let a1 = Symbol('a');
let a2 = Symbol('b');
a[a1] = 'hi';
a[a2] = 'oi';

let obj = Object.getOwnPropertySymbols(a);
obj; //  [Symbol(a), Symbol(b)]
复制代码

另外可以使用Reflect.ownKeys方法可以返回所有类型的键名,包括常规键名和 Symbol 键名。

let a = {
    [Symbol('leo')]: 1,
    aa : 2, 
    bb : 3,
}
Reflect.ownKeys(a); // ['aa', 'bb',Symbol('leo')]
复制代码

由于Symbol值作为名称的属性不被常规方法遍历获取,因此常用于定义对象的一些非私有,且内部使用的方法。

1.9.5 Symbol.for()、Symbol.keyFor()

  • Symbol.for()
    用于重复使用一个Symbol值,接收一个字符串作为参数,若存在用此参数作为名称的Symbol值,返回这个Symbol,否则新建并返回以这个参数为名称的Symbol值。
let a = Symbol.for('aaa');
let b = Symbol.for('aaa');

a === b;  // true
复制代码

Symbol()Symbol.for()区别:

Symbol.for('aa') === Symbol.for('aa'); // true
Symbol('aa') === Symbol('aa');         // false
复制代码
  • Symbol.keyFor()
    用于返回一个已使用的Symbol类型的key:
let a = Symbol.for('aa');
Symbol.keyFor(a);   //  'aa'

let b = Symbol('aa');
Symbol.keyFor(b);   //  undefined
复制代码

1.9.6 内置的Symbol值

ES6提供11个内置的Symbol值,指向语言内部使用的方法:

  • 1.Symbol.hasInstance
    当其他对象使用instanceof运算符,判断是否为该对象的实例时,会调用这个方法。比如,foo instanceof Foo在语言内部,实际调用的是Foo[Symbol.hasInstance](foo)
class P {
    [Symbol.hasInstance](a){
        return a instanceof Array;
    }
}
[1, 2, 3] instanceof new P(); // true
复制代码

P是一个类,new P()会返回一个实例,该实例的Symbol.hasInstance方法,会在进行instanceof运算时自动调用,判断左侧的运算子是否为Array的实例。

  • 2.Symbol.isConcatSpreadable
    值为布尔值,表示该对象用于Array.prototype.concat()时,是否可以展开。
let a = ['aa','bb'];
['cc','dd'].concat(a, 'ee'); 
// ['cc', 'dd', 'aa', 'bb', 'ee']
a[Symbol.isConcatSpreadable]; // undefined

let b = ['aa','bb']; 
b[Symbol.isConcatSpreadable] = false; 
['cc','dd'].concat(b, 'ee'); 
// ['cc', 'dd',[ 'aa', 'bb'], 'ee']
复制代码
  • 3.Symbol.species
    指向一个构造函数,在创建衍生对象时会使用,使用时需要用get取值器。
class P extends Array {
    static get [Symbol.species](){
        return this;
    }
}
复制代码

解决下面问题:

// 问题:  b应该是 Array 的实例,实际上是 P 的实例
class P extends Array{}

let a = new P(1,2,3);
let b = a.map(x => x);

b instanceof Array; // true
b instanceof P; // true

// 解决:  通过使用 Symbol.species
class P extends Array {
  static get [Symbol.species]() { return Array; }
}
let a = new P();
let b = a.map(x => x);
b instanceof P;     // false
b instanceof Array; // true
复制代码
  • 4.Symbol.match
    当执行str.match(myObject),传入的属性存在时会调用,并返回该方法的返回值。
class P {
    [Symbol.match](string){
        return 'hello world'.indexOf(string);
    }
}
'h'.match(new P());   // 0
复制代码
  • 5.Symbol.replace 当该对象被String.prototype.replace方法调用时,会返回该方法的返回值。
let a = {};
a[Symbol.replace] = (...s) => console.log(s);
'Hello'.replace(a , 'World') // ["Hello", "World"]
复制代码
  • 6.Symbol.hasInstance
    当该对象被String.prototype.search方法调用时,会返回该方法的返回值。
class P {
    constructor(val) {
        this.val = val;
    }
    [Symbol.search](s){
        return s.indexOf(this.val);
    }
}
'hileo'.search(new P('leo')); // 2
复制代码
  • 7.Symbol.split
    当该对象被String.prototype.split方法调用时,会返回该方法的返回值。
// 重新定义了字符串对象的split方法的行为
class P {
    constructor(val) {
        this.val = val;
    }
    [Symbol.split](s) {
        let i = s.indexOf(this.val);
        if(i == -1) return s;
        return [
            s.substr(0, i),
            s.substr(i + this.val.length)
        ]
    }
}

'helloworld'.split(new P('hello')); // ["hello", ""]
'helloworld'.split(new P('world')); // ["", "world"] 
'helloworld'.split(new P('leo'));   // "helloworld"
复制代码
  • 8.Symbol.iterator
    对象进行for...of循环时,会调用Symbol.iterator方法,返回该对象的默认遍历器。
class P {
    *[Symbol.interator]() {
        let i = 0;
        while(this[i] !== undefined ) {
            yield this[i];
            ++i;
        }
    }
}
let a = new P();
a[0] = 1;
a[1] = 2;

for (let k of a){
    console.log(k);
}
复制代码
  • 9.Symbol.toPrimitive
    该对象被转为原始类型的值时,会调用这个方法,返回该对象对应的原始类型值。调用时,需要接收一个字符串参数,表示当前运算模式,运算模式有:
    • Number : 此时需要转换成数值
    • String : 此时需要转换成字符串
    • Default : 此时可以转换成数值或字符串
let obj = {
  [Symbol.toPrimitive](hint) {
    switch (hint) {
      case 'number':
        return 123;
      case 'string':
        return 'str';
      case 'default':
        return 'default';
      default:
        throw new Error();
     }
   }
};

2 * obj // 246
3 + obj // '3default'
obj == 'default' // true
String(obj) // 'str'
复制代码
  • 10.Symbol.toStringTag
    在该对象上面调用Object.prototype.toString方法时,如果这个属性存在,它的返回值会出现在toString方法返回的字符串之中,表示对象的类型。也就是说,这个属性可以用来定制[object Object]或[object Array]object后面的那个字符串。
// 例一
({[Symbol.toStringTag]: 'Foo'}.toString())
// "[object Foo]"

// 例二
class Collection {
  get [Symbol.toStringTag]() {
    return 'xxx';
  }
}
let x = new Collection();
Object.prototype.toString.call(x) // "[object xxx]"
复制代码
  • 11.Symbol.unscopables
    该对象指定了使用with关键字时,哪些属性会被with环境排除。
// 没有 unscopables 时
class MyClass {
  foo() { return 1; }
}

var foo = function () { return 2; };

with (MyClass.prototype) {
  foo(); // 1
}

// 有 unscopables 时
class MyClass {
  foo() { return 1; }
  get [Symbol.unscopables]() {
    return { foo: true };
  }
}

var foo = function () { return 2; };

with (MyClass.prototype) {
  foo(); // 2
}
复制代码

上面代码通过指定Symbol.unscopables属性,使得with语法块不会在当前作用域寻找foo属性,即foo将指向外层作用域的变量。

1.10 Set和Map数据结构

1.10.1 Set

介绍:
Set数据结构类似数组,但所有成员的值唯一
Set本身为一个构造函数,用来生成Set数据结构,使用add方法来添加新成员。

let a = new Set();
[1,2,2,1,3,4,5,4,5].forEach(x=>a.add(x));
for(let k of a){
    console.log(k)
};
// 1 2 3 4 5
复制代码

基础使用

let a = new Set([1,2,3,3,4]);
[...a]; // [1,2,3,4]
a.size; // 4

// 数组去重
[...new Set([1,2,3,4,4,4])];// [1,2,3,4]
复制代码

注意

  • Set中添加值的时候,不会类型转换,即5'5'是不同的。
[...new Set([5,'5'])]; // [5, "5"]
复制代码

属性和方法

  • 属性:

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

    • add(value):添加某个值,返回 Set 结构本身。
    • delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
    • has(value):返回一个布尔值,表示该值是否为Set的成员。
    • clear():清除所有成员,没有返回值。
let a = new Set();
a.add(1).add(2); // a => Set(2) {1, 2}
a.has(2);        // true
a.has(3);        // false
a.delete(2);     // true  a => Set(1) {1}
a.clear();       // a => Set(0) {}
复制代码

数组去重

let a = new Set([1,2,3,3,3,3]);
复制代码

1.10.2 Set的应用

数组去重

// 方法1
[...new Set([1,2,3,4,4,4])]; // [1,2,3,4]
// 方法2
Array.from(new Set([1,2,3,4,4,4]));    // [1,2,3,4]
复制代码

遍历和过滤

let a = new Set([1,2,3,4]);

// map 遍历操作
let b = new Set([...a].map(x =>x*2));// b => Set(4) {2,4,6,8}

// filter 过滤操作
let c = new Set([...a].filter(x =>(x%2) == 0)); // b => Set(2) {2,4}
复制代码

获取并集、交集和差集

let a = new Set([1,2,3]);
let b = new Set([4,3,2]);

// 并集
let c1 = new Set([...a, ...b]);  // Set {1,2,3,4}

// 交集
let c2 = new Set([...a].filter(x => b.has(x))); // set {2,3}

// 差集
let c3 = new Set([...a].filter(x => !b.has(x))); // set {1}
复制代码
  • 遍历方法:
    • keys():返回键名的遍历器。
    • values():返回键值的遍历器。
    • entries():返回键值对的遍历器。
    • forEach():使用回调函数遍历每个成员

Set遍历顺序是插入顺序,当保存多个回调函数,只需按照顺序调用。但由于Set结构没有键名只有键值,所以keys()values()是返回结果相同。

let a = new Set(['a','b','c']);
for(let i of a.keys()){console.log(i)};   // 'a' 'b' 'c'
for(let i of a.values()){console.log(i)}; // 'a' 'b' 'c'
for(let i of a.entries()){console.log(i)}; 
// ['a','a'] ['b','b'] ['c','c']
复制代码

并且 还可以使用for...of直接遍历Set

let a = new Set(['a','b','c']);
for(let k of a){console.log(k)};   // 'a' 'b' 'c'
复制代码

forEach与数组相同,对每个成员执行操作,且无返回值。

let a = new Set(['a','b','c']);
a.forEach((v,k) => console.log(k + ' : ' + v));
复制代码

1.10.3 Map

由于传统的JavaScript对象只能用字符串当做键,给开发带来很大限制,ES6增加Map数据结构,使得各种类型的值(包括对象)都可以作为键。
Map结构提供了“值—值”的对应,是一种更完善的 Hash 结构实现。 基础使用

let a = new Map();
let b = {name: 'leo' };
a.set(b,'my name'); // 添加值
a.get(b);           // 获取值
a.size;      // 获取总数
a.has(b);    // 查询是否存在
a.delete(b); // 删除一个值
a.clear();   // 清空所有成员 无返回
复制代码

注意

  • 传入数组作为参数,指定键值对的数组
let a = new Map([
    ['name','leo'],
    ['age',18]
])
复制代码
  • 如果对同一个键多次赋值,后面的值将覆盖前面的值
let a = new Map();
a.set(1,'aaa').set(1,'bbb');
a.get(1); // 'bbb'
复制代码
  • 如果读取一个未知的键,则返回undefined
new Map().get('abcdef'); // undefined
复制代码
  • 同样的值的两个实例,在 Map 结构中被视为两个键。
let a = new Map();
let a1 = ['aaa'];
let a2 = ['aaa'];
a.set(a1,111).set(a2,222);
a.get(a1); // 111
a.get(a2); // 222
复制代码

遍历方法: Map 的遍历顺序就是插入顺序。

  • keys():返回键名的遍历器。
  • values():返回键值的遍历器。
  • entries():返回所有成员的遍历器。
  • forEach():遍历 Map 的所有成员。
let a = new Map([
    ['name','leo'],
    ['age',18]
])

for (let i of a.keys()){...};
for (let i of a.values()){...};
for (let i of a.entries()){...};
a.forEach((v,k,m)=>{
    console.log(`key:${k},value:${v},map:${m}`)
})
复制代码

将Map结构转成数组结构

let a = new Map([
    ['name','leo'],
    ['age',18]
])

let a1 = [...a.keys()];   // a1 => ["name", "age"]
let a2 = [...a.values()]; // a2 =>  ["leo", 18]
let a3 = [...a.entries()];// a3 => [['name','leo'], ['age',18]]
复制代码

1.10.4 Map与其他数据结构互相转换

  • Map 转 数组
let a = new Map().set(true,1).set({f:2},['abc']);
[...a]; // [[true:1], [ {f:2},['abc'] ]]
复制代码
  • 数组 转 Map
let a = [ ['name','leo'], [1, 'hi' ]]
let b = new Map(a);
复制代码
  • Map 转 对象 如果所有 Map 的键都是字符串,它可以无损地转为对象。
    如果有非字符串的键名,那么这个键名会被转成字符串,再作为对象的键名。
function fun(s) {
  let obj = Object.create(null);
  for (let [k,v] of s) {
    obj[k] = v;
  }
  return obj;
}

const a = new Map().set('yes', true).set('no', false);
fun(a)
// { yes: true, no: false }
复制代码
  • 对象 转 Map
function fun(obj) {
  let a = new Map();
  for (let k of Object.keys(obj)) {
    a.set(k, obj[k]);
  }
  return a;
}

fun({yes: true, no: false})
// Map {"yes" => true, "no" => false}
复制代码
  • Map 转 JSON
    (1)Map键名都是字符串,转为对象JSON:
function fun (s) {
    let obj = Object.create(null);
    for (let [k,v] of s) {
        obj[k] = v;
    }
    return JSON.stringify(obj)
}
let a = new Map().set('yes', true).set('no', false);
fun(a);
// '{"yes":true,"no":false}'
复制代码

(2)Map键名有非字符串,转为数组JSON:

function fun (map) {
  return JSON.stringify([...map]);
}

let a = new Map().set(true, 7).set({foo: 3}, ['abc']);
fun(a)
// '[[true,7],[{"foo":3},["abc"]]]'
复制代码
  • JSON 转 Map
    (1)所有键名都是字符串:
function fun (s) {
  let strMap = new Map();
  for (let k of Object.keys(s)) {
    strMap.set(k, s[k]);
  }
  return strMap;
  return JSON.parse(strMap);
}
fun('{"yes": true, "no": false}')
// Map {'yes' => true, 'no' => false}
复制代码

(2)整个 JSON 就是一个数组,且每个数组成员本身,又是一个有两个成员的数组:

function fun2(s) {
  return new Map(JSON.parse(s));
}
fun2('[[true,7],[{"foo":3},["abc"]]]')
// Map {true => 7, Object {foo: 3} => ['abc']}
复制代码

1.11 Proxy

proxy 用于修改某些操作的默认行为,可以理解为一种拦截外界对目标对象访问的一种机制,从而对外界的访问进行过滤和修改,即代理某些操作,也称“代理器”。

1.11.1 基础使用

proxy实例化需要传入两个参数,target参数表示所要拦截的目标对象,handler参数也是一个对象,用来定制拦截行为。

let p = new Proxy(target, handler);

let a = new Proxy({}, {
    get: function (target, handler){
        return 'leo';
    }
})
a.name; // leo
a.age;  // leo
a.abcd; // leo
复制代码

上述a实例中,在第二个参数中定义了get方法,来拦截外界访问,并且get方法接收两个参数,分别是目标对象所要访问的属性,所以不管外部访问对象中任何属性都会执行get方法返回leo
注意

  • 只能使用Proxy实例的对象才能使用这些操作。
  • 如果handler没有设置拦截,则直接返回原对象。
let target = {};
let handler = {};
let p = new Proxy(target, handler);
p.a = 'leo'; 
target.a;  // 'leo'
复制代码

同个拦截器函数,设置多个拦截操作

let p = new Proxy(function(a, b){
    return a + b;
},{
    get:function(){
        return 'get方法';
    },
    apply:function(){
        return 'apply方法';
    }
})
复制代码

Proxy支持的13种拦截操作
13种拦截操作的详细介绍:打开阮一峰老师的链接

  • get(target, propKey, receiver): 拦截对象属性的读取,比如proxy.foo和proxy['foo']。

  • set(target, propKey, value, receiver): 拦截对象属性的设置,比如proxy.foo = v或proxy['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.11.2 取消Proxy实例

使用Proxy.revocale方法取消Proxy实例。

let a = {};
let b = {};
let {proxy, revoke} = Proxy.revocale(a, b);

proxy.name = 'leo';  // 'leo'
revoeke();
proxy.name;  // TypeError: Revoked
复制代码

1.11.3 实现 Web服务的客户端

const service = createWebService('http://le.com/data');
service.employees().than(json =>{
    const employees = JSON.parse(json);
})

function createWebService(url){
    return new Proxy({}, {
        get(target, propKey, receiver{
            return () => httpGet(url+'/'+propKey);
        })
    })
}
复制代码

1.12 Promise对象

1.12.1 概念

主要用途:解决异步编程带来的回调地狱问题
Promise简单理解一个容器,存放着某个未来才会结束的事件(通常是一个异步操作)的结果。通过Promise对象来获取异步操作消息,处理各种异步操作。

Promise对象2特点

  • 对象的状态不受外界影响

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

  • 一旦状态改变,就不会再变,任何时候都可以得到这个结果

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

注意,为了行文方便,本章后面的resolved统一只指fulfilled状态,不包含rejected状态。

Promise缺点

  • 无法取消Promise,一旦新建它就会立即执行,无法中途取消。
  • 如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。
  • 当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

1.12.2 基本使用

Promise为一个构造函数,需要用new来实例化。

let p = new Promise(function (resolve, reject){
   if(/*异步操作成功*/){
       resolve(value);
   } else {
       reject(error);
   }
})
复制代码

Promise接收一个函数作为参数,该函数两个参数resolvereject,有JS引擎提供。

  • resolve作用是将Promise的状态从pending变成resolved,在异步操作成功时调用,返回异步操作的结果,作为参数传递出去。
  • reject作用是将Promise的状态从pending变成rejected,在异步操作失败时报错,作为参数传递出去。

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

p.then(function(val){
    // success...
},function(err){
    // error...
})
复制代码

几个例子来理解

  • 当一段时间过后,Promise状态便成为resolved触发then方法绑定的回调函数。
function timeout (s){
    return new Promise((resolve, reject){
        setTimeout(result,ms, 'done');
    })
}
timeout(100).then(val => {
    console.log(val);
})
复制代码
  • Promise新建后立刻执行。
let p = new Promise(function(resolve, reject){
    console.log(1);
    resolve();
})
p.then(()=>{
    console.log(2);
})
console.log(3);
// 1
// 3
// 2 
复制代码

异步加载图片

function f(url){
    return new Promise(function(resolve, reject){
        const img = new Image ();
        img.onload = function(){
            resolve(img);
        }
        img.onerror = function(){
            reject(new Error(
                'Could not load image at ' + url
            ));
        }
        img.src = url;
    })
}
复制代码

resolve函数和reject函数的参数为resolve函数或reject函数
p1的状态决定了p2的状态,所以p2要等待p1的结果再执行回调函数。

const p1 = new Promise(function (resolve, reject) {
  setTimeout(() => reject(new Error('fail')), 3000)
})

const p2 = new Promise(function (resolve, reject) {
  setTimeout(() => resolve(p1), 1000)
})

p2
  .then(result => console.log(result))
  .catch(error => console.log(error))
// Error: fail
复制代码

调用resolvereject不会结束Promise参数函数的执行,除了return:

new Promise((resolve, reject){
    resolve(1);
    console.log(2);
}).then(r => {
    console.log(3);
})
// 2
// 1

new Promise((resolve, reject){
    return resolve(1);
    console.log(2);
})
// 1
复制代码

1.12.3 Promise.prototype.then()

作用是为Promise添加状态改变时的回调函数,then方法的第一个参数是resolved状态的回调函数,第二个参数(可选)是rejected状态的回调函数。
then方法返回一个新Promise实例,与原来Promise实例不同,因此可以使用链式写法,上一个then的结果作为下一个then的参数。

getJSON("/posts.json").then(function(json) {
  return json.post;
}).then(function(post) {
  // ...
});
复制代码

1.12.4 Promise.prototype.catch()

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

getJSON('/posts.json').then(function(posts) {
  // ...
}).catch(function(error) {
  // 处理 getJSON 和 前一个回调函数运行时发生的错误
  console.log('发生错误!', error);
});
复制代码

如果 Promise 状态已经变成resolved,再抛出错误是无效的。

const p = new Promise(function(resolve, reject) {
  resolve('ok');
  throw new Error('test');
});
p
  .then(function(value) { console.log(value) })
  .catch(function(error) { console.log(error) });
// ok
复制代码

promise抛出一个错误,就被catch方法指定的回调函数捕获,下面三种写法相同。

// 写法一
const p = new Promise(function(resolve, reject) {
  throw new Error('test');
});
p.catch(function(error) {
  console.log(error);
});
// Error: test

// 写法二
const p = new Promise(function(resolve, reject) {
  try {
    throw new Error('test');
  } catch(e) {
    reject(e);
  }
});
p.catch(function(error) {
  console.log(error);
});

// 写法三
const p = new Promise(function(resolve, reject) {
  reject(new Error('test'));
});
p.catch(function(error) {
  console.log(error);
});
复制代码

一般来说,不要在then方法里面定义Reject 状态的回调函数(即then的第二个参数),总是使用catch方法。

// bad
promise
  .then(function(data) {
    // success
  }, function(err) {
    // error
  });

// good
promise
  .then(function(data) { //cb
    // success
  })
  .catch(function(err) {
    // error
  });
复制代码

1.12.5 Promise.prototype.finally()

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

promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});
复制代码

finally不接收任何参数,与状态无关,本质上是then方法的特例。

promise
.finally(() => {
  // 语句
});

// 等同于
promise
.then(
  result => {
    // 语句
    return result;
  },
  error => {
    // 语句
    throw error;
  }
);
复制代码

上面代码中,如果不使用finally方法,同样的语句需要为成功和失败两种情况各写一次。有了finally方法,则只需要写一次。
finally方法总是会返回原来的值。

// resolve 的值是 undefined
Promise.resolve(2).then(() => {}, () => {})

// resolve 的值是 2
Promise.resolve(2).finally(() => {})

// reject 的值是 undefined
Promise.reject(3).then(() => {}, () => {})

// reject 的值是 3
Promise.reject(3).finally(() => {})
复制代码

1.12.6 Promise.all()

用于将多个 Promise 实例,包装成一个新的 Promise 实例,参数可以不是数组,但必须是Iterator接口,且返回的每个成员都是Promise实例。

const p = Promise.all([p1, p2, p3]);
复制代码

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

  1. 只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。
  2. 只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。
// 生成一个Promise对象的数组
const promises = [2, 3, 5, 7, 11, 13].map(function (id) {
  return getJSON('/post/' + id + ".json");
});

Promise.all(promises).then(function (posts) {
  // ...
}).catch(function(reason){
  // ...
});
复制代码

上面代码中,promises是包含 6 个 Promise 实例的数组,只有这 6 个实例的状态都变成fulfilled,或者其中有一个变为rejected,才会调用Promise.all方法后面的回调函数。

注意:如果Promise的参数中定义了catch方法,则rejected后不会触发Promise.all()catch方法,因为参数中的catch方法执行完后也会变成resolved,当Promise.all()方法参数的实例都是resolved时就会调用Promise.all()then方法。

const p1 = new Promise((resolve, reject) => {
  resolve('hello');
})
.then(result => result)
.catch(e => e);

const p2 = new Promise((resolve, reject) => {
  throw new Error('报错了');
})
.then(result => result)
.catch(e => e);

Promise.all([p1, p2])
.then(result => console.log(result))
.catch(e => console.log(e));
// ["hello", Error: 报错了]
复制代码

如果参数里面都没有catch方法,就会调用Promise.all()的catch方法。

const p1 = new Promise((resolve, reject) => {
  resolve('hello');
})
.then(result => result);

const p2 = new Promise((resolve, reject) => {
  throw new Error('报错了');
})
.then(result => result);

Promise.all([p1, p2])
.then(result => console.log(result))
.catch(e => console.log(e));
// Error: 报错了
复制代码

1.12.7 Promise.race()

Promise.all方法类似,也是将多个Promise实例包装成一个新的Promise实例。

const p = Promise.race([p1, p2, p3]);
复制代码

Promise.all方法区别在于,Promise.race方法是p1, p2, p3中只要一个参数先改变状态,就会把这个参数的返回值传给p的回调函数。

1.12.8 Promise.resolve()

将现有对象转换成 Promise 对象。

const p = Promise.resolve($.ajax('/whatever.json'));
复制代码

1.12.9 Promise.reject()

返回一个rejected状态的Promise实例。

const p = Promise.reject('出错了');
// 等同于
const p = new Promise((resolve, reject) => reject('出错了'))

p.then(null, function (s) {
  console.log(s)
});
// 出错了
复制代码

注意,Promise.reject()方法的参数,会原封不动地作为reject的理由,变成后续方法的参数。这一点与Promise.resolve方法不一致。

const thenable = {
  then(resolve, reject) {
    reject('出错了');
  }
};

Promise.reject(thenable)
.catch(e => {
  console.log(e === thenable)
})
// true
复制代码

1.13 Iterator和 for...of循环

1.13.1 Iterator遍历器概念

Iterator是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。

Iterator三个作用

  • 为各种数据结构,提供一个统一的、简便的访问接口;
  • 使得数据结构的成员能够按某种次序排列;
  • Iterator 接口主要供ES6新增的for...of消费;

1.13.2 Iterator遍历过程

  1. 创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。
  2. 第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员。
  3. 第二次调用指针对象的next方法,指针就指向数据结构的第二个成员。
  4. 不断调用指针对象的next方法,直到它指向数据结构的结束位置。

每一次调用next方法,都会返回数据结构的当前成员的信息。具体来说,就是返回一个包含valuedone两个属性的对象。

  • value属性是当前成员的值;
  • done属性是一个布尔值,表示遍历是否结束;

模拟next方法返回值:

let f = function (arr){
    var nextIndex = 0;
    return {
        next:function(){
            return nextIndex < arr.length ?
            {value: arr[nextIndex++], done: false}:
            {value: undefined, done: true}
        }
    }
}

let a = f(['a', 'b']);
a.next(); // { value: "a", done: false }
a.next(); // { value: "b", done: false }
a.next(); // { value: undefined, done: true }
复制代码

1.13.3 默认Iterator接口

若数据可遍历,即一种数据部署了Iterator接口。
ES6中默认的Iterator接口部署在数据结构的Symbol.iterator属性,即如果一个数据结构具有Symbol.iterator属性,就可以认为是可遍历
Symbol.iterator属性本身是函数,是当前数据结构默认的遍历器生成函数。执行这个函数,就会返回一个遍历器。至于属性名Symbol.iterator,它是一个表达式,返回Symbol对象的iterator属性,这是一个预定义好的、类型为 Symbol 的特殊值,所以要放在方括号内(参见《Symbol》一章)。

原生具有Iterator接口的数据结构有

  • Array
  • Map
  • Set
  • String
  • TypedArray
  • 函数的 arguments 对象
  • NodeList 对象

1.13.4 Iterator使用场景

  • (1)解构赋值
    对数组和 Set 结构进行解构赋值时,会默认调用Symbol.iterator方法。
let a = new Set().add('a').add('b').add('c');
let [x, y] = a;       // x = 'a'  y = 'b'
let [a1, ...a2] = a;  // a1 = 'a' a2 = ['b','c']
复制代码
  • (2)扩展运算符
    扩展运算符(...)也会调用默认的 Iterator 接口。
let a = 'hello';
[...a];            //  ['h','e','l','l','o']

let a = ['b', 'c'];
['a', ...a, 'd'];  // ['a', 'b', 'c', 'd']
复制代码
  • (2)yield*
    yield*后面跟的是一个可遍历的结构,它会调用该结构的遍历器接口。
let a = function*(){
    yield 1;
    yield* [2,3,4];
    yield 5;
}

let b = a();
b.next() // { value: 1, done: false }
b.next() // { value: 2, done: false }
b.next() // { value: 3, done: false }
b.next() // { value: 4, done: false }
b.next() // { value: 5, done: false }
b.next() // { value: undefined, done: true }
复制代码
  • (4)其他场合
    由于数组的遍历会调用遍历器接口,所以任何接受数组作为参数的场合,其实都调用了遍历器接口。下面是一些例子。

  • for...of

  • Array.from()

  • Map(), Set(), WeakMap(), WeakSet()(比如new Map([['a',1],['b',2]])

  • Promise.all()

  • Promise.race()

1.13.5 for...of循环

只要数据结构部署了Symbol.iterator属性,即具有 iterator 接口,可以用for...of循环遍历它的成员。也就是说,for...of循环内部调用的是数据结构的Symbol.iterato方法。
使用场景
for...of可以使用在数组SetMap结构类数组对象Genetator对象字符串

  • 数组
    for...of循环可以代替数组实例的forEach方法。
let a = ['a', 'b', 'c'];
for (let k of a){console.log(k)}; // a b c

a.forEach((ele, index)=>{
    console.log(ele);    // a b c
    console.log(index);  // 0 1 2 
})
复制代码

for...in对比,for...in只能获取对象键名,不能直接获取键值,而for...of允许直接获取键值。

let a = ['a', 'b', 'c'];
for (let k of a){console.log(k)};  // a b c
for (let k in a){console.log(k)};  // 0 1 2
复制代码
  • Set和Map
    可以使用数组作为变量,如for (let [k,v] of b){...}
let a = new Set(['a', 'b', 'c']);
for (let k of a){console.log(k)}; // a b c

let b = new Map();
b.set('name','leo');
b.set('age', 18);
b.set('aaa','bbb');
for (let [k,v] of b){console.log(k + ":" + v)};
// name:leo
// age:18
// aaa:bbb
复制代码
  • 类数组对象
// 字符串
let a = 'hello';
for (let k of a ){console.log(k)}; // h e l l o

// DOM NodeList对象
let b = document.querySelectorAll('p');
for (let k of b ){
    k.classList.add('test');
}

// arguments对象
function f(){
    for (let k of arguments){
        console.log(k);
    }
}
f('a','b'); // a b
复制代码
  • 对象
    普通对象不能直接使用for...of会报错,要部署Iterator才能使用。
let a = {a:'aa',b:'bb',c:'cc'};
for (let k in a){console.log(k)}; // a b c
for (let k of a){console>log(k)}; // TypeError
复制代码

1.13.6 跳出for...of

使用break来实现。

for (let k of a){
    if(k>100)
        break;
    console.log(k);
}
复制代码

1.14 Generator函数和应用

1.14.1 基本概念

Generator函数是一种异步编程解决方案。
原理
执行Genenrator函数会返回一个遍历器对象,依次遍历Generator函数内部的每一个状态。
Generator函数是一个普通函数,有以下两个特征:

  • function关键字与函数名之间有个星号;
  • 函数体内使用yield表达式,定义不同状态;

通过调用next方法,将指针移向下一个状态,直到遇到下一个yield表达式(或return语句)为止。简单理解,Generator函数分段执行,yield表达式是暂停执行的标记,而next恢复执行。

function * f (){
    yield 'hi';
    yield 'leo';
    return 'ending';
}
let a = f();
a.next();  // {value: 'hi', done : false}
a.next();  // {value: 'leo', done : false}
a.next();  // {value: 'ending', done : true}
a.next();  // {value: undefined, done : false}
复制代码

1.14.2 yield表达式

yield表达式是暂停标志,遍历器对象的next方法的运行逻辑如下:

  1. 遇到yield就暂停执行,将这个yield后的表达式的值,作为返回对象的value属性值。
  2. 下次调用next往下执行,直到遇到下一个yield
  3. 直到函数结束或者return为止,并返回return语句后面表达式的值,作为返回对象的value属性值。
  4. 如果该函数没有return语句,则返回对象的valueundefined

注意:

  • yield只能用在Generator函数里使用,其他地方使用会报错。
// 错误1
(function(){
    yiled 1;  // SyntaxError: Unexpected number
})()

// 错误2  forEach参数是个普通函数
let a = [1, [[2, 3], 4], [5, 6]];
let f = function * (i){
    i.forEach(function(m){
        if(typeof m !== 'number'){
            yield * f (m);
        }else{
            yield m;
        }
    })
}
for (let k of f(a)){
    console.log(k)
}
复制代码
  • yield表达式如果用于另一个表达式之中,必须放在圆括号内。
function * a (){
    console.log('a' + yield);     //  SyntaxErro
    console.log('a' + yield 123); //  SyntaxErro
    console.log('a' + (yield));     //  ok
    console.log('a' + (yield 123)); //  ok
}
复制代码
  • yield表达式用做函数参数或放在表达式右边,可以不加括号
function * a (){
    f(yield 'a', yield 'b');    //  ok
    lei i = yield;              //  ok
}
复制代码

1.14.3 next方法

yield本身没有返回值,或者是总返回undefinednext方法可带一个参数,作为上一个yield表达式的返回值。

function * f (){
    for (let k = 0; true; k++){
        let a = yield k;
        if(a){k = -1};
    }
}
let g =f();
g.next();    // {value: 0, done: false}
g.next();    // {value: 1, done: false}
g.next(true);    // {value: 0, done: false}
复制代码

这一特点,可以让Generator函数开始执行之后,可以从外部向内部注入不同值,从而调整函数行为。

function * f(x){
    let y = 2 * (yield (x+1));
    let z = yield (y/3);
    return (x + y + z);
}
let a = f(5);
a.next();   // {value : 6 ,done : false}
a.next();   // {value : NaN ,done : false}  
a.next();   // {value : NaN ,done : true}
// NaN因为yeild返回的是对象 和数字计算会NaN

let b = f(5);
b.next();     // {value : 6 ,done : false}
b.next(12);   // {value : 8 ,done : false}
b.next(13);   // {value : 42 ,done : false}
// x 5 y 24 z 13
复制代码

1.14.4 for...of循环

for...of循环会自动遍历,不用调用next方法,需要注意的是,for...of遇到next返回值的done属性为true就会终止,return返回的不包括在for...of循环中。

function * f(){
    yield 1;
    yield 2;
    yield 3;
    yield 4;
    return 5;
}
for (let k of f()){
    console.log(k);
}
// 1 2 3 4  没有 5 
复制代码

1.14.5 Generator.prototype.throw()

throw方法用来向函数外抛出错误,并且在Generator函数体内捕获。

let f = function * (){
    try { yield }
    catch (e) { console.log('内部捕获', e) }
}

let a = f();
a.next();

try{
    a.throw('a');
    a.throw('b');
}catch(e){
    console.log('外部捕获',e);
}
// 内部捕获 a
// 外部捕获 b
复制代码

1.14.6 Generator.prototype.return()

return方法用来返回给定的值,并结束遍历Generator函数,如果return方法没有参数,则返回值的value属性为undefined

function * f(){
    yield 1;
    yield 2;
    yield 3;
}
let g = f();
g.next();          // {value : 1, done : false}
g.return('leo');   // {value : 'leo', done " true}
g.next();          // {value : undefined, done : true}
复制代码

1.14.7 next()/throw()/return()共同点

相同点就是都是用来恢复Generator函数的执行,并且使用不同语句替换yield表达式。

  • next()yield表达式替换成一个值。
let f = function * (x,y){
    let r = yield x + y;
    return r;
}
let g = f(1, 2); 
g.next();   // {value : 3, done : false}
g.next(1);  // {value : 1, done : true}
// 相当于把 let r = yield x + y;
// 替换成 let r = 1;
复制代码
  • throw()yield表达式替换成一个throw语句。
g.throw(new Error('报错'));  // Uncaught Error:报错
// 相当于将 let r = yield x + y
// 替换成 let r = throw(new Error('报错'));
复制代码
  • next()yield表达式替换成一个return语句。
g.return(2); // {value: 2, done: true}
// 相当于将 let r = yield x + y
// 替换成 let r = return 2;
复制代码

1.14.8 yield* 表达式

用于在一个Generator中执行另一个Generator函数,如果没有使用yield*会没有效果。

function * a(){
    yield 1;
    yield 2;
}
function * b(){
    yield 3;
    yield * a();
    yield 4;
}
// 等同于
function * b(){
    yield 3;
    yield 1;
    yield 2;
    yield 4;
}
for(let k of b()){console.log(k)}
// 3
// 1
// 2
// 4
复制代码

1.14.9 应用场景

  1. 控制流管理
    解决回调地狱:
// 使用前
f1(function(v1){
    f2(function(v2){
        f3(function(v3){
            // ... more and more
        })
    })
})

// 使用Promise 
Promise.resolve(f1)
    .then(f2)
    .then(f3)
    .then(function(v4){
        // ...
    },function (err){
        // ...
    }).done();

// 使用Generator
function * f (v1){
    try{
        let v2 = yield f1(v1);
        let v3 = yield f1(v2);
        let v4 = yield f1(v3);
        // ...
    }catch(err){
        // console.log(err)
    }
}
function g (task){
    let obj = task.next(task.value);
  // 如果Generator函数未结束,就继续调用
  if(!obj.done){
      task.value = obj.value;
      g(task);
  }
}
g( f(initValue) );
复制代码
  1. 异步编程的使用 在真实的异步任务封装的情况:
let fetch = require('node-fetch');
function * f(){
    let url = 'http://www.baidu.com';
    let res = yield fetch(url);
    console.log(res.bio);
}
// 执行该函数
let g = f();
let result = g.next();
// 由于fetch返回的是Promise对象,所以用then
result.value.then(function(data){
    return data.json();
}).then(function(data){
    g.next(data);
})
复制代码

1.15 Class语法和继承

1.15.1 介绍

ES6中的class可以看作只是一个语法糖,绝大部分功能都可以用ES5实现,并且,类和模块的内部,默认就是严格模式,所以不需要使用use strict指定运行模式

// ES5
function P (x,y){
    this.x = x;
    this.y = y;
}
P.prototype.toString = function () {
  return '(' + this.x + ', ' + this.y + ')';
};
var a = new P(1, 2);

// ES6
class P {
    constructor(x, y){
        this.x = x;
        this.y = y;
    }
    toString(){
        return '(' + this.x + ', ' + this.y + ')';
    }
}
let a = new P(1, 2);
复制代码

值得注意: ES6的的所有方法都是定义在prototype属性上,调用类的实例的方法,其实就是调用原型上的方法。

class P {
    constructor(){ ... }
    toString(){ ... }
    toNumber(){ ... }
}
// 等同于
P.prototyoe = {
    constructor(){ ... },
    toString(){ ... },
    toNumber(){ ... }
}

let a = new P();
a.constructor === P.prototype.constructor; // true
复制代码

类的属性名可以使用表达式

let name = 'leo';
class P {
    constructor (){ ... }
    [name](){ ... }
}
复制代码

Class不存在变量提升: ES6中的类不存在变量提升,与ES5完全不同:

new P ();   // ReferenceError
class P{...};
复制代码

Class的name属性
name属性总是返回紧跟在class后的类名。

class P {}
P.name;  // 'P'
复制代码

1.15.2 constructor()方法

constructor()是类的默认方法,通过new实例化时自动调用执行,一个类必须有constructor()方法,否则一个空的constructor()会默认添加。
constructor()方法默认返回实例对象(即this)。

class P { ... }
// 等同于
class P {
    constructor(){ ... }
}
复制代码

1.15.3 类的实例对象

与ES5一样,ES6的类必须使用new命令实例化,否则报错。

class P { ... }
let a = P (1,2);     // 报错
let b = new P(1, 2); // 正确
复制代码

与 ES5 一样,实例的属性除非显式定义在其本身(即定义在this对象上),否则都是定义在原型上(即定义在class上)。

class P {
    constructor(x, y){
        this.x = x;
        this.y = y;
    }
    toString(){
        return '(' + this.x + ', ' + this.y + ')';
    }
}
var point = new Point(2, 3);

point.toString() // (2, 3)

point.hasOwnProperty('x') // true
point.hasOwnProperty('y') // true
point.hasOwnProperty('toString') // false 
point.__proto__.hasOwnProperty('toString') // true
// toString是原型对象的属性(因为定义在Point类上)
复制代码

1.15.4 Class表达式

与函数一样,类也可以使用表达式来定义,使用表达式来作为类的名字,而class后跟的名字,用来指代当前类,只能再Class内部使用。

let a = class P{
    get(){
        return P.name;
    }
}

let b = new a();
b.get(); // P
P.name;  // ReferenceError: P is not defined
复制代码

如果类的内部没用到的话,可以省略P,也就是可以写成下面的形式。

let a = class { ... }
复制代码

1.15.5 私有方法和私有属性

由于ES6不提供,只能变通来实现:

  • 1.使用命名加以区别,如变量名前添加_,但是不保险,外面也可以调用到。
class P {
    // 公有方法
    f1 (x) {
        this._x(x);
    }
    // 私有方法
    _x (x){
        return this.y = x;
    }
}
复制代码
  • 2.将私有方法移除模块,再在类内部调用call方法。
class P {
    f1 (x){
        f2.call(this, x);
    }
}
function f2 (x){
    return this.y = x;
}
复制代码
  • 3.使用Symbol为私有方法命名。
const a1 = Symbol('a1');
const a2 = Symbol('a2');
export default class P{
    // 公有方法
    f1 (x){
        this[a1](x);
    }
    // 私有方法
    [a1](x){
        return this[a2] = x;
    }
}
复制代码

1.15.6 this指向问题

类内部方法的this默认指向类的实例,但单独使用该方法可能报错,因为this指向的问题。

class P{
    leoDo(thing = 'any'){
        this.print(`Leo do ${thing}`)
    }
    print(text){
        console.log(text);
    }
}
let a = new P();
let { leoDo } = a;
leoDo(); // TypeError: Cannot read property 'print' of undefined
// 问题出在 单独使用leoDo时,this指向调用的环境,
// 但是leoDo中的this是指向P类的实例,所以报错
复制代码

解决方法

  • 1.在类里面绑定this
class P {
    constructor(){
        this.name = this.name.bind(this);
    }
}
复制代码
  • 2.使用箭头函数
class P{
    constructor(){
        this.name = (name = 'leo' )=>{
            this.print(`my name is ${name}`)
        }
    }
}
复制代码

1.15.7 Class的getter和setter

使用getset关键词对属性设置取值函数和存值函数,拦截属性的存取行为。

class P {
    constructor (){ ... }
    get f (){
        return 'getter';
    }
    set f (val) {
        console.log('setter: ' + val);
    }
}

let a = new P();
a.f = 100;   // setter : 100
a.f;          // getter
复制代码

1.15.8 Class的generator方法

只要在方法之前加个(*)即可。

class P {
    constructor (...args){
        this.args = args;
    }
    *[Symbol.iterator](){
        for (let arg of this.args){
            yield arg;
        }
    }
}
for (let k of new P('aa', 'bb')){
    console.log(k);
}
// 'aa'
// 'bb'
复制代码

1.15.9 Class的静态方法

由于类相当于实例的原型,所有类中定义的方法都会被实例继承,若不想被继承,只要加上static关键字,只能通过类来调用,即“静态方法”。

class P (){
    static f1 (){ return 'aaa' };
}
P.f1();    // 'aa'
let a = new P();
a.f1();    // TypeError: a.f1 is not a function
复制代码

如果静态方法包含this关键字,则this指向类,而不是实例。

class P {
    static f1 (){
        this.f2();
    }
    static f2 (){
        console.log('aaa');
    }
    f2(){
        console.log('bbb');
    }
}
P.f2();  // 'aaa'
复制代码

并且静态方法可以被子类继承,或者super对象中调用。

class P{
    static f1(){ return 'leo' };
}
class Q extends P { ... };
Q.f1();  // 'leo'

class R extends P {
    static f2(){
        return super.f1() + ',too';
    }
}
R.f2();  // 'leo , too'
复制代码

1.15.10 Class的静态属性和实例属性

ES6中明确规定,Class内部只有静态方法没有静态属性,所以只能通过下面实现。

// 正确写法
class P {}
P.a1 = 1;
P.a1;      // 1

// 无效写法
class P {
    a1: 2,          // 无效
    static a1 : 2,  // 无效
}
P.a1;      // undefined
复制代码

新提案来规定实例属性和静态属性的新写法

  • 1.类的实例属性
    类的实例属性可以用等式,写入类的定义中。
class P {
    prop = 100;   // prop为P的实例属性 可直接读取
    constructor(){
        console.log(this.prop); // 100
    }
}
复制代码

有了新写法后,就可以不再contructor方法里定义。
为了可读性的目的,对于那些在constructor里面已经定义的实例属性,新写法允许直接列出

// 之前写法:
class RouctCounter extends React.Component {
    constructor(prop){
        super(prop);
        this.state = {
            count : 0
        }
    }
}

// 新写法
class RouctCounter extends React.Component {
    state;
    constructor(prop){
        super(prop);
        this.state = {
            count : 0
        }
    }
    
}
复制代码
  • 2.类的静态属性
    只要在实例属性前面加上static关键字就可以。
class P {
    static prop = 100;
    constructor(){console.log(this.prop)}; // 100
}
复制代码

新写法方便静态属性的表达。

// old 
class P  { .... }
P.a = 1;

// new 
class P {
    static a = 1;
}
复制代码

1.15.11 Class的继承

主要通过extends关键字实现,继承父类的所有属性和方法,通过super关键字来新建父类构造函数的this对象。

class P { ... }
class Q extends P { ... }

class P { 
    constructor(x, y){
        // ...
    }
    f1 (){ ... }
}
class Q extends P {
    constructor(a, b, c){
        super(x, y);  // 调用父类 constructor(x, y)
        this.color = color ;
    }
    f2 (){
        return this.color + ' ' + super.f1(); 
        // 调用父类的f1()方法
    }
}
复制代码

子类必须在constructor()调用super()否则报错,并且只有super方法才能调用父类实例,还有就是,父类的静态方法,子类也可以继承到

class P { 
    constructor(x, y){
        this.x = x;
        this.y = y;
    }
    static fun(){
        console.log('hello leo')
    }
}
// 关键点1 调用super
class Q extends P {
    constructor(){ ... }
}
let a = new Q(); // ReferenceError 因为Q没有调用super

// 关键点2 调用super
class R extends P {
    constructor (x, y. z){
        this.z = z; // ReferenceError 没调用super不能使用
        super(x, y);
        this.z = z; // 正确
    }
}

// 关键点3 子类继承父类静态方法
R.hello(); // 'hello leo'
复制代码

super关键字
既可以当函数使用,还可以当对象使用。

  • 1.当函数调用,代表父类的构造函数,但必须执行一次。
class P {... };
class R extends P {
    constructor(){
        super();
    }
}
复制代码
  • 2.当对象调用,指向原型对象,在静态方法中指向父类。
class P {
    f (){ return 2 };
}
class R extends P {
    constructor (){
        super();
        console.log(super.f()); // 2
    }
}
let a = new R()
复制代码

注意super指向父类原型对象,所以定义在父类实例的方法和属性,是无法通过super调用的,但是通过调用super方法可以把内部this指向当前实例,就可以访问到。

class P {
    constructor(){
        this.a = 1;
    }
    print(){
        console.log(this.a);
    }
}
class R extends P {
    get f (){
        return super.a;
    }
}
let b = new R();
b.a; // undefined 因为a是父类P实例的属性

// 先调用super就可以访问
class Q extends P {
    constructor(){
        super();   // 将内部this指向当前实例
        return super.a;
    }
}
let c = new Q();
c.a; // 1

// 情况3
class J extends P {
    constructor(){
        super();
        this.a = 3;
    }
    g(){
        super.print();
    }
}
let c = new J();
c.g(); // 3  由于执行了super()后 this指向当前实例
复制代码

1.16 Module语法和加载实现

1.16.1 介绍

ES6之前用于JavaScript的模块加载方案,是一些社区提供的,主要有CommonJSAMD两种,前者用于服务器,后者用于浏览器
ES6提供了模块的实现,使用export命令对外暴露接口,使用import命令输入其他模块暴露的接口。

// CommonJS模块
let { stat, exists, readFire } = require('fs');

// ES6模块
import { stat, exists, readFire } = from 'fs';
复制代码

1.16.2 严格模式

ES6模块自动采用严格模式,无论模块头部是否有"use strict"
严格模式有以下限制

  • 变量必须声明后再使用
  • 函数的参数不能有同名属性,否则报错
  • 不能使用with语句
  • 不能对只读属性赋值,否则报错
  • 不能使用前缀 0 表示八进制数,否则报错
  • 不能删除不可删除的属性,否则报错
  • 不能删除变量delete prop,会报错,只能删除属性delete * global[prop]
  • eval不会在它的外层作用域引入变量
  • evalarguments不能被重新赋值
  • arguments不会自动反映函数参数的变化
  • 不能使用arguments.callee
  • 不能使用arguments.caller
  • 禁止this指向全局对象
  • 不能使用fn.callerfn.arguments获取函数调用的堆栈
  • 增加了保留字(比如protectedstaticinterface

特别是,ES6中顶层this指向undefined,即不应该在顶层代码使用this

1.16.3 export命令

使用export向模块外暴露接口,可以是方法,也可以是变量。

// 1. 变量
export let a = 'leo';
export let b = 100;

// 还可以
let a = 'leo';
let b = 100;
export {a, b};

// 2. 方法
export function f(a,b){
    return a*b;
}

// 还可以
function f1 (){ ... }
function f2 (){ ... }
export {
    a1 as f1,
    a2 as f2
}
复制代码

可以使用as重命名函数的对外接口。
特别注意
export暴露的必须是接口,不能是值。

// 错误
export 1; // 报错

let a = 1;
export a; // 报错

// 正确
export let a = 1; // 正确

let a = 1;
export {a};       // 正确

let a = 1;
export { a as b}; // 正确
复制代码

暴露方法也是一样:

// 错误
function f(){...};
export f;

// 正确
export function f () {...};

function f(){...};
export {f};
复制代码

1.16.4 import命令

加载export暴露的接口,输出为变量。

import { a, b } from '/a.js';
function f(){
    return a + b;
}
复制代码

import后大括号指定变量名,需要与export的模块暴露的名称一致。
也可以使用as为输入的变量重命名。

import { a as leo } from './a.js';
复制代码

import不能直接修改输入变量的值,因为输入变量只读只是个接口,但是如果是个对象,可以修改它的属性。

// 错误
import {a} from './f.js';
a = {}; // 报错

// 正确
a.foo = 'leo';  // 不报错
复制代码

import命令具有提升效果,会提升到整个模块头部最先执行,且多次执行相同import只会执行一次。

1.16.5 模块的整体加载

当一个模块暴露多个方法和变量,引用时可以用*整体加载。

// a.js
export function f(){...}
export function g(){...}

// b.js
import * as obj from '/a.js';
console.log(obj.f());
console.log(obj.g());
复制代码

但是,不允许运行时改变:

import * as obj from '/a.js';
// 不允许
obj.a = 'leo';   
obj.b = function(){...}; 
复制代码

1.16.6 export default 命令

使用export default命令,为模块指定默认输出,引用的时候直接指定任意名称即可。

// a.js
export default function(){console.log('leo')};

// b.js
import leo from './a.js';
leo(); // 'leo'
复制代码

export defualt暴露有函数名的函数时,在调用时相当于匿名函数。

// a.js
export default function f(){console.log('leo')};
// 或者
function f(){console.log('leo')};
export default f;

// b.js
import leo from './a.js';
复制代码

export defualt其实是输出一个名字叫default的变量,所以后面不能跟变量赋值语句。

// 正确
export let a= 1;

let a = 1;
export defualt a;

// 错误
export default let a = 1;
复制代码

export default命令的本质是将后面的值,赋给default变量,所以可以直接将一个值写在export default之后。

// 正确
export default 1;
// 错误
export 1;
复制代码

1.16.7 export 和 import 复合写法

常常在先输入后输出同一个模块使用,即转发接口,将两者写在一起。

export {a, b} from './leo.js';

// 理解为
import {a, b} from './leo.js';
export {a, b}
复制代码

常见的写法还有:

// 接口改名
export { a as b} from './leo.js';

// 整体输出
export *  from './leo.js';

// 默认接口改名
export { default as a } from './leo.js';
复制代码

常常用在模块继承

1.16.8 浏览器中的加载规则

ES6中,可以在浏览器使用<script>标签,需要加入type="module"属性,并且这些都是异步加载,避免浏览器阻塞,即等到整个页面渲染完,再执行模块脚本,等同于打开了<script>标签的defer属性。

<script type="module" src="./a.js"></script>
复制代码

另外,ES6模块也可以内嵌到网页,语法与外部加载脚本一致:

<script type="module">
    import a from './a.js';
</script>
复制代码

注意点

  • 代码是在模块作用域之中运行,而不是在全局作用域运行。模块内部的顶层变量,外部不可见。
  • 模块脚本自动采用严格模式,不管有没有声明use strict
  • 模块之中,可以使用import命令加载其他模块(.js后缀不可省略,需要提供绝对 URL 或相对 URL),也可以使用export命令输出对外接口。
  • 模块之中,顶层的this关键字返回undefined,而不是指向window。也就是说,在模块顶层使用this关键字,是无意义的。
  • 同一个模块如果加载多次,将只执行一次。

2. ES7

2.1 Array.prototype.includes()方法

includes()用于查找一个值是否在数组中,如果在返回true,否则返回false

['a', 'b', 'c'].includes('a');     // true
['a', 'b', 'c'].includes('d');     // false
复制代码

includes()方法接收两个参数,搜索的内容开始搜索的索引,默认值为0,若搜索值在数组中则返回true否则返回false

['a', 'b', 'c', 'd'].includes('b');      // true
['a', 'b', 'c', 'd'].includes('b', 1);   // true
['a', 'b', 'c', 'd'].includes('b', 2);   // false
复制代码

indexOf方法对比,下面方法效果相同:

['a', 'b', 'c', 'd'].indexOf('b') > -1;       // true
['a', 'b', 'c', 'd'].includes('b'); // true 
复制代码

includes()与indexOf对比:

  • includes相比indexOf更具语义化,includes返回的是是否存在的具体结果,值为布尔值,而indexOf返回的是搜索值的下标。
  • includes相比indexOf更准确,includes认为两个NaN相等,而indexOf不会。
let a = [1, NaN, 3];
a.indexOf(NaN);     // -1
a.includes(NaN);    // true
复制代码

另外在判断+0-0时,includesindexOf的返回相同。

[1, +0, 3, 4].includes(-0);   // true
[1, +0, 3, 4].indexOf(-0);    // 1
复制代码

2.2 指数操作符(**)

基本用法:

let a = 3 ** 2 ; // 9
// 等效于
Math.pow(3, 2);  // 9
复制代码

**是一个运算符,也可以满足类似假发的操作,如下:

let a = 3;
a **= 2;    // 9
复制代码

3. ES8

3.1 async函数

3.1.1 介绍

ES8引入async函数,是为了使异步操作更加方便,其实它就是Generator函数的语法糖。
async函数使用起来,只要把Generator函数的(*)号换成asyncyield换成await即可。对比如下:

// Generator写法
const fs = require('fs');
const readFile = function (fileName) {
  return new Promise(function (resolve, reject) {
    fs.readFile(fileName, function(error, data) {
      if (error) return reject(error);
      resolve(data);
    });
  });
};
const gen = function* () {
  const f1 = yield readFile('/etc/fstab');
  const f2 = yield readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};

// async await写法
const asyncReadFile = async function () {
  const f1 = await readFile('/etc/fstab');
  const f2 = await readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};
复制代码

对比Genenrator有四个优点:

  • (1)内置执行器 Generator函数执行需要有执行器,而async函数自带执行器,即async函数与普通函数一模一样:
async f();
复制代码
  • (2)更好的语义 asyncawait,比起星号yield,语义更清楚了。async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。
  • (3)更广的适用性 yield命令后面只能是 Thunk 函数或 Promise 对象,而async函数的await命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)。
  • (4)返回值是Promise async函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。你可以用then方法指定下一步的操作。

进一步说,async函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而await命令就是内部then命令的语法糖。

3.1.2 基本用法

async函数返回一个Promise对象,可以使用then方法添加回调函数,函数执行时,遇到await就会先返回,等到异步操作完成,在继续执行。

async function f(item){
    let a = await g(item);
    let b = await h(item);
    return b;
}
f('hello').then(res => {
    console.log(res);
})
复制代码

async表明该函数内部有异步操作,调用该函数时,会立即返回一个Promise对象。
另外还有个定时的案例,指定时间后执行:

function f (ms){
    return new Promise(res => {
        setTimeout(res, ms);
    });
}
async function g(val, ms){
    await f(ms);
    console.log(val);
}
g('leo', 50);
复制代码

async函数还有很多使用形式:

// 函数声明
async function f (){...}

// 函数表达式
let f = async function (){...}

// 对象的方法
let a = {
    async f(){...}
}
a.f().then(...)

// Class的方法
class c {
    constructor(){...}
    async f(){...}
}

// 箭头函数
let f = async () => {...}
复制代码

3.1.3 返回Promise对象

async内部return返回的值会作为then方法的参数,另外只有async函数内部的异步操作执行完,才会执行then方法指定的回调函数。

async function f(){
    return 'leo';
}
f().then(res => { console.log (res) }); // 'leo'
复制代码

async内部抛出的错误,会被catch接收。

async function f(){
    throw new Error('err');
}
f().then (
    v => console.log(v),
    e => console.log(e)
)
// Error: err
复制代码

3.1.4 await命令

通常await后面是一个Promise对象,如果不是就返回对应的值。

async function f(){
    return await 10;
}
f().then(v => console.log(v)); // 10
复制代码

我们常常将async awaittry..catch一起使用,并且可以放多个await命令,也是防止异步操作失败因为中断后续异步操作的情况。

async function f(){
    try{
        await Promise.reject('err');
    }catch(err){ ... }
    return await Promise.resolve('leo');
}
f().then(v => console.log(v)); // 'leo'
复制代码

3.1.5 使用注意

  • (1)await命令放在try...catch代码块中,防止Promise返回rejected
  • (2)若多个await后面的异步操作不存在继发关系,最好让他们同时执行。
// 效率低
let a = await f();
let b = await g();

// 效率高
let [a, b] = await Promise.all([f(), g()]);
// 或者
let a = f();
let b = g();
let a1 = await a();
let b1 = await b();
复制代码
  • (3)await命令只能用在async函数之中,如果用在普通函数,就会报错。
// 错误
async function f(){
    let a = [{}, {}, {}];
    a.forEach(v =>{  // 报错,forEach是普通函数
        await post(v);
    });
}

// 正确
async function f(){
    let a = [{}, {}, {}];
    for(let k of a){
        await post(k);
    }
}
复制代码

3.2 Promise.prototype.finally()

finally()是ES8中Promise添加的一个新标准,用于指定不管Promise对象最后状态(是fulfilled还是rejected)如何,都会执行此操作,并且finally方法必须写在最后面,即在thencatch方法后面。

// 写法如下:  
promise
    .then(res => {...})
    .catch(err => {...})
    .finally(() => {...})
复制代码

finally方法常用在处理Promise请求后关闭服务器连接:

server.listen(port)
    .then(() => {..})
    .finally(server.stop);
复制代码

本质上,finally方法是then方法的特例:

promise.finally(() => {...});

// 等同于
promise.then(
    result => {
        // ...
        return result
    }, 
    error => {
        // ...
        throw error
    }
)
复制代码

3.3 Object.values(),Object.entries()

ES7中新增加的 Object.values()Object.entries()与之前的Object.keys()类似,返回数组类型。
回顾下Object.keys()

var a = { f1: 'hi', f2: 'leo'};
Object.keys(a); // ['f1', 'f2']
复制代码

3.3.1 Object.values()

返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历属性的键值。

let a = { f1: 'hi', f2: 'leo'};
Object.values(a); // ['hi', 'leo']
复制代码

如果参数不是对象,则返回空数组:

Object.values(10);   // []
Object.values(true); // []
复制代码

3.3.2 Object.entries()

返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历属性的键值对数组。

let a = { f1: 'hi', f2: 'leo'};
Object.entries(a); // [['f1','hi'], ['f2', 'leo']]
复制代码
  • 用途1
    遍历对象属性。
let a = { f1: 'hi', f2: 'leo'};
for (let [k, v] of Object.entries(a)){
    console.log(
        `${JSON.stringfy(k)}:${JSON.stringfy(v)}`
    )
}
// 'f1':'hi'
// 'f2':'leo'
复制代码
  • 用途2: 将对象转为真正的Map结构。
let a = { f1: 'hi', f2: 'leo'};
let map = new Map(Object.entries(a));
复制代码

手动实现Object.entries()方法:

// Generator函数实现:  
function* entries(obj){
    for (let k of Object.keys(obj)){
        yield [k ,obj[k]];
    }
}

// 非Generator函数实现:
function entries (obj){
    let arr = [];
    for(let k of Object.keys(obj)){
        arr.push([k, obj[k]]);
    }
    return arr;
}
复制代码

3.4 Object.getOwnPropertyDescriptors()

之前有Object.getOwnPropertyDescriptor方法会返回某个对象属性的描述对象,新增的Object.getOwnPropertyDescriptors()方法,返回指定对象所有自身属性(非继承属性)的描述对象,所有原对象的属性名都是该对象的属性名,对应的属性值就是该属性的描述对象

let a = {
    a1:1,
    get f1(){ return 100}
}
Object.getOwnPropetyDescriptors(a);
/*
{ 
    a:{ configurable:true, enumerable:true, value:1, writeable:true}
    f1:{ configurable:true, enumerable:true, get:f, set:undefined}
}
*/
复制代码

实现原理:

function getOwnPropertyDescriptors(obj) {
  const result = {};
  for (let key of Reflect.ownKeys(obj)) {
    result[key] = Object.getOwnPropertyDescriptor(obj, key);
  }
  return result;
}
复制代码

引入这个方法,主要是为了解决Object.assign()无法正确拷贝get属性和set属性的问题。

let a = {
    set f(v){
        console.log(v)
    }
}
let b = {};
Object.assign(b, a);
Object.a(b, 'f');
/*
f = {
    configurable: true,
    enumable: true,
    value: undefined,
    writeable: true
}
*/
复制代码

valueundefined是因为Object.assign方法不会拷贝其中的getset方法,使用getOwnPropertyDescriptors配合Object.defineProperties方法来实现正确的拷贝:

let a = {
    set f(v){
        console.log(v)
    }
}
let b = {};
Object.defineProperties(b, Object.getOwnPropertyDescriptors(a));
Object.getOwnPropertyDescriptor(b, 'f')
/*
    configurable: true,
    enumable: true,
    get: undefined,
    set: function(){...}
*/
复制代码

Object.getOwnPropertyDescriptors方法的配合Object.create方法,将对象属性克隆到一个新对象,实现浅拷贝。

const clone = Object.create(Object.getPrototypeOf(obj),
  Object.getOwnPropertyDescriptors(obj));

// 或者
const shallowClone = (obj) => Object.create(
  Object.getPrototypeOf(obj),
  Object.getOwnPropertyDescriptors(obj)
);
复制代码

3.5 字符串填充 padStart和padEnd

用来为字符串填充特定字符串,并且都有两个参数:字符串目标长度填充字段,第二个参数可选,默认空格。

'es8'.padStart(2);          // 'es8'
'es8'.padStart(5);          // '  es8'
'es8'.padStart(6, 'woof');  // 'wooes8'
'es8'.padStart(14, 'wow');  // 'wowwowwowwoes8'
'es8'.padStart(7, '0');     // '0000es8'

'es8'.padEnd(2);            // 'es8'
'es8'.padEnd(5);            // 'es8  '
'es8'.padEnd(6, 'woof');    // 'es8woo'
'es8'.padEnd(14, 'wow');    // 'es8wowwowwowwo'
'es8'.padEnd(7, '6');       // 'es86666'
复制代码

从上面结果来看,填充函数只有在字符长度小于目标长度时才有效,若字符长度已经等于或小于目标长度时,填充字符不会起作用,而且目标长度如果小于字符串本身长度时,字符串也不会做截断处理,只会原样输出。

3.6 函数参数列表与调用中的尾部逗号

该特性允许我们在定义或者调用函数时添加尾部逗号而不报错:

function es8(var1, var2, var3,) {
  // ...
}
es8(10, 20, 30,);
复制代码

3.7 共享内存与原子操作

当内存被共享时,多个线程可以并发读、写内存中相同的数据。原子操作可以确保那些被读、写的值都是可预期的,即新的事务是在旧的事务结束之后启动的,旧的事务在结束之前并不会被中断。这部分主要介绍了 ES8 中新的构造函数 SharedArrayBuffer 以及拥有许多静态方法的命名空间对象 Atomic
Atomic 对象类似于 Math 对象,拥有许多静态方法,所以我们不能把它当做构造函数。 Atomic 对象有如下常用的静态方法:

  • add /sub :为某个指定的value值在某个特定的位置增加或者减去某个值
  • and / or /xor :进行位操作
  • load :获取特定位置的值

4. ES9

4.1 对象的拓展运算符

4.1.1 介绍

对象的拓展运算符,即对象的Rest/Spread属性,可将对象解构赋值用于从一个对象取值,搜键值对分配到指定对象上,与数组的拓展运算符类似:

let  {x, y, ...z} = {x:1, y:2, a:3, b:4};
x;  // 1
y;  // 2
z;  // {a:3, b:4} 
复制代码

对象的解构赋值要求等号右边必须是个对象,所以如果等号右边是undefinednull就会报错无法转成对象。

let {a, ...b} = null;      // 运行时报错
let {a, ...b} = undefined; // 运行时报错
复制代码

解构赋值必须是最后一个参数,否则报错。

let {...a, b, c} = obj;     // 语法错误
let {a, ...b, c} = obj;     // 语法错误
复制代码

注意

  • 1.解构赋值是浅拷贝。
let a = {a1: {a2: 'leo'}};
let {...b} = a;
a.a1.a2 = 'leo';
b.a1.a2 = 'leo';
复制代码
  • 2.拓展运算符的解构赋值,不能复制继承自原型对象的属性。
let o1 = { a: 1 };
let o2 = { b: 2 };
o2.__proto__ = o1;
let { ...o3 } = o2;
o3;    // { b: 2 }
o3.a;  // undefined
复制代码

4.1.2 使用场景

  • 1.取出参数对象所有可遍历属性,拷贝到当前对象中。
let a = { a1:1, a2:2 };
let b = { ...a };
b;   // { a1:1, a2:2 }

// 类似Object.assign方法
复制代码
  • 2.合并两个对象。
let a = { a1:1, a2:2 };
let b = { b1:11, b2:22 };
let ab = { ...a, ...b }; // {a1: 1, a2: 2, b1: 11, b2: 22}
// 等同于
let ab = Object.assign({}, a, b);
复制代码
  • 3.将自定义属性放在拓展运算符后面,覆盖对象原有的同名属性。
let a = { a1:1, a2:2, a3:3 };
let r = { ...a, a3:666 };   
// r {a1: 1, a2: 2, a3: 666}

// 等同于
let r = { ...a, ...{ a3:666 }};
// r {a1: 1, a2: 2, a3: 666}

// 等同于
let r = Object.assign({}, a, { a3:666 });
// r {a1: 1, a2: 2, a3: 666}
复制代码
  • 4.将自定义属性放在拓展运算符前面,就会成为设置新对象的默认值。
let a = { a1:1, a2:2 };
let r = { a3:666, ...a };
// r {a3: 666, a1: 1, a2: 2}

// 等同于
let r = Object.assign({}, {a3:666}, a);
// r {a3: 666, a1: 1, a2: 2}

// 等同于
let r = Object.assign({a3:666}, a);
// r {a3: 666, a1: 1, a2: 2}
复制代码
  • 5.拓展运算符后面可以使用表达式。
let a = {
    ...(x>1? {a:!:{}),
    b:2
}
复制代码
  • 6.拓展运算符后面如果是个空对象,则没有任何效果。
{...{}, a:1};  // {a:1}
复制代码
  • 7.若参数是nullundefined则忽略且不报错。
let a = { ...null, ...undefined }; // 不报错
复制代码
  • 8.若有取值函数get则会执行。
// 不会打印 因为f属性只是定义 而不没执行
let a = {
    ...a1,
    get f(){console.log(1)}
}

// 会打印 因为f执行了
let a = {
    ...a1,
    ...{
        get f(){console.log(1)}
    }
}
复制代码

4.2 正则表达式 s 修饰符

在正则表达式中,点(.)可以表示任意单个字符,除了两个:用u修饰符解决四个字节的UTF-16字符,另一个是行终止符。
终止符即表示一行的结束,如下四个字符属于“行终止符”:

  • U+000A 换行符(\n)
  • U+000D 回车符(\r)
  • U+2028 行分隔符(line separator)
  • U+2029 段分隔符(paragraph separator)
/foo.bar/.test('foo\nbar')
// false
复制代码

上面代码中,因为.不匹配\n,所以正则表达式返回false
换个醒,可以匹配任意单个字符:

/foo[^]bar/.test('foo\nbar')
// true
复制代码

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

/foo.bar/s.test('foo\nbar') // true
复制代码

这被称为dotAll模式,即点(dot)代表一切字符。所以,正则表达式还引入了一个dotAll属性,返回一个布尔值,表示该正则表达式是否处在dotAll模式。

const re = /foo.bar/s;
// 另一种写法
// const re = new RegExp('foo.bar', 's');

re.test('foo\nbar') // true
re.dotAll // true
re.flags // 's'
复制代码

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

4.3 异步遍历器

在前面ES6章节中,介绍了Iterator接口,而ES6引入了“异步遍历器”,是为异步操作提供原生的遍历器接口,即valuedone这两个属性都是异步产生的。

4.3.1 异步遍历的接口

通过调用遍历器的next方法,返回一个Promise对象。

a.next().then( 
    ({value, done}) => {
        //...
    }
)
复制代码

上述a为异步遍历器,调用next后返回一个Promise对象,再调用then方法就可以指定Promise对象状态变为resolve后执行的回调函数,参数为valuedone两个属性的对象,与同步遍历器一致。
与同步遍历器一样,异步遍历器接口也是部署在Symbol.asyncIterator属性上,只要有这个属性,就都可以异步遍历。

let a = createAsyncIterable(['a', 'b']);
//createAsyncIterable方法用于构建一个iterator接口
let b = a[Symbol.asyncInterator]();

b.next().then( result1 => {
    console.log(result1); // {value: 'a', done:false}
    return b.next();
}).then( result2 => {
    console.log(result2); // {value: 'b', done:false}
    return b.next();
}).then( result3 => {
    console.log(result3); // {value: undefined, done:true}
})
复制代码

另外next方法返回的是一个Promise对象,所以可以放在await命令后。

async function f(){
    let a = createAsyncIterable(['a', 'b']);
    let b = a[Symbol.asyncInterator]();
    console.log(await b.next());// {value: 'a', done:false}
    console.log(await b.next());// {value: 'b', done:false}
    console.log(await b.next());// {value: undefined, done:true}
}
复制代码

还有一种情况,使用Promise.all方法,将所有的next按顺序连续调用:

let a = createAsyncIterable(['a', 'b']);
let b = a[Symbol.asyncInterator]();
let {{value:v1}, {value:v2}} = await Promise.all([
    b.next(), b.next()
])
复制代码

也可以一次调用所有next方法,再用await最后一步操作。

async function f(){
    let write = openFile('aaa.txt');
    write.next('hi');
    write.next('leo');
    await write.return();
}
f();
复制代码

4.3.2 for await...of

for...of用于遍历同步的Iterator接口,而ES8引入for await...of遍历异步的Iterator接口。

async function f(){
    for await(let a of createAsyncIterable(['a', 'b'])) {
        console.log(x);
    }
}
// a
// b
复制代码

上面代码,createAsyncIterable()返回一个拥有异步遍历器接口的对象,for...of自动调用这个对象的next方法,得到一个Promise对象,await用来处理这个Promise,一但resolve就把得到的值x传到for...of里面。
用途
直接把部署了asyncIteable操作的异步接口放入这个循环。

let a = '';
async function f(){
    for await (let b of req) {
        a += b;
    }
    let c = JSON.parse(a);
    console.log('leo', c);
}
复制代码

next返回的Promise对象被rejectfor await...of就会保错,用try...catch捕获。

async function f(){
    try{
        for await (let a of iterableObj()){
            console.log(a);
        }
    }catch(e){
        console.error(e);
    }
}
复制代码

注意,for await...of循环也可以用于同步遍历器。

(async function () {
  for await (let a of ['a', 'b']) {
    console.log(a);
  }
})();
// a
// b
复制代码

4.3.3 异步Generator函数

就像 Generator 函数返回一个同步遍历器对象一样,异步 Generator 函数的作用,是返回一个异步遍历器对象。
在语法上,异步 Generator 函数就是async函数与 Generator 函数的结合。

async function* f() {
  yield 'hi';
}
const a = f();
a.next().then(x => console.log(x));
// { value: 'hello', done: false }
复制代码

设计异步遍历器的目的之一,就是为了让Generator函数能用同一套接口处理同步和异步操作。

// 同步Generator函数
function * f(iterable, fun){
    let a = iterabl[Symbol.iterator]();
    while(true){
        let {val, done} = a.next();
        if(done) break;
        yield fun(val);
    }
}

// 异步Generator函数
async function * f(iterable, fun){
    let a = iterabl[Symbol.iterator]();
    while(true){
        let {val, done} = await a.next();
        if(done) break;
        yield fun(val);
    }
}
复制代码

同步和异步Generator函数相同点:在yield时用next方法停下,将后面表达式的值作为next()返回对象的value
在异步Generator函数中,同时使用awaityield,简单样理解,await命令用于将外部操作产生的值输入函数内部,yield命令用于将函数内部的值输出。

(async function () {
  for await (const line of readLines(filePath)) {
    console.log(line);
  }
})()
复制代码

异步 Generator 函数可以与for await...of循环结合起来使用。

async function* f(asyncIterable) {
  for await (const line of asyncIterable) {
    yield '> ' + line;
  }
}
复制代码

4.3.4 yield* 语句

yield*语句跟一个异步遍历器。

async function * f(){
  yield 'a';
  yield 'b';
  return 'leo';
}
async function * g(){
  const a = yield* f();  // a => 'leo'
}
复制代码

与同步 Generator 函数一样,for await...of循环会展开yield*

(async function () {
  for await (const x of gen2()) {
    console.log(x);
  }
})();
// a
// b
复制代码

5. 知识补充

5.1 块级作用域

通常指一个函数内部,或者一个代码块内部
比如:

function fun1 () {
    // 块级作用域
    if (true) {
        // 块级作用域
    }
}
复制代码

缺点: 1.导致内层变量覆盖外层变量

var a1 = new Date();
function f1 (){
    console.log(a1); // undefined
    if (false) {
        var a1 = 'hello'
    }
}
复制代码

输出 undefined 是因为 if 内的 a1 变量声明的变量提升,导致内部的 a1 覆盖外部的 a1,所以输出为 undefined

2.变量的全局污染

var a = 'hello';
for (var i = 0; i< a.length; i++) {
    //...
}
console.log(i); // 5
复制代码

循环结束后,变量 i 的值依然存在,造成变量的全局污染。

5.2 ES5/6对数组空位的处理

数组的空位不是undefined,而是没有任何值,数组的undefined也是有值。

0 in [undefined,undefined,undefined] // true
0 in [,,,] // false
复制代码

ES5对空位的处理

  • forEach(), filter(), reduce(), every()some()都会跳过空位。
  • map()会跳过空位,但会保留这个值。
  • join()toString()会将空位视为undefined,而undefinednull会被处理成空字符串。
[,'a'].forEach((x,i) => console.log(i)); // 1
['a',,'b'].filter(x => true);      // ['a','b']
[,'a'].every(x => x==='a');        // true
[1,,2].reduce((x,y) => x+y);       // 3
[,'a'].some(x => x !== 'a');       // false
[,'a'].map(x => 1);                // [,1]
[,'a',undefined,null].join('#');   // "#a##"
[,'a',undefined,null].toString();  // ",a,,"
复制代码

ES6对空位的处理
将空位视为正常值,转成undefined

Array.from(['a',,'b']);// [ "a", undefined, "b" ]
[...['a',,'b']];       // [ "a", undefined, "b" ]

//copyWithin() 会连空位一起拷贝。  
[,'a','b',,].copyWithin(2,0) // [,"a",,"a"]

//fill()会将空位视为正常的数组位置。
new Array(3).fill('a') // ["a","a","a"]

//for...of循环也会遍历空位。
let arr = [, ,];
for (let i of arr) {
  console.log(1);
}  // 1 1
复制代码

entries()keys()values()find()findIndex()会将空位处理成undefined

[...[,'a'].entries()] // [[0,undefined], [1,"a"]]

[...[,'a'].keys()] // [0,1]

[...[,'a'].values()] // [undefined,"a"]

[,'a'].find(x => true) // undefined

[,'a'].findIndex(x => true) // 0
复制代码

由于空位的处理规则非常不统一,所以建议避免出现空位。

三、结语

参考文章

推荐文章

关注下面的标签,发现更多相似文章
评论

查看更多 >