ES6~ES12
Published on Feb 20, 2023, with 13 view(s) and 0 comment(s)
Ai 摘要:本文系统梳理了ES6至ES12的主要新增特性。ES6核心包括class语法糖、解构赋值、let/const、箭头函数、Set/Map等;ES7新增数组includes方法和指数运算符;ES8引入对象扩展方法、字符串填充等;ES9扩展异步迭代和展开运算符;ES10新增数组扁平化、URL参数转换等;ES11引入BigInt、可选链、全局对象等;ES12增加FinalizationRegistry、WeakRefs等。这些特性逐步完善了JavaScript的语法体系,提升了开发效率和代码可读性。

ES6

一,class相关

按照构造函数形式创建类,不仅仅和编写普通的函数过于相似,而且代码并不容易理解。在ES6(ECMAScript2015)新的标准中使用了class关键字来直接定义类,但是类本质上依然是前面所讲的构造函数、原型链的语法糖而已;  定义一个类

class Person1 {}
let person2 = class {};

类的构造函数如果我们希望在创建对象的时候给类传递一些参数,这个时候应该如何做呢?每个类都可以有一个自己的构造函数(方法),这个方法的名称是固定的constructor。当我们通过new操作符,操作一个类的时候会调用这个类的构造函数constructor。每个类只能有一个构造函数,如果包含多个构造函数,那么会抛出异常。

class Person {
  constructor(name, age, height) {
    this.name = name;
    this.age = age;
    this.height· = height;
  }
  running() {
    console.log(this.name· + " running~");
  }
  eating() {
    console.log(this.name + " eating~");
  }
}

类的实例方法上面定义的name,age,height属性都是通过this绑定创建的新对象上。而对于running和eating方法则是原型上,实现多个实例的共享。

类的访问器方法在对象的属性中可以有getter和setter函数,同样在类中也有。

class Person {
  constructor(name) {
    this._name = name;
  }

  set name(newName) {
    console.log("调用了set方法");
    this._name = newName;
  }
  get name() {
    console.log("调用了get方法");
    return this._name;
  }
}
let p1 = new Person("sun");
p1.name = "buke";
// console.log(p1.name);
// 调用了set方法
// 调用了get方法
// buke

类的静态方法静态方法通常用于定义直接使用类来执行的方法,不需要有类的实例,使用static关键字来定义

class Person {
  constructor(name) {
    this.name = name;
  }
  static createPerson(name) {
    return new Person(name);
  }
}
let p = Person.createPerson("sun");
console.log(p); //Person { name: "sun" }

类的继承在ES6中新增了使用extends关键字,可以方便的帮助我们实现继承

class Person {}
class Student extends Person {}

super关键字super关键字可以对父类中的方法进行调用。

class Person {
  constructor(name) {
    this.name = name;
  }
  say() {
    console.log(this.name);
  }
}
class Student extends Person {
  constructor(name) {
    super(name);
  }
}
let s1 = new Student("sun");
s1.say();

继承内置类我们也可以让我们的类继承自内置类,从而实现对这个内置类的扩展。但通常情况下不需要如此。

class sArray extends Array {
  lastItem() {
    return this[this.length - 1];
  }
}
let arr = new sArray(2, 3, 4, 5, 6);
arr.push(7);
console.log(arr.lastItem());

JavaScript中的多态多态(英语:polymorphism)指为不同数据类型的实体提供统一的接口,或使用一个单一的符号来表示多个不同的类型。如此看来JavaScript是一定存在多态的。

function sum(a, b) {
  console.log(a + b);
}
sum(10, 20);
sum("abc", "cba");

二,基础语法

1,字面量增强写法

属性增强写法是对象中代表键值的变量名称与键名一致,则可以直接缩写为一个。函数增强写法是对象中的函数,function() 可以改为 ()计算属性写法

let name = "sun";
let age = 19;
let person = {
  name,
  age,
  [name + 123]: "hello",
  eating() {
    console.log(this.name);
  },
};
console.log(person); //{ name: "sun", age: 19, sun123: "hello", eating: [Function: eating] }

