ES6/ES7/ES8/ES9/ES10新特性总结

新特性的原理是什么

大家估计都听说过babel,就是用于低版本的浏览器兼容javascript的新特性,实际是用将新特性转换成ES5,使得浏览器兼容。

  • 那么就有一个疑问:新语法仅仅就是封装了ES5的语法而已吗?那我用原生ES5的语法会不会比ES6、ES7新语法的性能会更高?

举个例子:includes、indexOf 和 for 遍历查找

function testIncludes(str, specialWord) {
	for (var specialWordItem = 0; specialWordItem < specialWord.length; specialWordItem++) {
		var specialIndex = str.includes(specialWord[specialWordItem]);
		if (specialIndex !== true) {
			return false;
		}
	}
	return true;
}
function testIndexOf(str, specialWord) {
	for (var specialWordItem = 0; specialWordItem < specialWord.length; specialWordItem++) {
		var specialIndex = str.indexOf(specialWord[specialWordItem]);
		if (specialIndex !== 1) {
			return false;
		}
	}
	return true;
}

function testFor(str, specialWord) {
	for (var specialWordItem = 0; specialWordItem < specialWord.length; specialWordItem++) {
		for (var j = 0; j < str.length; j++) {
			if (specialWord[specialWordItem] === str.charAt(j)) {
				return false;
			}
		}
	}
	return true;
}

function test() {
	var str = "abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij";
	var specialWord = new Array("''", "\\", "<", ">", "%", "?", "/", "+", "@", "&", "#", "¥", "……", "^", "~", "!", "‘", "’", "!", "¥");

	console.time('1');
	testIncludes(str, specialWord);
	console.timeEnd('1');

	console.time('2');
	testIndexOf(str, specialWord);
	console.timeEnd('2');

	console.time('3');
	testFor(str, specialWord);
	console.timeEnd('3');
}
test()

结果如下,显然includes和indexOf要比for遍历快得多。
在这里插入图片描述
因此有一个大致的结论就是,babel仅是用ES5语法模拟新语法,而新语法并非简单的使用ES5旧语法封装出来的东西,其实新语法TC39(技术委员会第 39 号)的专家肯定是考虑过的,毕竟每个新特性都会经历提案、草案、候选、完成四个步骤,既提高便利性也提高性能,能够使用新语法的环境下可以尽可能的使用新语法,性能会有所提高,特别在nodeJS开发中,都9102年了,不能老写旧语法


概述

ECMAScript版本发布时间新增特性
ECMAScript 2009(ES5)2009年11月扩展了Object、Array、Function的功能等
ECMAScript 2015(ES6)2015年6月类、模块化、箭头函数、函数参数默认值、模板字符串、解构赋值、延展操作符、对象属性简写、Promise、Let与Const等
ECMAScript 2016(ES7)2016年3月includes,指数操作符
ECMAScript 2017(ES8)2017年6月sync/await,Object.values(),Object.entries(),字符串填补,Object.getOwnPropertyDescriptors,容许函数参数尾后逗号,内存分享和原子操作等
ECMAScript 2018(ES9)2018年6月正则表达式 s 标志,正则表达式中捕获组的命名,对象属性的解构赋值,正则表达式向后断言,正则表达式 Unicode 属性转义字符,Promise.prototype.finally,异步迭代器
ECMAScript 2019(ES10)2019年6月string.prototype.matchAll(),动态导入,Array.flat(),Array.flatMap(),Object.fromEntries(),String.trimStart() 与 String.trimEnd(),格式良好的 JSON.stringify(),稳定的 Array.prototype.sort(),新的Function.toString(),可选的 Catch Binding,标准化 globalThis 对象,Symbol.description,Hashbang 语法,ES10类:private、static 和 公共成员

官方正式叫法应该是ES2015而不是ES6,并且每年发布一版新特性。

在这可以查看node的各版本能够使用ES几的功能 https://node.green/


ES6的特性

1、类(class)

ES6 引入了class(类),让JavaScript的面向对象编程变得更加简单和易于理解。

class Animal {
	// 构造函数,实例化的时候将会被调用,如果不指定,那么会有一个不带参数的默认构造函数.
	constructor(name, color) {
		this.name = name;
		this.color = color;
	}
	// toString是原型对象上的属性
	toString() {
		console.log('name:' + this.name + ',color:' + this.color);
	}
}

var animal = new Animal('dog', 'white'); // 实例化Animal
animal.toString(); // name:dog,color:white

console.log(animal.hasOwnProperty('name')); // true
console.log(animal.hasOwnProperty('toString')); // false
console.log(animal.__proto__.hasOwnProperty('toString')); // true

class Cat extends Animal {
	constructor(action) {
		// 子类必须要在constructor中指定super函数,否则在新建实例的时候会报错.
		// 如果没有置顶consructor,默认带super函数的constructor将会被添加
		super('cat', 'white');
		this.action = action;
	}
	toString() {
		console.log(super.toString());
	}
}

var cat = new Cat('catch')
cat.toString();

// 实例cat 是 Cat 和 Animal 的实例,和ES5完全一致。
console.log(cat instanceof Cat); // true
console.log(cat instanceof Animal); // true

类(class)实际是一种语法糖

JS 构造函数

function MathHandle(x,y){  //构造函数
  this.x = x;
  this.y = y;
}
MathHandle.prototype.add = function(){ //原型的拓展
  return this.x + this.y;
};

var m = new MathHandle(1,2);  //实例化构造函数
console.log(m.add)

Class 语法

class MathHandle {
    constructor(x, y) {  //构造器
         this.x = x;
         this.y = y;
    }
    add() {
        return this.x + this.y;
    }
}

const m = new MathHandle(1, 2)
console.log(m.add()) // 3

Class 静态(私有)方法、属性

class Foo {
	constructor(){
		console.log(this.a); // constructor的this执行实例属性及方法
	}
    static a = 1; // 静态属性
    a = 2; // 实例属性
	static bar() { 
		this.baz(); // 静态方法this指向类的方法,实例无法访问静态方法
	}
	static baz() { // 静态方法	
		console.log('hello');
	}
	baz() { // 实例方法
		console.log('world');
	}
	bac() { // 实例方法
		console.log('hhhh');
	}
}