2,解构

ES6中新增了一个从数组或对象中方便获取数据的方法,称之为解构Destructuring。数组结构:基本解构过程、顺序解构、解构出数组、默认值

let names = ["sun0", "sun1", "sun2"];
let [item1, item2, item3] = names;
let [item4, item5] = names;
let [, , item6] = names;
let [item7, ...item8] = names;
console.log(item1, item2, item3); //sun0 sun1 sun2
console.log(item4, item5); //sun0 sun1
console.log(item6); //sun2
console.log(item7, item8); //sun0 [ "sun1", "sun2" ]

对象结构:基本解构过程、任意顺序、重命名、默认值

let obj = {
  name: "sun",
  age: 19,
  height: 190,
};
let { age, name: newName, height, address = "上海" } = obj;
console.log(age, newName, address); //19 sun 上海

解构的应用场景解构目前在开发中使用是非常多的:

  • 比如在开发中拿到一个变量时,自动对其进行解构使用

  • 比如对函数的参数进行解构DescriptionDescription

2,let和const

var和let/const区别

  • 1,let和const不允许重复声明

  • 2,var、let、const都用于声明一个变量,const声明的变量值不能再改变,如果声明一个对象,可以修改其属性和属性值,因为存对象时保存的是地址,const保证地址不变。

  • 3,作用域提升:var有,let和const没有,但是let和const声明的变量仍旧在解析阶段被创建出来但是没有赋值也不能访问。

  • 4,window和var、let和const关系。var声明的变量会添加到window对象上,而let和const声明的变量被保存在Variable_:variableMap中

  • 5,作用域,es5中只有全局作用域和函数作用域,es6中增添了块级作用域,如if语句,switch语句,for语句,class,{}。var没有块级作用域,let和const有。

  • 6,暂时性死区,在一个代码块中,不能在声明之前去使用变量

// 暂时性死区
let obj = "hello";
function say() {
  console.log(obj);
  let obj = "nihao";
}
say();

var、let、const的选择尽量不使用var,优先选择const,次选择let

3,字符串模板的使用

es6之前如果需要将字符串和变量拼接在一起需要使用到连接符 + ,但是es6之后可以使用模板字符 ``,在模板字符串中可以使用 ${expression} 写变量甚至是表达式来嵌入动态内容。

模板字符串还有另外一种用法:标签模板字符串(Tagged Template Literals),如下例。如果我们使用标签模板字符串,并且在调用的时候插入其他的变量:

  • 模板字符串被拆分了;

  • 第一个元素是数组,是被模块字符串拆分的字符串组合;

  • 后面的元素是一个个模块字符串传入的内容;

let name = "sun",
  age = 20;
console.log(`${name} is ${age} years old now!`); //sun is 20 years old now!

function foo(...args) {
  console.log(args);
}
foo("helloworld"); //[ "helloworld" ]
foo`hello${name}world${age}`; //[ [ "hello", "world", "" ], "sun", 20 ]

4,展开语法

展开语法 (Spread syntax), 可以在函数调用/数组构造时,将数组表达式或者 string 在语法层面展开;还可以在构造字面量对象时,将对象表达式按 key-value 的方式展开。在函数调用时使用展开语法

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

在 new 表达式中应用

let dateFields = [1970, 0, 1]; // 1970 年 1 月 1 日
let d = new Date(...dateFields);

数组拷贝 (copy)注意,浅拷贝,只遍历一层。

let arr = [1, 2, [3]];
let arr2 = [...arr];

arr2[2].push(4);

console.log(arr2);
console.log(arr); //arr也被改变了
// [ 1, 2, [ 3, 4 ] ]
// [ 1, 2, [ 3, 4 ] ]

连接多个数组

let arr1 = [0, 1, 2];
let arr2 = [3, 4, 5];
let arr3 = [...arr1, ...arr2];
console.log(arr3); //[ 0, 1, 2, 3, 4, 5 ]

构造字面量对象时使用展开语法

var obj1 = { foo: "bar", x: 42 };
var obj2 = { foo: "baz", y: 13 };

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

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

5,数值的表示

在ES6中规范了二进制和八进制的写法:

const num1 = 100;
const num2 = 0b100; //binary二进制
const num3 = 0o100; //octonary八进制
const num4 = 0x100; // hexadecimal十六进制
console.log(num1, num2, num3, num4); //100 4 64 256

ES2021新增特性:数字过长时,可以使用_作为连接符

const num5 = 100_000_000;
console.log(num5); //100000000

6,symbol

Symbol是什么呢?Symbol是ES6中新增的一个基本数据类型,翻译为符号。 那么为什么需要Symbol呢? 在ES6之前,对象的属性名都是字符串形式,那么很容易造成属性名的冲突;比如原来有一个对象,我们希望在其中添加一个新的属性和值,但是我们在不确定它原来内部有什么内容的情况下,很容易造成冲突,从而覆盖掉它内部的某个属性; Symbol就是为了解决上面的问题,用来生成一个独一无二的值。Symbol值是通过Symbol函数来生成的,生成后可以作为属性名;也就是在ES6中,对象的属性名可以使用字符串,也可以使用Symbol值;Symbol即使多次创建值,它们也是不同的:Symbol函数执行后每次创建出来的值都是独一无二的;我们也可以在创建Symbol值的时候传入一个描述description:这个是ES2019(ES10)新增的特性;

基本使用

const s1 = Symbol();
const s2 = Symbol();
console.log(s1 === s2); //false

const obj = {
  [s1]: "sun",
  [s2]: 20,
};
console.log(obj[s1], obj[s2]); //sun 20,只能通过obj[]访问,不能通过obj.s1获取属性值

description获取描述

const s3 = Symbol("hello");
console.log(s3.description); //hello

新增属性

const s1 = Symbol();
const s2 = Symbol();
const s3 = Symbol();
const s4 = Symbol();
const obj = {
  [s1]: "sun",
  [s2]: 20,
};
obj[s3] = "180";
Object.defineProperty(obj, s4, {
  value: "河南",
  enumerable: true,
  writable: true,
  configurable: true,
});
console.log(obj[s1], obj[s2], obj[s3], obj[s4]); //sun 20 180 河南

获取属性只能通过Object.getOwnPropertySymbols(obj)来获取属性值。

console.log(Object.keys(obj)); //[]
console.log(Object.getOwnPropertyNames(obj)); //[];
console.log(Object.getOwnPropertySymbols(obj)); //[ Symbol(), Symbol(), Symbol(), Symbol() ]
const skeys = Object.getOwnPropertySymbols(obj);
for (let i of skeys) {
  console.log(obj[i]); //sun 20 180 河南
}

相同值的Symbol前面我们讲Symbol的目的是为了创建一个独一无二的值,那么如果我们现在就是想创建相同的Symbol应该怎么来做呢?

const s1 = Symbol.for("hello");
const s2 = Symbol.for("hello");
console.log(s1 === s2); //true
console.log(typeof s1); //symbol
console.log(Symbol.keyFor(s1)); //hello

三,函数相关

1,函数的默认参数

在es6中定义函数可以给参数一个默认值,当调用函数时传入参数则使用传入的参数,若没有传入参数则使用默认参数。

function add(x = 10, y = 20) {
  return x + y;
}
console.log(add(1, 2)); // 3
console.log(add()); //30

配合解构使用

function foo({ name, age } = { name: "sun", age: 20 }) {
  return `${name} is ${age} years old now!`;
}
let obj1 = {
  name: "xiaoming",
  age: 18,
};
console.log(foo(obj1)); //xiaoming is 18 years old now!
console.log(foo()); //sun is 20 years old now!

2,函数的剩余参数

ES6中引用了rest parameter,可以将不定数量的参数放入到一个数组中。如果最后一个参数是 … 为前缀的,那么它会将剩余的参数放到该参数中,并且作为一个数组;