class Boo extends Foo { // 可集成静态方法、属性
	constructor(){
		super() 
	}
}

Foo.a = 4; // 可修改静态属性
Foo.prototype.b = 3;  // 公共属性
Foo.bar() // hello 
let foo = new Foo(); // 2 
foo.baz() // world
console.log(Foo.a) // 4
console.log(foo.a) // 2
console.log(foo.b) // 3
Boo.bar() // hello 
foo.bar() // foo.bar is not a function
Foo.bac() // Foo.bac is not a function

总结:
在class里,普通方法、属性为实例的方法、属性;
带static的方法、属性为静态,仅类自己可访问;
constructor的this指向实例;
继承不仅能继承实例的方法、属性,同时也可以继承静态的;
实例方法、属性与类的静态方法、属性不能互相访问。

语法糖

class MathHandle{
  //.......
}
typeof MathHandle   //'function'
//class MathHandle的本质是function
function MathHandle(){} // Identifier 'MathHandle' has already been declared
MathHandle === MathHandle.prototype.constructor  //true
m.__proto__ === MathHandle.prototype  //true  实例的隐式原型等于构造函数的显示原型

继承 - JS

function Animal(name){
  this.name = name
  this.eat = function(){
    console.log('eat')
  }
}

function Dog(){
  this.bark = function(){
    console.log('bark')
  }
}

Dog.prototype = new Animal()  //绑定原型 实现继承
var hasiqi = new Dog()
hasiqi.bark()
hasiqi.eat()

继承 - Class

class Animal{
  constructor(name){
    this.name = name
  }
  eat(){
    console.log('eat')
  }
}

class Dog extends Animal{
  constructor(name){
    super(name)  //将name传到Animal的constructor构造器中去
    this.name = name
  }
  say(){
    console.log('say')
  }
}

const dog= new Dog('哈士奇')
dog.say() // eat
dog.eat() // say

Class在语法上更加贴近面向对象的写法
Class在实现继承更加易读、易理解
更易于写java等后端语言的使用
Class本质是语法糖,还是使用prototype的继承方式


2、模块化(Module)

ES5不支持原生的模块化,在ES6中模块作为重要的组成部分被添加进来。模块的功能主要由 exportimport组成。每一个模块都有自己单独的作用域,模块之间的相互调用关系是通过 export 来规定模块对外暴露的接口,通过import来引用其它模块提供的接口。同时还为模块创造了命名空间,防止函数的命名冲突。

导出(export)

ES6允许在一个模块中使用export来导出多个变量或函数。
导出变量

// test.js
export let name = 'vin'
export const age = '24';

// 等价于

let name  = 'vin';
const age = '24';
export { name, age }

---

export { name as myName, age } // 起别名

---

export default name  // 导出默认参数
// 等价于
export {name as default};

导出函数

// fn.js
export function fn(arg) {
  return arg;
}

// 等价于
export { fn } // 起别名

---

export default function fn(arg) {  // 导出默认函数
  return arg;
}
导入(import)

定义好模块的输出以后就可以在另外一个模块通过import引用。

import { fn } from 'fn'; // fn.js 不需要加.js
import { name, age } from 'test'; // test.js

---

import fn from 'fn'; // 导入默认函数
---
import { fn as fn1 } from 'fn'; // 别名

3、箭头函数(Arrow)

=>不只是关键字function的简写,它还带来了其它好处。箭头函数与包围它的代码共享同一个this,即取上下文的this作为自己的this,能帮你很好的解决this的指向问题。有经验的JavaScript开发者都熟悉诸如let self = thislet that = this这种引用外围this的模式。但借助=>,就不需要这种模式了。

箭头函数的结构
箭头函数的箭头=>左边是一个空括号、单个的参数名、或用括号括起的多个参数名,而箭头右边可以是一个表达式(作为函数的返回值),或者是用花括号括起的函数体(需要自行通过return来返回值,否则返回的是undefined)。

function fn1(){
	console.log(this);
}
const fn2 = () => {
	console.log(this);
}
const obj = { fn1, fn2 };
obj.fn1(); // { fn1, fn2 }
obj.fn2(); // window

4、函数参数默认值

ES6支持在定义函数的时候为其设置默认值:

function foo(height = 50, color = 'red')
{
    // ...
}

不使用默认值:

function foo(height, color)
{
    height = height || 50;
    color = color || 'red';
    //...
}

这样写一般没问题,但当参数的布尔值为false时,就会有问题了。比如,我们这样调用foo函数:

foo(0, "")

复制代码因为0的布尔值为false,这样height的取值将是50。同理color的取值为‘red’。
所以说,函数参数默认值不仅能是代码变得更加简洁而且能规避一些问题。


5、模板字符串

ES6支持模板字符串,使得字符串的拼接更加的简洁、直观。

不使用模板字符串:

let name = 'Your name is ' + first + ' ' + last + '.'

使用模板字符串:

let name = `Your name is ${first} ${last}.`

复制代码在ES6中通过 ${} 就可以完成字符串的拼接,只需要将变量放在大括号之中。


6、解构赋值

解构赋值语法是JavaScript的一种表达式,可以方便的从数组或者对象中快速提取值赋给定义的变量。
获取数组中的值
从数组中获取值并赋值到变量中,变量的顺序与数组中对象顺序对应。

let foo = ["one", "two", "three", "four"];

let [one, two, three] = foo;
console.log(one); // "one"
console.log(two); // "two"
console.log(three); // "three"

// 如果你要忽略某些值,你可以按照下面的写法获取你想要的值
var [first, , , last] = foo;
console.log(first); // "one"
console.log(last); // "four"

//你也可以这样写
let a, b; //先声明变量

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

复制代码如果没有从数组中的获取到值,你可以为变量设置一个默认值。

var a, b;

[a=5, b=7] = [1];
console.log(a); // 1
console.log(b); // 7

复制代码通过解构赋值可以方便的交换两个变量的值。

var a = 1;
var b = 3;

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