function foo(m, n, ...args) {
  console.log(m, n); // 1 2
  console.log(args); //[3, 4, 5, 6, 7, 8, 9, 0];
}
let arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0];
foo(...arr);

剩余参数和arguments区别

  • 1,剩余参数只包含那些没有对应形参的实参,而arguments对象包含了传给函数的所有实参;

  • 2,arguments对象不是一个真正的数组,而rest参数是一个真正的数组,可以进行数组的所有操作;

  • 3,arguments是早期的ECMAScript中为了方便去获取所有的参数提供的一个数据结构,而rest参数是ES6中提供,并且希望以此来替代arguments的;

  • 4,剩余参数必须放到最后一个位置,否则会报错。

3,箭头函数

基本语法

// 箭头函数
let fun = (name) => {
    return `Hello ${name} !`;
};

// 普通函数
let fun = function (name) {
    return `Hello ${name} !`;
};

箭头函数的参数

  • 如果箭头函数没有参数,直接写一个空括号即可。

  • 如果箭头函数的参数只有一个,也可以省去包裹参数的括号。

  • 如果箭头函数有多个参数,将参数依次用逗号(,)分隔,包裹在括号中即可。

// 没有参数
let fun1 = () => {
    console.log("dingFY");
};

// 只有一个参数,可以省去参数括号
let fun2 = name => {
    console.log(`Hello ${name} !`)
};

// 有多个参数,逗号分隔
let fun3 = (val1, val2, val3) => {
    return [val1, val2, val3];
};

箭头函数的函数体

  • 如果箭头函数的函数体只有一句代码,就是简单返回某个变量或者返回一个简单的JS表达式,可以省去函数体的大括号{ }。

let fun = val => val;
// 等同于
let fun = function (val) { return val };

let sum = (num1, num2) => num1 + num2;
// 等同于
let sum = function(num1, num2) {
  return num1 + num2;
};
  • 如果箭头函数的函数体只有一句代码,就是返回一个对象,可以像下面这样写:

// 用小括号包裹要返回的对象,不报错
let getTempItem = id => ({ id: id, name: "Temp" });

// 但绝不能这样写,会报错,因为对象的大括号会被解释为函数体的大括号
let getTempItem = id => { id: id, name: "Temp" };
  • 如果箭头函数的函数体只有一条语句并且不需要返回值(最常见是调用一个函数),可以给这条语句前面加一个void关键字

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

箭头函数与普通函数的区别

  • 1,语法更加简洁、清晰

  • 2,箭头函数没有 prototype (原型),所以箭头函数本身没有this

  • 3,箭头函数不会创建自己的this。箭头函数没有自己的this,箭头函数的this指向在定义(注意:是定义时,不是调用时)的时候继承自外层第一个普通函数的this。所以,箭头函数中 this 的指向在它被定义的时候就已经确定了,之后永远不会改变。

  • 4,call | apply | bind 无法改变箭头函数中this的指向

  • 5,箭头函数不能作为构造函数使用

  • 6,箭头函数不绑定arguments,取而代之用rest参数…代替arguments对象,来访问箭头函数的参数列表

  • 7,箭头函数不能用作Generator函数,不能使用yield关键字

四,set和map

set和map

ES7

1,Array Includes

在ES7之前,如果我们想判断一个数组中是否包含某个元素,需要通过 indexOf 获取结果,并且判断是否为 -1。在ES7中,我们可以通过includes来判断一个数组中是否包含一个指定的元素,根据情况,如果包含则返回 true,否则返回false

const arr = [1, 2, 3, 4, 5, 6, 7, NaN];
if (!(arr.indexOf(2) == -1)) {
  console.log("包含2"); //包含2
}
if (!(arr.indexOf(NaN) == -1)) {
  console.log("包含NaN");
}
if (arr.includes(2)) {
  console.log("包含2"); //包含2
}
if (arr.includes(NaN)) {
  console.log("包含NaN"); //包含NaN
}

2,ES7 –指数(乘方)运算符

在ES7之前,计算数字的乘方需要通过 Math.pow 方法来完成。在ES7中,增加了 ** 运算符,可以对数字来计算乘方。

const x = 3;
console.log(Math.pow(x, 3)); //27
console.log(x ** 3); //27

ES8

1, Object.values

之前我们可以通过 Object.keys 获取一个对象所有的key,在ES8中提供了 Object.values 来获取所有的value值

const obj = {
  name: "sun",
  age: 20,
};
console.log(Object.keys(obj)); //[ "name", "age" ]
console.log(Object.values(obj)); //[ "sun", 20 ]

2,Object entries

通过Object.entries 可以获取到一个数组,数组中会存放可枚举属性的键值对数组

const obj = {
  name: "sun",
  age: 20,
};
console.log(Object.entries(obj)); //[ [ "name", "sun" ], [ "age", 20 ] ]

3,String Padding

某些字符串我们需要对其进行前后的填充,来实现某种格式化效果,ES8中增加了 padStart 和 padEnd 方法,分别是对字符串的首尾进行填充的。

const message = "Hello World";
console.log(message.padStart(15, "a")); //aaaaHello·World
console.log(message.padEnd(15, "b")); //Hello worldbbbb

//场景:脱敏显示号码
const num = "17634989523";
const lastNum = num.slice(-4);
const newNum = lastNum.padStart(num.length, "*");
console.log(newNum); //*******9523

4,Trailing Commas

在ES8中,我们允许在函数定义和调用时多加一个逗号:

function foo(a, b) {
  console.log(a, b);
}
foo(10, 20);

4,getOwnPropertyDescriptors获取属性描述符

5,async和await

ES9

1,Async iterators

2,Object spread operators展开运算符

3,Promise finally

ES10

1,flat flatMap

flat() 方法会按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回。  flatMap() 方法首先使用映射函数映射每个元素,然后将结果压缩成一个新数组。

  • 注意一:flatMap是先进行map操作,再做flat的操作;

  • 注意二:flatMap中的flat相当于深度为1;

const arr = [1, 2, [3, 4, 5], [6, 7, [8, 9]]];
const arrflat = arr.flat();
const arrflat1 = arr.flat(2);
console.log(arrflat); //[ 1, 2, 3, 4, 5, 6, 7, [ 8, 9 ] ]
console.log(arrflat1); //[1, 2, 3, 4, 5, 6, 7, 8, 9];

const arr1 = ["hello world", "ni hao", "ni hao china"];
const arrflatmap = arr1.flatMap((item) => {
  return item.split(" ");
});
console.log(arrflatmap);
//["hello", "world", "ni", "hao", "ni", "hao", "china"];

2,Object fromEntries

我们可以通过 Object.entries 将一个对象转换成 entries,那么如果我们有一个entries了,如何将其转换成对象呢?ES10提供了 Object.formEntries来完成转换

const obj = {
  name: "sun",
  age: 20,
};
const entriesObj = Object.entries(obj);
console.log(entriesObj); //[ [ "name", "sun" ], [ "age", 20 ] ]

const fromEntriesObj = Object.fromEntries(entriesObj);
console.log(fromEntriesObj); //{ name: "sun", age: 20 }

实例:url参数转对象

const queryString = "name=sun&age=20&height=190";
const searchParams = new URLSearchParams(queryString);
console.log(searchParams); //URLSearchParams { "name" => "sun", "age" => "20", "height" => "190" }
for (let i of searchParams) {
  console.log(i); // ["name", "sun"]["age", "20"]["height", "190"];
}
const queryObj = Object.fromEntries(searchParams);
console.log(queryObj); //{ name: "sun", age: "20", height: "190" }

3,trimStart、trimEnd

去除一个字符串首尾的空格,我们可以通过trim方法,如果单独去除前面或者后面呢?ES10中给我们提供了trimStart和trimEnd

const str = " hello world ";
console.log(str.trim() + "nihao"); //hello worldnihao
console.log(str.trimStart() + "nihao"); //hello world nihao
console.log(str.trimEnd() + "nihao");// hello worldnihao

4,Symbol description