复制代码获取对象中的值

const student = {
  name: 'Ming',
  age : '18',
  city: 'Shanghai',
  hello: { world: '!'}
};

const { name, age, city, hello: { world } } = student;
console.log(name); // "Ming"
console.log(age); // "18"
console.log(city); // "Shanghai"
console.log(hello); // "hello is not defined"
console.log(world ); // "!"

7、延展操作符(Spread operator)

延展操作符...可以在函数调用/数组构造时, 将数组表达式或者string在语法层面展开;
还可以在构造对象时, 将对象表达式按key-value的方式展开。
语法

函数调用:

function show(a, b, ...args) { 
    console.log(a)
    console.log(b)
    console.log(args)
}
show(2, 3, 5, 6, 88, 44) 
// 2
// 3
// [ 5, 6, 88, 44 ]
// 可以理解为函数参数中使用延展操作符是数组延展的反操作

数组构造或字符串:

[ '4', ...'hello', 6 ]; // [ '4', 'h', 'e', 'l', 'l', 'o', '6']

构造对象时,进行克隆或者属性拷贝(ECMAScript 2018规范新增特性):

let obj = {
	a:'1',
		b:'2',
		c:{
			d:'3'
		}
	}
let obj1 = { ...obj }; // 浅拷贝
obj.c.d = '4'
console.log(obj1)
/* 
{
	a:'1',
		b:'2',
		c:{
			d:'4'
		}
	}
*/

应用场景

在函数调用时使用延展操作符

function sum(x, y, z) {
  return x + y + z;
}
const numbers = [ 1, 2, 3 ];

//不使用延展操作符
console.log(sum.apply(null, numbers));

//使用延展操作符
console.log(sum(...numbers)); // 6

构造数组

没有展开语法的时候,只能组合使用 push,splice,concat 等方法,来将已有数组元素变成新数组的一部分。有了展开语法, 构造新数组会变得更简单、更优雅:

const stuendts = [ 'Jine', 'Tom' ]; 
const persons = [ 'Tony', ...stuendts, 'Aaron', 'Anna' ];
conslog.log(persions)// ["Tony", "Jine", "Tom", "Aaron", "Anna"]

和参数列表的展开类似, ... 在构造字数组时, 可以在任意位置多次使用。

数组拷贝

let arr = [ 1, 2, 3 ];
let arr2 = [ ...arr ]; // 等同于 arr.slice()
arr2.push(4); 
console.log(arr); // [ 1, 2, 3 ]
console.log(arr2); // [ 1, 2, 3, 4 ]

连接多个数组

let arr1 = [ 0, 1, 2 ];
let arr2 = [ 3, 4, 5 ];
let arr3 = [ ...arr1, ...arr2 ]; // 将 arr2 中所有元素附加到 arr1 后面并返回
//等同于
let arr4 = arr1.concat(arr2);

在ECMAScript 2018中延展操作符增加了对对象的支持
展开语法和 Object.assign() 行为一致, 执行的都是浅拷贝(只遍历一层)。

let obj1 = { foo: 'bar', x: 42 };
let obj2 = { foo: 'baz', y: 13 };

var clonedObj = { ...obj1 };
// 克隆后的对象: { foo: "bar", x: 42 }

var mergedObj = { ...obj1, ...obj2 };
// 合并后的对象: { foo: "baz", x: 42, y: 13 }

8.对象属性简写

在ES6中允许我们在设置一个对象的属性的时候不指定属性名。

不使用ES6

const name = 'Ming', age = '18', city = 'Shanghai';
        
const student = {
    name : name,
    age  : age,
    city : city
};
console.log(student); // { name: "Ming", age: "18", city: "Shanghai" }

复制代码对象中必须包含属性和值,显得非常冗余。

使用ES6

const name = 'Ming',age = '18',city = 'Shanghai';
        
const student = { name, age, city };
console.log(student); // {name: "Ming", age: "18", city: "Shanghai"}

复制代码对象中直接写变量,非常简洁。


9.Promise

Promise是异步编程的一种解决方案,比传统的解决方案callback更加的优雅。它最早由社区提出和实现的,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象

不使用ES6

嵌套两个setTimeout回调函数:

setTimeout(function() {
    console.log('Hello'); // 1秒后输出"Hello"
    setTimeout(function() {
        console.log('Hi'); // 2秒后输出"Hi"
    }, 1000);
}, 1000);

使用ES6

let waitSecond = new Promise(function(resolve, reject) {
    setTimeout(resolve, 1000);
});

waitSecond
    .then(function() {
        console.log("Hello"); // 1秒后输出"Hello"
        return waitSecond;
    })
    .then(function() {
        console.log("Hi"); // 2秒后输出"Hi"
    });

上面的的代码使用两个then来进行异步编程串行化,避免了回调地狱


10.支持let与const

在之前JS是没有块级作用域的(var是函数作用域),
constlet填补了这方便的空白,constlet都是块级作用域。

使用var定义的变量为函数级作用域:

{
  var a = 10;
}

console.log(a); // 输出10

使用letconst定义的变量为块级作用域:

{
  let a = 10;
}

console.log(a); //-1 or Error“ReferenceError: a is not defined”

ES7的特性

1、Array.prototype.includes()

includes() 函数用来判断一个数组是否包含一个指定的值,如果包含则返回true,否则返回false
includes 函数与 indexOf 函数很相似

arr.includes(x)
// 等同于
arr.indexOf(x) >= 0

接下来我们来判断数字中是否包含某个元素:

在ES7之前,使用indexOf()验证数组中是否存在某个元素,这时需要根据返回值是否为-1来判断:

let arr = ['react', 'angular', 'vue'];

if (arr.indexOf('react') !== -1) {
    console.log('react存在');
}

在ES7,使用includes()验证数组中是否存在某个元素,这样更加直观简单:

let arr = ['react', 'angular', 'vue'];

if (arr.includes('react')) {
    console.log('react存在');
}

2、指数操作符

在ES7中引入了指数运算符****具有与Math.pow(..)等效的计算结果。

不使用指数操作符

使用自定义的递归函数calculateExponent或者Math.pow()进行指数运算:

function calculateExponent(base, exponent) {
    if (exponent === 1) {
        return base;
    } else {
        return base * calculateExponent(base, exponent - 1);
    }
}

console.log(calculateExponent(2, 10)); // 输出1024
console.log(Math.pow(2, 10)); // 输出1024

使用指数操作符

使用指数运算符**,就像+-等操作符一样:

console.log(2 ** 10); // 输出1024

ES8的特性

1、async/await

在ES8中加入了对async/await的支持,也就我们所说的异步函数,这是一个很实用的功能。
async/await将我们从头痛的回调地狱中解脱出来了,使整个代码看起来很简洁。

使用async/await与不使用async/await的差别:

login(userName) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve('1001');
        }, 600);
    });
}

getData(userId) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (userId === '1001') {
                resolve('Success');
            } else {
                reject('Fail');
            }
        }, 600);
    });
}

// 不使用async/await ES7
doLogin(userName) {
    this.login(userName)
        .then(this.getData)
        .then(result => {
            console.log(result)
        })
}

// 使用async/await ES8
async doLogin2(userName) {
    const userId = await this.login(userName);
    const result = await this.getData(userId);
}

this.doLogin() // Success
this.doLogin2() // Success
async/await的几种应用场景

接下来我们来看一下async/await的几种应用场景。

获取异步函数的返回值

异步函数本身会返回一个Promise,所以我们可以通过then来获取异步函数的返回值。

async function charCountAdd(data1, data2) {
    const d1 = await charCount(data1);
    const d2 = await charCount(data2);
    return d1 + d2;
}

charCountAdd('Hello','Hi').then(console.log); //通过then获取异步函数的返回值。

function charCount(data) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(data.length);
        }, 1000);
    });
}
async/await在并发场景中的应用

对于上述的例子,我们调用await两次,每次都是等待1秒一共是2秒,效率比较低,而且两次await的调用并没有依赖关系,那能不能让其并发执行呢,答案是可以的,接下来我们通过Promise.all来实现await的并发调用。

// 将不影响前后执行的await放在promise.all执行,提高接口性能
async function charCountAdd(data1, data2) {
    const [ d1, d2 ] = await Promise.all([ charCount(data1), charCount(data2) ]);
    return d1 +  d2;
}

charCountAdd('Hello','Hi').then(console.log);

function charCount(data) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(data.length);
        }, 1000);
    });
}

通过上述代码我们实现了两次charCount的并发调用,Promise.all接受的是一个数组,它可以将数组中的promise对象并发执行

async/await的几种错误处理方式

第一种:捕捉整个async/await函数的错误

async function charCountAdd(data1, data2) {
    const d1 = await charCount(data1);
    const d2 = await charCount(data2);
    return d1 + d2;
}
charCountAdd('Hello', 'Hi')
    .then(console.log)
    .catch(console.log); // 捕捉整个async/await函数的错误
...

这种方式可以捕捉整个charCountAdd运行过程中出现的错误,错误可能是由charCountAdd本身产生的,也可能是由对data1的计算中或data2的计算中产生的。

第二种:捕捉单个的await表达式的错误

async function charCountAdd(data1, data2) {
    const d1 = await charCount(data1)
        .catch(e => console.log('d1 is null'));
    const d2 = await charCount(data2)
        .catch(e => console.log('d2 is null'));
    return d1 + d2;
}
charCountAdd('Hello','Hi').then(console.log);

通过这种方式可以捕捉每一个await表达式的错误,如果既要捕捉每一个await表达式的错误,又要捕捉整个charCountAdd函数的错误,可以在调用charCountAdd的时候加个catch

...
charCountAdd('Hello','Hi')
    .then(console.log)
    .catch(console.log); // 捕捉整个async/await函数的错误
...

第三种:同时捕捉多个的await表达式的错误

async function charCountAdd(data1, data2) {
    let d1, d2;
    try {
        d1 = await charCount(data1);
        d2 = await charCount(data2);
    } catch (e) {
        console.log('d1 is null');
    }
    return d1 + d2;
}
charCountAdd('Hello','Hi')
    .then(console.log);

function charCount(data) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(data.length);
        }, 1000);
    });
}

2、Object.values()

Object.values()是一个与Object.keys()类似的新函数,但返回的是Object自身属性的所有值,不包括继承的值。
假设我们要遍历如下对象obj的所有值:

const obj = { a: 1, b: 2, c: 3 };

不使用Object.values() :ES7

const vals = Object.keys(obj).map(key => obj[key]);
console.log(vals); // [ 1, 2, 3 ]

使用Object.values() :ES8

const values = Object.values(obj1);
console.log(values); // [ 1, 2, 3 ]

从上述代码中可以看出Object.values()为我们省去了遍历key,并根据这些key获取value的步骤。

3.Object.entries

Object.entries()函数返回一个给定对象自身可枚举属性的键值对的数组。
接下来我们来遍历上文中的obj对象的所有属性的keyvalue

不使用Object.entries():ES7

Object.keys(obj).forEach(key=>{
	console.log('key:' + key + ' value:' + obj[key]);
})
//key:a value:1
//key:b value:2
//key:c value:3

使用Object.entries() :ES8

console.log(Object.entries(obj1))  // [ [a, 1], [b, 2], [c, 3] ]

for(const [key,value] of Object.entries(obj1)){
	console.log(`key:${key} value:${value}`)
}
//key:a value:1
//key:b value:2
//key:c value:3

4、String padding

在ES8中String新增了两个实例函数String.prototype.padStartString.prototype.padEnd,允许将空字符串或其他字符串添加到原始字符串的开头或结尾。

String.padStart(targetLength, [padString])

targetLength:当前字符串需要填充到的目标长度。如果这个数值小于当前字符串的长度,则返回当前字符串本身。
padString:(可选)填充字符串。如果字符串太长,使填充后的字符串长度超过了目标长度,则只保留最左侧的部分,其他部分会被截断,此参数的缺省值为 " "。