5,Optional catch binding

ES11

1,BigInt

在早期的JavaScript中,我们不能正确的表示过大的数字。  大于MAXSAFEINTEGER的数值,表示的可能是不正确的。  那么ES11中,引入了新的数据类型BigInt,用于表示大的整数:BitInt的表示方法是在数值的后面加上n

const maxInt = Number.MAX_SAFE_INTEGER;
console.log(maxInt); //9007199254740991 默认最大安全数值
console.log(maxInt + 1, maxInt + 2);
//9007199254740992 9007199254740992 发生错误
const bigInt = 9107199254740992n;
const bigInt2 = 20n;
console.log(bigInt + bigInt2);//9107199254741012n
console.log(typeof bigInt); //bigint

2,空值合并操作符

const params = 0; // undefined、null、0、""
const ans = params || "default value";
const ans1 = params ?? "default value";
console.log(ans); // default value
console.log(ans1); // ""
// ||:无论params为 0 undefined null ""  都返回默认值
// ??:当params为 0 或者 ""时,返回该值

3,可选链

const obj = {
  friend: {
    girlFriend: { name: "lucy" },
  },
};
if (obj.friend && obj.friend.girlFriend) {
  console.log(obj.friend.girlFriend.name);
}
//可选链的方式
console.log(obj.friend?.girlFriend?.name);

4,获取全局对象 GlobalThis

在之前我们希望获取JavaScript环境的全局对象,不同的环境获取的方式是不一样的

  • 比如在浏览器中可以通过this、window来获取;

  • 比如在Node中我们需要通过global来获取; 那么在ES11中对获取全局对象进行了统一的规范:globalThis

console.log(globalThis);

5,for..in标准化

在ES11之前,虽然很多浏览器支持for…in来遍历对象类型,但是并没有被ECMA标准化。在ES11中,对其进行了标准化,for…in是用于遍历对象的key的

const obj = {
  name: "sun",
  age: 18,
  height: 1.88,
};
for (const key in obj) {
  console.log(key); //sun age height
}

6,Dynamic Import:动态导入

7,Promise.allSettled

8,import meta

ES12

1,FinalizationRegistry

FinalizationRegistry 对象可以让你在对象被垃圾回收时请求一个回调。  FinalizationRegistry 提供了这样的一种方法:当一个在注册表中注册的对象被回收时,请求在某个时间点上调用一个清理回调。(清理回调有时被称为 finalizer );  你可以通过调用register方法,注册任何你想要清理回调的对象,传入该对象和所含的值;

2,WeakRefs

如果我们默认将一个对象赋值给另外一个引用,那么这个引用是一个强引用,如果我们希望是一个弱引用的话,可以使用WeakRef

// WeakRef.prototype.deref: 
// > 如果原对象没有销毁, 那么可以获取到原对象
// > 如果原对象已经销毁, 那么获取到的是undefined
const finalRegistry = new FinalizationRegistry((value) => {
  console.log("注册在finalRegistry的对象, 某一个被销毁", value)
})

let obj = { name: "why" }
let info = new WeakRef(obj)

finalRegistry.register(obj, "obj")

obj = null

setTimeout(() => {
  console.log(info.deref()?.name)
  console.log(info.deref() && info.deref().name)
}, 10000)

3,逻辑赋值运算

// 1.||= 逻辑或赋值运算
// let message = "hello world"
// message = message || "default value"
// message ||= "default value"
// console.log(message)

// 2.&&= 逻辑与赋值运算
// &&
// const obj = {
//   name: "why",
//   foo: function() {
//     console.log("foo函数被调用")

//   }
// }

// obj.foo && obj.foo()

// &&=
// let info = {
//   name: "why"
// }

// // 1.判断info
// // 2.有值的情况下, 取出info.name
// // info = info && info.name
// info &&= info.name
// console.log(info)

// 3.??= 逻辑空赋值运算
let message = 0
message ??= "default value"
console.log(message)

4,Numeric Separator,大数字分隔符;

5,String.replaceAll:字符串替换;