console.log('0.0'.padStart(4,'10')) // 10.0
console.log('0.0'.padStart(20)) //                0.00    
String.padEnd(targetLength, [padString])

targetLength:当前字符串需要填充到的目标长度。如果这个数值小于当前字符串的长度,则返回当前字符串本身。
padString:(可选) 填充字符串。如果字符串太长,使填充后的字符串长度超过了目标长度,则只保留最左侧的部分,其他部分会被截断,此参数的缺省值为 " ";

console.log('0.0'.padEnd(4,'0'))  // 0.00    
console.log('0.0'.padEnd(10,'0')) // 0.00000000

5、函数参数列表结尾允许逗号

这是一个不痛不痒的更新,主要作用是方便使用git进行多人协作开发时修改同一个函数减少不必要的行变更。

不使用ES8

// 程序员A
const f = function(a,
  b
   ) { 
  ...
  }

// 程序员B
const f = function(a,
  b,   // 变更行
  c   // 变更行
   ) { 
  ...
  }

// 程序员C
const f = function(a,
  b,
  c,   // 变更行
  d   // 变更行
   ) { 
  ...
  }

使用ES8

// 程序员A
const f = function(a,
  b,
   ) { 
  ...
  }

// 程序员B
const f = function(a,
  b,
  c,   // 变更行
   ) { 
  ...
  }

// 程序员C
const f = function(a,
  b,
  c,
  d,   // 变更行
   ) { 
  ...
  }

6、Object.getOwnPropertyDescriptors()

Object.getOwnPropertyDescriptors()函数用来获取一个对象的所有自身属性的描述符,如果没有任何自身属性,则返回空对象。

函数原型:

Object.getOwnPropertyDescriptors(obj)

返回obj对象的所有自身属性的描述符,如果没有任何自身属性,则返回空对象。

const obj2 = {
	name: 'Jine',
	get age() { return '18' }
};

Object.getOwnPropertyDescriptors(obj2)
// {
//   age: {
//     configurable : true,
//     enumerable   : true,
//     get          : function age(){}, // the getter function
//     set          : undefined
//   },
//   name: {
//     configurable : true,
//     enumerable   : true,
//	   value        : "Jine",
//	   writable     : true
//   }
// }

ES9

1、异步迭代

async/await的某些时刻,你可能尝试在同步循环中调用异步函数。例如:

async function process(array) {
  for (const i of array) {
    await doSomething(i);
  }
}

这段代码不会正常运行,下面这段同样也不会:

async function process(array) {
  array.forEach(async i => {
    await doSomething(i);
  });
}

这段代码中,循环本身依旧保持同步,并在在内部异步函数之前全部调用完成。
ES2018引入异步迭代器(asynchronous iterators),这就像常规迭代器,除了next()方法返回一个Promise。因此await可以和for...of循环一起使用,以串行的方式运行异步操作。例如:

async function process(array) {
  for await (let i of array) {
    doSomething(i);
  }
}

2、Promise.finally()

一个Promise调用链要么成功到达最后一个.then(),要么失败触发.catch()。在某些情况下,你想要在无论Promise运行成功还是失败,运行相同的代码,例如清除,删除对话,关闭数据库连接等。
.finally()允许你指定最终的逻辑:

function doSomething() {
  doSomething1()
    .then(doSomething2)
    .then(doSomething3)
    .catch(err => {
       console.log(err);
     })
    .finally(() => {
      // finish here!
    });
}

3、Rest / Spread 属性

ES2015引入了Rest参数扩展运算符。三个点...仅用于数组。Rest参数语法允许我们将一个布丁数量的参数表示为一个数组。

restParam(1, 2, 3, 4, 5);

function restParam(p1, p2, ...p3) {
  // p1 = 1
  // p2 = 2
  // p3 = [3, 4, 5]
}

展开操作符以相反的方式工作,将数组转换成可传递给函数的单独参数。例如Math.max()返回给定数字中的最大值:

const values = [ 99, 100, -1, 48, 16 ];
console.log( Math.max(...values) ); // 100

ES2018为对象解构提供了和数组一样的Rest参数展开操作符,一个简单的例子:

const myObject = {
  a: 1,
  b: 2,
  c: 3
};

const { a, ...x } = myObject;
// a = 1
// x = { b: 2, c: 3 }

或者你可以使用它给函数传递参数:

restParam({
  a: 1,
  b: 2,
  c: 3
});

function restParam({ a, ...x }) {
  // a = 1
  // x = { b: 2, c: 3 }
}

跟数组一样,Rest参数只能在声明的结尾处使用。此外,它只适用于每个对象的顶层,如果对象中嵌套对象则无法适用。
扩展运算符可以在其他对象内使用,例如:

const obj1 = { a: 1, b: 2, c: 3 };
const obj2 = { ...obj1, z: 26 };
// obj2 is { a: 1, b: 2, c: 3, z: 26 }

可以使用扩展运算符拷贝一个对象,像是这样obj2 = { …obj1 },但是 这只是一个对象的浅拷贝。另外,如果一个对象A的属性是对象B,那么在克隆后的对象cloneB中,该属性指向对象B。

4、正则表达式命名捕获组(Regular Expression Named Capture Groups)

JavaScript正则表达式可以返回一个匹配的对象——一个包含匹配字符串的类数组,例如:以YYYY-MM-DD的格式解析日期:

const
  reDate = /([0-9]{4})-([0-9]{2})-([0-9]{2})/,
  match  = reDate.exec('2018-04-30'),
  year   = match[1], // 2018
  month  = match[2], // 04
  day    = match[3]; // 30

这样的代码很难读懂,并且改变正则表达式的结构有可能改变匹配对象的索引。
ES2018允许命名捕获组使用符号?<name>,在打开捕获括号后立即命名,示例如下:

const
  reDate = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/,
  match  = reDate.exec('2018-04-30'),
  year   = match.groups.year,  // 2018
  month  = match.groups.month, // 04
  day    = match.groups.day;   // 30

任何匹配失败的命名组都将返回undefined
命名捕获也可以使用在replace()方法中。例如将日期转换为美国的 MM-DD-YYYY 格式:

const
  reDate = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/,
  d      = '2018-04-30',
  usDate = d.replace(reDate, '$<month>-$<day>-$<year>');

5、正则表达式反向断言(lookbehind)

目前JavaScript在正则表达式中支持先行断言(lookahead)。这意味着匹配会发生,但不会有任何捕获,并且断言没有包含在整个匹配字段中。例如从价格中捕获货币符号:

const
  reLookahead = /\D(?=\d+)/,
  match       = reLookahead.exec('$123.89');

console.log( match[0] ); // $

ES2018引入以相同方式工作但是匹配前面的反向断言(lookbehind),这样我就可以忽略货币符号,单纯的捕获价格的数字:

const
  reLookbehind = /(?<=\D)\d+/,
  match        = reLookbehind.exec('$123.89');

console.log( match[0] ); // 123.89

以上是 肯定反向断言,非数字\D必须存在。同样的,还存在 否定反向断言,表示一个值必须不存在,例如:

const
  reLookbehindNeg = /(?<!\D)\d+/,
  match           = reLookbehind.exec('$123.89');

console.log( match[0] ); // null

6、正则表达式dotAll模式

正则表达式中点.匹配除回车外的任何单字符,标记s改变这种行为,允许行终止符的出现,例如:

/hello.world/.test('hello\nworld');  // false
/hello.world/s.test('hello\nworld'); // true

7、正则表达式 Unicode 转义

到目前为止,在正则表达式中本地访问Unicode 字符属性是不被允许的。ES2018添加了Unicode 属性转义——形式为\p{...}\P{...},在正则表达式中使用标记u (unicode) 设置,在\p块儿内,可以以键值对的方式设置需要匹配的属性而非具体内容。例如:

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

此特性可以避免使用特定 Unicode 区间来进行内容类型判断,提升可读性和可维护性。

8、非转义序列的模板字符串

最后,ES2018 移除对 ECMAScript 在带标签的模版字符串中转义序列的语法限制。
之前,\u开始一个 unicode 转义,\x开始一个十六进制转义,\后跟一个数字开始一个八进制转义。这使得创建特定的字符串变得不可能,例如Windows文件路径 C:\uuu\xxx\111。更多细节参考模板字符串。


ES10

1、BigInt 任意精度整数

BigInt 是第七种 原始类型

BigInt 是一个任意精度的整数。这意味着变量现在可以 表示²⁵³ 数字,而不仅仅是9007199254740992

const b = 1n;  // 追加 n 以创建 BigInt

在过去,不支持大于9007199254740992 的整数值。如果超过,该值将锁定为


const limit = Number.MAX_SAFE_INTEGER; // 9007199254740991
limit + 1; // 9007199254740992
limit + 2; // 9007199254740992 

const larger = 9007199254740991n; // 9007199254740991n
larger + 1; // 9007199254740992n
larger + 2; // 9007199254740993n 

const integer = BigInt(9007199254740991); // 9007199254740991n
// 等价于
const same = BigInt("9007199254740991"); //  9007199254740991n

typeof

typeof 10; // 'number'
typeof 10n; // 'bigint'

等于运算符可用于两种类型之间比较:

10n === BigInt(10); // true
10n == 10; // true

数学运算符只能在自己的类型中工作:

200n / 10n  // 20n
200n / 20
// Uncaught TypeError: Cannot mix BigInt and other types, use explicit conversions <

-运算符可以操作, + 不可用

-100n // -100n
+100n // Uncaught TypeError: Cannot convert a BigInt value to a number

2、string.prototype.matchAll()

如果您运行谷歌搜索JavaScript string match all,第一个结果将是这样的:如何编写正则表达式“match all”?

最佳结果将建议 String.match 与正则表达式和/g 一起使用或者带有/gRegExp.exec 或者带有/gRegExp.test

首先,让我们看看旧规范是如何工作的。

带字符串参数的 String.match 仅返回第一个匹配:

let string = 'Hello';
let matches = string.match('l');
console.log(matches[0]); // "l"

结果是单个"l"(注意:匹配存储在 matches[0] 中而不是 matches)

“hello”中搜索 "l" 只返回 "l"

string.matchregexp 参数一起使用也是如此:

让我们使用正则表达式 /l/ 找到字符 串“hello” 中的 “l” 字符:

let string = "Hello";
let matches = string.match(/l/);
console.log(matches[0]); // "l"
/**
matches: {
	0: "l"
	groups: undefined
	index: 2
	input: "Hello"
	length: 1
}
*/

添加 /g 混合

let string = "Hello";
let ret = string.match(/l/g); // (2) [“l”, “l”];

很好,我们使用ES10方式得到了多个匹配,它一直起作用。

那么为什么要使用全新的 matchAll 方法呢? 在我们更详细地回答这个问题之前,让我们先来看看 捕获组。如果不出意外,你可能会学到一些关于正则表达式的新知识。

正则表达式捕获组
regex 中捕获组只是从()括号中提取一个模式,可以使用 /regex/.exec(string)string.match 捕捉组。

常规捕获组是通过将模式包装在 (pattern) 中创建的,但是要在结果对象上创建groups 属性,它是: (?<name>pattern)

要创建一个新的组名,只需在括号内附加?<name>,结果中,分组 (pattern) 匹配将成为 group.name,并附加到 match 对象,以下是一个实例:

字符串标本匹配:

图片描述

这里创建了 match.groups.color 和 match.groups.bird :

const string = 'black*raven lime*parrot white*seagull';
const regex = /(?<color>.*?)\*(?<bird>[a-z0-9]+)/g;
while (match = regex.exec(string)) {
    let value = match[0];
    let index = match.index;
    let input = match.input;
    console.log(`${value} at ${index} with '${input}'`);
    console.log(match.groups.color);
    console.log(match.groups.bird);
}

需要多次调用 regex.exec方法来遍历整个搜索结果集。 在每次迭代期间调用.exec 时,将显示下一个结果(它不会立即返回所有匹配项。),因此使用 while 循环

输出如下:

black*raven at 0 with 'black*raven lime*parrot white*seagull'
black
raven
lime*parrot at 11 with 'black*raven lime*parrot white*seagull'
lime
parrot
white*seagull at 23 with 'black*raven lime*parrot white*seagull'
white
seagull

但奇怪的是:

如果你从这个正则表达式中删除/g,你将永远在第一个结果上创建一个无限循环。这在过去是一个巨大的痛苦。想象一下,从某个数据库接收正则表达式时,你不确定它的末尾是否有 /g,你得先检查一下。
使用 .matchAll() 的好理由
在与捕获组一起使用时,它可以更加优雅,捕获组只是使用()提取模式的正则表达式的一部分。
它返回一个迭代器而不是一个数组,迭代器本身是有用的。
迭代器可以使用扩展运算符(…) 转换为数组。
它避免了带有/g 标志的正则表达式,当从数据库或外部源检索未知正则表达式并与陈旧的RegExp 对象一起使用时,它非常有用。
使用 RegExp 对象创建的正则表达式不能使用点(.)操作符链接。
高级: RegExp 对象更改跟踪最后匹配位置的内部 .lastindex 属性,这在复杂的情况下会造成严重破坏。
.matchAll() 是如何工作的?
让我们尝试匹配单词 hello 中字母 e 和 l 的所有实例, 因为返回了迭代器,所以可以使用 for…of 循环遍历它:

// Match all occurrences of the letters: "e" or "l" 
let iterator = "hello".matchAll(/[el]/);
for (const match of iterator)
    console.log(match);

这一次你可以跳过/g.matchall方法不需要它,结果如下:

[ 'e', index: 1, input: 'hello' ] // Iteration 1
[ 'l', index: 2, input: 'hello' ] // Iteration 2
[ 'l', index: 3, input: 'hello' ] // Iteration 3

使用 .matchAll()捕获组示例:
.matchAll 具有上面列出的所有好处。它是一个迭代器,可以用for…of循环遍历它,这就是整个语法的不同。

const string = 'black*raven lime*parrot white*seagull';
const regex = /(?<color>.*?)\*(?<bird>[a-z0-9]+)/;
for (const match of string.matchAll(regex)) {
    let value = match[0];
    let index = match.index;
    let input = match.input;
    console.log(`${value} at ${index} with '${input}'`);
    console.log(match.groups.color);
    console.log(match.groups.bird);
}

请注意已经没有/g 标志,因为 .matchAll() 已经包含了它,打印如下:

black*raven at 0 with 'black*raven lime*parrot white*seagull'
black
raven
lime*parrot at 11 with 'black*raven lime*parrot white*seagull'
lime
parrot
white*seagull at 23 with 'black*raven lime*parrot white*seagull'
white
seagull

也许在美学上它与原始正则表达式非常相似,执行while循环实现。但是如前所述,由于上面提到的许多原因,这是更好的方法,移除/g 不会导致无限循环。

3、动态导入

现在可以将导入分配给变量:

element.addEventListener('click', async () => {
  const module = await import(`./api-scripts/button-click.js`);
  module.clickEvent();
})

4、Array.flat()

扁平化多维数组:

let multi = [ 1, 2, 3, [ 4, 5, 6, [ 7, 8, 9, [ 10, 11, 12 ] ] ] ];
multi.flat();               // [ 1, 2, 3, 4, 5, 6, Array(4) ]
multi.flat().flat();        // [ 1, 2, 3, 4, 5, 6, 7, 8, 9, Array(3) ]
multi.flat().flat().flat(); // [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 ]
multi.flat(Infinity);       // [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 ]

5、Array.flatMap()

let array = [ 1, 2, 3, 4, 5 ];
array.map(x => [ x, x * 2 ]);

结果:

[Array(2), Array(2), Array(2), Array(2), Array(2)]
0: (2) [ 1, 2 ]
1: (2) [ 2, 4 ]
2: (2) [ 3, 6 ]
3: (2) [ 4, 8 ]
4: (2) [ 5, 10 ]

使用 flatMap 方法:

array.flatMap(v => [v, v * 2]);
[ 1, 2, 2, 4, 3, 6, 4, 8, 5, 10 ]

6、Object.fromEntries()

将键值对列表转换为对象:

let obj = { apple : 10, orange : 20, banana : 30 };
let entries = Object.entries(obj);
/**
[Array(2), Array(2), Array(2)]
 0: (2) ["apple", 10]
 1: (2) ["orange", 20]
 2: (2) ["banana", 30]
 */
let fromEntries = Object.fromEntries(entries);
{ apple: 10, orange: 20, banana: 30 }

7、String.trimStart() 与 String.trimEnd()

let greeting = "     Space around     ";
greeting.trimEnd();   // "     Space around";
greeting.trimStart(); // "Space around     ";

8、格式良好的 JSON.stringify()

此更新修复了字符 U+D800U+DFFF 的处理,有时可以进入 JSON 字符串。 这可能是一个问题,因为 JSON.stringify 可能会将这些数字格式化为没有等效 UTF-8 字符的值, 但 JSON 格式需要 UTF-8 编码

解析方法使用格式良好的JSON字符串,如:

'{ "prop1" : 1, "prop2" : 2 }'; // A well-formed JSON format string

注意,要创建正确JSON 格式的字符串,绝对需要在属性名周围加上双引号。缺少或任何其他类型的引号都不会生成格式良好的JSON

'{ "prop1" : 1, "meth" : () => {} }'; // Not JSON format string

JSON 字符串格式与 Object Literal 不同,后者看起来几乎一样,但可以使用任何类型的引号括住属性名,也可以包含方法(JSON格式不允许使用方法):

let object_literal = { property: 1, meth: () => {} };

不管怎样,一切似乎都很好。第一个示例看起来是兼容的。但它们也是简单的例子,大多数情况下都能顺利地工作!

U+2028U+2029 字符
问题是, ES10 之前的 EcmaScript 实际上并不完全支持 JSON 格式。前 ES10 时代不接受未转义行分隔符 U+2028和段落分隔符U+2029 字符:
在这里插入图片描述
U+2029 is the line separator.

U+2029 is the paragraph separator. Sometimes it can lurk into your JSON formatted string.

对于U+D800 - U+DFFF之间的所有字符也是如此
如果这些字符潜入 JSON 格式的字符串(假设来自数据库记录),你可能会花费数小时试图弄清楚为什么程序的其余部分会产生解析错误。

因此,如果你传递eval 这样的字符串 “console.log(' hello ')”,它将执行 JavaScript语句 (通过尝试将字符串转换为实际代码),也类似于 JSON.parse 将处理你的 JSON 字符串的方式。

9、稳定的 Array.prototype.sort()

V8 之前的实现对包含10个以上项的数组使用了一种不稳定的快速排序算法。

一个稳定的排序算法是当两个键值相等的对象在排序后的输出中出现的顺序与在未排序的输入中出现的顺序相同时。
但情况不再是这样了,ES10 提供了一个稳定的数组排序:

let fruit = [
    { name: "Apple",      count: 13, },
    { name: "Pear",       count: 12, },
    { name: "Banana",     count: 12, },
    { name: "Strawberry", count: 11, },
    { name: "Cherry",     count: 11, },
    { name: "Blackberry", count: 10, },
    { name: "Pineapple",  count: 10, }
];
// 创建排序函数:
let my_sort = (a, b) => a.count - b.count;
// 执行稳定的ES10排序:
let sorted = fruit.sort(my_sort);
console.log(sorted);

控制台输出(项目以相反的顺序出现):
在这里插入图片描述

10、新的Function.toString()

函数是对象,并且每个对象都有一个 .toString() 方法,因为它最初存在于Object.prototype.toString()上。 所有对象(包括函数)都是通过基于原型的类继承从它继承的。

这意味着我们以前已经有 funcion.toString()方法了。

但是 ES10 进一步尝试标准化所有对象和内置函数的字符串表示。 以下是各种新案例:

典型的例子:

function () { console.log('Hello there.'); }.toString();

控制台输出(函数体的字符串格式:)

// function () { console.log('Hello there.'); }

下面是剩下的例子:

直接在方法名 .toString()

Number.parseInt.toString();
// function parseInt() { [native code] }

绑定上下文:

function () { }.bind(0).toString();
// function () { [native code] }

内置可调用函数对象:

Symbol.toString();
// function Symbol() { [native code] }

动态生成的函数:

function* () { }.toString();
// function* () { }

prototype.toString

Function.prototype.toString.call({});
// Function.prototype.toString requires that 'this' be a Function"

11、可选的 Catch Binding

在过去,try/catch 语句中的 catch 语句需要一个变量。 try/catch 语句帮助捕获终端级别的错误:

try {
    // Call a non-existing function undefined_Function
    undefined_Function("I'm trying");
} catch(error) {
    // Display the error if statements inside try above fail
    console.log( error ); // undefined_Function is undefined
}

在某些情况下,所需的错误变量是未使用的:

try {
    JSON.parse(text); // <--- this will fail with "text not defined"
    return true; // <--- exit without error even if there is one
} catch (redundant_sometmes) { // <--- this makes error variable redundant
    return false;
}

编写此代码的人通过尝试强制 true 退出 try 子句。但是,这并不是实际发生的情况

(() => {
    try {
        JSON.parse(text)
        return true
    } catch(err) {
        return false
    }
})()
// false

在 ES10 中,捕获错误的变量是可选的
现在可以跳过错误变量:

try {
    JSON.parse(text);
    return true;
} catch {
    return false;
}

目前还无法测试上一个示例中的 try 语句的结果,但一旦它出来,我将更新这部分。

12、标准化 globalThis 对象

这在ES10之前, globalThis 还没有标准化。

在产品代码中,你可以自己编写这个怪物,在多个平台上“标准化”它:

let getGlobal = function() {
    if (typeof self !== 'undefined') { return self; }
    if (typeof window !== 'undefined') { return window; }
    if (typeof global !== 'undefined') { return global; }
    throw new Error('unable to locate global object');
};

但即使这样也不总是奏效。因此,ES10 添加了 globalThis 对象,从现在开始,该对象用于在任何平台上访问全局作用域:

// 访问全局数组构造函数
globalThis.Array(0, 1, 2);
// [ 0, 1, 2 ]

// 类似于 ES5 之前的 window.v = { flag: true }
globalThis.v = { flag: true };

console.log(globalThis.v);
// { flag: true }

13、Symbol.description

description 是一个只读属性,它返回 Symbol 对象的可选描述。

let mySymbol = 'My Symbol';
let symObj = Symbol(mySymbol);
symObj; // Symbol(My Symbol)
symObj.description; // "My Symbol"

14、Hashbang 语法

也就是 unix 用户熟悉的 shebang。它指定一个解释器(什么将执行JavaScript文件?)。

ES10标准化,我不会对此进行详细介绍,因为从技术上讲,这并不是一个真正的语言特性,但它基本上统一了 JavaScript 在服务器端的执行方式。

$ ./index.js

代替

$ node index.js

15、ES10类:private、static 和 公共成员

新的语法字符 #octothorpe(hash tag)现在用于直接在类主体的范围内定义变量,函数,getter 和 setter …以及构造函数和类方法。

下面是一个毫无意义的例子,它只关注新语法:

class Raven extends Bird {
#state = { eggs: 10};
// getter
    get #eggs() { 
        return state.eggs;
    }
// setter
    set #eggs(value) {
        this.#state.eggs = value;
    }
#lay() {
        this.#eggs++;
    }
constructor() {
        super();
        this.#lay.bind(this);
    }
#render() {
        /* paint UI */
    }
}

参考文章
https://blog.csdn.net/ch4230052/article/details/7966417
https://juejin.im/post/5b9cb3336fb9a05d290ee47e
https://juejin.im/post/5c346fe7f265da611510f823
https://juejin.im/post/5b2a186cf265da596d04a648
https://www.jianshu.com/p/602d3f136a59
https://www.runoob.com/w3cnote/es6-class.html
https://blog.csdn.net/qdmoment/article/details/82496685
https://segmentfault.com/a/1190000018311280?utm_source=tag-newest

  • 2
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值