ECMA2015 – ECMA2021新增特性(es6)

  • Post author:
  • Post category:其他




一、 ECMAScript2015(ES6)



1. 声明方式



1.1 作用域

es6之前,js有两种作用域:全局作用域,函数作用域,es6在此基础上扩展了块级作用域,即:在代码块({})中的作用域。


全局作用域

不在函数内部,或者代码块({})内部的视为全局作用域

var b = 2; // 全局作用域
{
  let = 3;
}
function a(){
  var a = 1;
}


函数作用域

函数内部的作用域

var b = 2; // 全局作用域
{
  let = 3;
}
function a(){
  var a = 1; // 函数作用域
}


块级作用域

es6将代码块中作用域称为块级作用域

var b = 2; // 全局作用域
{
  let = 3; // 块级作用域
}
function a(){
  var a = 1; // 函数作用域
}



1.2 let

es6中新加了let关键字来声明变量。

  • let具有块级作用域
{
    let a = 5
}
console.log(a) // undefined

在for循环中,在()中let的作用域属于for循环本身。可以理解使用一对{}将for循环整个扩起来。

function test(){
  let i = 1;
  for(let i = 0; i < 3; i++){
    let i = 'in for';
    console.log(i);
  }
  console.log(i);
}
test();
// in for
// in for
// in for
// 1
  • let不具备声明全局变量的能力

这就意味着使用let声明的变量没有绑定到window对象上

var a = 1;
let b = 2;
console.log(window.a); // 1
console.log(window.b); // undefined
  • let定义的变量不允许重复声明

var关键字定义的变量是可以重复定义的,let是不被允许的,即使let和var混合使用也不被允许。

// var 是可以重复定义的
var a = 1;
var a = 2;
console.log(a); // 2
// let 不允许重复定义
let a = 1;
let a = 2; // Uncaught SyntaxError: Identifier 'a' has already been declared
console.log(a);
// 即使let和var放在一起也不能重复定义
var a = 1;
let a = 2; // Uncaught SyntaxError: Identifier 'a' has already been declared
console.log(a);
  • let声明的变量不存在变量提升

所谓的变量提升是指:使用var声明变量时,编译时,编译器会自定把变量的声明放在作用域前端

function a(){
  console.log(b); // undefined
  var b = 4;
  console.log(b); // 4
}

a();

在编译器眼中,上面的代码时这样的:

function a(){
  var b;
  console.log(b); // undefined
  b = 4;
  console.log(b); // 4
}

a();

但是let并不存在变量提升,let只能先声明,后使用

function a(){
  console.log(b); // Uncaught ReferenceError: Cannot access 'b' before initialization
  let b = 4;
  console.log(b);
}

a();
  • let 声明变量存在暂时性死区

所谓的暂时性死区是指:只要作用域中(块级作用域,函数作用域)使用了let声明变量,那么变量就和这个作用域绑定了,不再受外部影响

// 块级作用域
var a = 5;
{
  a = 6; // Uncaught ReferenceError: Cannot access 'a' before initialization
  let a;
}

// 函数作用域
var b = 8;
function test(){
  b = 7; // // Uncaught ReferenceError: Cannot access 'a' before initialization
  let b;
}

test();



1.3 const

const关键字也可以定义变量,const与let不同之处在于:const定义的是常量,它定义之后变量不可更改。

这里的不可更改是指const定义的变量的内存地址不可更改,如果用const定义的是对象,那么对象的属性还是可以更改的。

const PI = 3.14;
PI = 3.1415; // Uncaught TypeError: Assignment to constant variable.

const a = {a: 1};
a.a = 2;



1.4 使用建议

不使用var,主要使用const,配合使用let

  1. const可以用在对象、数组与函数上,常量一声明时就要赋值,犯错的机会会减少很多
  2. JS引擎也可以作优化。虽然JS语言上并没有感受差异,但实际上引擎里有区分出常量与变量,而最重要的是因为JS语言是弱(动态)类型的脚本语言,常量一声明就赋值,代表它立即决定好是什么类型与值,自然效能比变量好得多。

    在编译语言中,常量将在编译时被替换,并且它的使用将允许其他优化(如死代码删除)以进一步提高代码的运行时效率。JavaScript引擎实际上编译JS代码以获得更好的性能,因此使用const关键字将告知它们上述优化是可能的并且应该完成。这导致更好的性能。
  3. eslint 有个 规则 prefer const,如果配置了这条 ESLint 规则(或者引用的第三方 ESLint 库配置了),且编辑器有自动格式化或者项目有提交前格式化的功能,那么代码里所有的未重新赋值的 let 会自动被改成 const(实际上,你看到的多是 const 也往往是这么产生的,毕竟 const 打起来麻烦一点)



2. 解构赋值

解构赋值:允许按照一定模式,从数组和对象中提取值,对变量进行赋值。



2.1 数组解构

  • 赋值元素可以是任意可遍历的对象,数组,字符串,map,set都可以。
const [a, b, c] = [1, 2, 3];
const [aa, bb, cc] = 'abc';
const [e, f, g] = new Set([1, 2, 3]);
const [ee, ff, gg] = new Map([[1, 2], [3, 4], [5, 6]]);
console.log(a, b, c); // 1 2 3
console.log(aa, bb, cc); // a b c
console.log(e, f, g); // 1 2 3
console.log(ee, ff, gg); // [1, 2] [3, 4] [5, 6]
  • 左边的变量可以是对象的属性,不局限于单纯的变量
const test = {};
[test.a, test.b] = [12, 34];
console.log(test.a, test.b); // 12 34
  • 在循环体中配合entries使用
const test = {
  a: 1,
  b: 2
};
for(let [key, value] of Object.entries(test)){
  console.log(key, value);
}
// a 1
// b 2
  • 如果想忽略数组的某个元素对变量进行赋值,可以使用逗号来处理。
const [a, , b] = [1, 2, 3];
console.log(a, b); // 1 3

  • rest参数(使用 rest 来接受赋值数组的剩余元素,要确保这个 rest 参数是放在被赋值变量的最后一个位置上)
const [a, ...b] = [1, 2, 3];
console.log(a); // 1
console.log(b); // [2, 3]
  • 果数组的内容少于变量的个数,并不会报错,没有分配到内容的变量会是 undefined
const [a, b, c] = [1, 2];
console.log(a, b, c); // 1 2 undefiend



2.2 对象解构

  • 基本用法
let {a, b, c} = {a: 1, b: 2, c: 3};
console.log(a, b, c); //  1 2 3

左侧的“模板”结构要与右侧的 Object 一致,但是属性的顺序无需一致。

上面例子用了简写,类似于:

let {a: a, b: b, c: c} = {a: 1, b: 2, c: 3};
console.log(a, b, c); //  1 2 3

如果不想这么写或者想使用其他的变量名,可以自定义的,如下

let {a: aa, b: bb, c: cc} = {a: 1, b: 2, c: 3};
console.log(aa, bb, cc); //  1 2 3
  • 指定默认值
let {a, b, c = 20} = {a: 1, b: 2};
console.log(a, b, c); // 1 2 20
  • rest运算符
let {a, ...b} = {a: 1, b: 2, c: 3};
console.log(a); // 1
console.log(b); // {b: 2, c: 3}
  • 嵌套对象

如果一个 Array 或者 Object 比较复杂,它嵌套了 Array 或者 Object,那只要被赋值的结构和右侧赋值的元素一致就好了。

let options = {
  size: {
    width: 100,
    height: 200
  },
  items: ["Cake", "Donut"],
  extra: true    // something extra that we will not destruct
}

// destructuring assignment on multiple lines for clarity
let {
  size: { // put size here
    width,
    height
  },
  items: [item1, item2], // assign items here
  title = 'Menu' // not present in the object (default value is used)
} = options

console.log(title)  // Menu
console.log(width)  // 100
console.log(height) // 200
console.log(item1)  // Cake
console.log(item2)  // Donut



2.3 字符串解构

可以当做是数组的结构

let str = 'imooc'

let [a, b, c, d, e] = str

console.log(a, b, c, d, e)



2.4 解构原理

解构是一种语法糖,其实内在是针对可迭代对象的Iterator接口,通过遍历器按顺序获取对应的值进行赋值。 String、Array、Map、Set等原生数据结构都是可迭代对象,可以通过for of循环遍历它。

原生object对象是默认没有部署Iterator接口,即object不是一个可迭代对象。因为遍历时,不知道到底哪个属性先遍历,哪个属性后遍历,需要开发者手动指定。实际上对象被解构时,会被当作Map进行解构。

对象的解构与数组有一个重要的不同。数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。



3. Array

在 ES6 中新增了很多实用的原生 API,方便开发者对 Array 的操控性更强,如 for…of、from、of、fill、find、findIndex等



3.1 for…of

for (let val of [1, 2, 3]) {
    console.log(val);
}
// 1,2,3
  1. for…of是支持 break、continue、return的,所以在功能上非常贴近原生的 for
  2. for…of可以用来遍历数组,map,set以及其它实现了Iterator接口的对象
  3. 不能直接遍历对象,需要实现Iterator接口才可以遍历对象
  4. 支持自定义遍历顺序,只需要将要遍历的数据实现Iterator接口即可



3.2 Array.from()

  • 将伪数组(Array-Like)转化为数组。
let arrLike = {
    0: 'a',
    1: 'b',
    2: 'c',
    length: 3
}

console.log(Array.from(arrLike)); // ["a", "b", "c"]
  • 伪数组

伪数组具备两个特征,

  1. 按索引方式储存数据
  2. 具有length属性
const a = {a: 1, b: 2, c: 3};
console.log(Array.from(a)); // []

const b = new Set([1, 2, 3]);
console.log(Array.from(b)); // [1, 2, 3]

const c = new Map([['a', 1], ['b', 2], ['c', 3]]);
console.log(Array.from(c)); // [['a', 1], ['b', 2], ['c', 3]]

const d = {a: 1, b: 2, c: 3, length: 4};
console.log(Array.from(d)); // [undefined, undefined, undefined, undefined]


const e = {a: 1, b: 2, c: 3, length: 2};
console.log(Array.from(e)); // [undefined, undefined]

const f = {length: 2};
console.log(Array.from(f)); // [undefined, undefined]

  • 语法

Array.from(arrayLike[, mapFn[, thisArg]])

参数 含义 必选
arrayLike 想要转换成数组的伪数组对象或可迭代对象 Y
mapFn 如果指定了该参数,新数组中的每个元素会执行该回调函数 N
thisArg 可选参数,执行回调函数 mapFn 时 this 对象 N

初始化一个长度为 5 的数组,每个数组元素默认为 1

console.log(Array.from({length: 5}, () => 1)); // [1, 1, 1, 1, 1]



3.3 Array.of()

Array.of() 和 Array 构造函数之间的区别在于处理整数参数:Array.of(7) 创建一个具有单个元素 7 的数组,而 Array(7) 创建一个长度为7的空数组(注意:这是指一个有7个空位(empty)的数组,而不是由7个undefined组成的数组)。

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

Array(7); // [ , , , , , , ]
Array(1, 2, 3); // [1, 2, 3]



3.4 Array.prototype.fill()

fill() 方法用一个固定值填充一个数组中从起始索引到终止索引内的全部元素。不包括终止索引。

let array = [1, 2, 3, 4]
array.fill(0, 1, 2)
// [1,0,3,4]

初始化一个长度为 5 的数组,每个数组元素默认为 1

Array(5).fill(1)
// [1,1,1,1,1]



3.5 Array.prototype.find()

find() 方法返回数组中满足提供的测试函数的第一个元素的值,否则返回 undefined。



3.6 Array.prototype.findIndex()

findIndex()方法返回数组中满足提供的测试函数的第一个元素的索引。否则返回-1。其实这个和 find() 是成对的,不同的是它返回的是索引而不是值。



3.7 Array.prototype.copyWithin()

在当前数组内部,将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组。也就是说,使用这个方法,会修改当前数组。

arr.copyWithin(target, start = 0, end = this.length)

  • target: 从该位置开始替换数据。如果为负值,表示倒数
  • start: 从该位置开始读取数据,默认为 0。如果为负值,表示从末尾开始计算
  • end: 到该位置前停止读取数据,默认等于数组长度。如果为负值,表示从末尾开始计算
let arr = [1, 2, 3, 4, 5]
console.log(arr.copyWithin(1, 3)) // [1, 4, 5, 4, 5]



4. Function



4.1 参数的默认值

  • 基本用法
function test(a, b = 1){
  console.log(a, b);
}
test(); // undefined 1 ;函数参数是从左到右解析,如果没有默认值会被解析成 undefined
  • 如果我们想让具体某个参数使用默认值,我们可以使用 undefined 进行赋值
function test(a, b = 1, c){
  console.log(a, b, c);
}
test(1, undefined, 2); // 1 1 2
  • 支持参数的逻辑运算进行赋值
function f(x, y = 7, z = x + y) {
    return z * 0.5
}

console.log(f(1, 7)) // 4



4.2 Rest参数

不是很确定参数有多少个可以使用Rest参数

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

当然,Rest Parameter 也可以和其他参数一起来用,比如:

function sum(base, ...nums) {
    let num = base
    nums.forEach(function(item) {
        num += item * 1
    })
    return num
}

console.log(sum(30, 1, 2, 3)) // 36
console.log(sum(30, 1, 2, 3, 4)) // 40

arguments 不是数组,所以不能直接使用数组的原生 API 如 forEach,而 Rest Parameter 是数组,可以直接使用数组的原生 API。



4.3 扩展运算符

Spread Operator 和 Rest Parameter 是形似但相反意义的操作符,简单的来说 Rest Parameter 是把不定的参数“收敛”到数组,而 Spread Operator 是把固定的数组内容“打散”到对应的参数。示例如下:

function sum(x = 1, y = 2, z = 3) {
    return x + y + z
}

console.log(sum(...[4])) // 9
console.log(sum(...[4, 5])) // 12
console.log(sum(...[4, 5, 6])) // 15



4.4 length属性

函数指定了默认值以后,函数的length属性,将返回没有指定默认值的参数个数。

function foo(x = 1, y = 2, z = 3) {
  console.log(arguments.length);
}
console.log(foo.length); // 0
foo(1); // 1
foo(2, 3); // 2
foo(2, 3, 4); // 3
foo(1, 2, 3, 4); // 4

function test(a, b = 1, c){}
console.log(test.length); // 1

function test1(a , b, c = 1) {}
console.log(test1.length); // 2
  • 函数的length属性: 统计第一个默认参数前面的变量数
  • arguments.length: 统计传入参数的个数



4.5 name属性

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

function foo() {}

foo.name // "foo"



4.6 箭头函数

  • 箭头函数的形式
// 不带参数
let hello = () => {
    console.log('say hello')
}

// 带参数
let hello = (name) => {
    console.log('say hello', name)
}

// 带一个参数
let hello = name => {
    console.log('say hello', name)
}

// 如果返回值是表达式: 可以省略 return 和 {}
let pow = x => x * x

// 如果返回值是字面量对象: 一定要用小括号包起来
let persion = name => ({age: 20, name: name, addr: 'test'});
  • 箭头函数中this指向定义时所在的对象,而不是调用时所在的对象
let foo = {
    name: 'es',
    say: () => {
        console.log(this.name, this)
    }
}
console.log(foo.say()) // undefined window
  • 箭头函数不可以当作构造函数
let a = () => {};
new a(); // Uncaught TypeError: a is not a constructor
  • 箭头函数不可以使用arguments对象
let test = () => {
  console.log(arguments.length);
}
test(); // VM36:2 Uncaught ReferenceError: arguments is not defined
  • 箭头函数的length 和 name 属性和普通函数相同



5. Object



5.1 属性简洁表示法

  • 在 ES6 之前 Object 的属性必须是 key-value 形式,如下:
  let name = 'xiecheng'
  let age = 34
  let obj = {
      name: name,
      age: age,
      study: function() {
          console.log(this.name + '正在学习')
      }
  }
  • 在 ES6 之后是可以用简写的形式来表达:
  let name = 'xiecheng'
  let age = 34
  let obj = {
      name,
      age,
      study() {
          console.log(this.name + '正在学习')
      }
  }



5.2 属性名表达式

在 ES6 可以直接用变量或者表达式来定义Object的 key。

  let s = 'school'
  let obj = {
      foo: 'bar',
      [s]: 'imooc'
  }



5.3 Object.is

判断两个对象是否相等。

let obj1 = { // new Object()
    name: 'xiecheng',
    age: 34
}

let obj2 = { // new Object()
    name: 'xiecheng',
    age: 34
}
console.log(obj1 == obj2) // false

console.log(Object.is(obj1, obj2)) // false

let obj2 = obj1

console.log(Object.is(obj1, obj2)) // true



5.4 Object.assign()

Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象,它将返回目标对象。

const target = {
    a: 1,
    b: 2
}
const source = {
    b: 4,
    c: 5
}

const returnedTarget = Object.assign(target, source)

console.log(target)
// expected output: Object { a: 1, b: 4, c: 5 }

console.log(returnedTarget)
// expected output: Object { a: 1, b: 4, c: 5 }
  • 如果是多个相同属性的会被后边的源对象的属相覆盖。
  • 如果目的对象不是对象,则会自动转换为对象
  • Object.assign()对于引用数据类型属于浅拷贝



5.5 对象的遍历方式

  • for…in不能够用于遍历Array,for…in的作用是用于遍历对象的。
for (let key in obj) {
    console.log(key, obj[key])
}
  • Object.keys()用于返回对象所有key组成的数组。
Object.keys(obj).forEach(key => {
    console.log(key, obj[key])
})
  • Object.getOwnPropertyNames()用于返回对象所有key组成的数组。
Object.getOwnPropertyNames(obj).forEach(key => {
    console.log(key, obj[key])
})
  • Reflect.ownKeys()用于返回对象所有key组成的数组。
Reflect.ownKeys(obj).forEach(key => {
    console.log(key, obj[key])
})



6. Class



6.1 声明类

class Animal {
    constructor(type) {
        this.type = type
    }
    walk() {
        console.log( `I am walking` )
    }
}
let dog = new Animal('dog')
let monkey = new Animal('monkey')
  • class 的方式是 function 方式的语法糖。
console.log(dog.hasOwnProperty('type')) //true

console.log(typeof Animal) //function

console.log(Animal.prototype)
// {constructor: ƒ, walk: ƒ}
//   constructor: class Animal
//   walk: ƒ walk()
//   __proto__:
//   constructor: ƒ Object()
//   hasOwnProperty: ƒ hasOwnProperty()
//   isPrototypeOf: ƒ isPrototypeOf()
//   propertyIsEnumerable: ƒ propertyIsEnumerable()
//   toLocaleString: ƒ toLocaleString()
//   toString: ƒ toString()
//   valueOf: ƒ valueOf()
//   __defineGetter__: ƒ __defineGetter__()
//   __defineSetter__: ƒ __defineSetter__()
//   __lookupGetter__: ƒ __lookupGetter__()
//   __lookupSetter__: ƒ __lookupSetter__()
//   get __proto__: ƒ __proto__()
//   set __proto__: ƒ __proto__()



6.2 Setters & Getters

对于类中的属性,可以直接在 constructor 中通过 this 直接定义,还可以直接在类的顶层来定义:

class Animal {
    constructor(type, age) {
        this.type = type
        this._age = age
    }
    get age() {
        return this._age
    }
    set age(val) {
        this._age = val
    }
}



6.3 静态方法

在 ES6 中使用 static 的标记是不是静态方法,代码如下:

class Animal {
    constructor(type) {
        this.type = type
    }
    walk() {
        console.log( `I am walking` )
    }
    static eat() {
        console.log( `I am eating` )
    }
}



6.4 继承

class Animal {
    constructor(type) {
        this.type = type
    }
    walk() {
        console.log( `I am walking` )
    }
    static eat() {
        console.log( `I am eating` )
    }
}

class Dog extends Animal {
  constructor () {
    super('dog')
  }
  run () {
    console.log('I can run')
  }
}



7. Symbol

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



7.1 引入原因

  1. 对象的属性使用字符串指定,但是很可能会跟原有的属性名一致,或者后来者造成属性覆盖。以往通常是用 字符串+Date毫秒(或随机数)。
  2. 在使用第三方库的时候,想给第三方库扩展属性,往往担心属性覆盖



7.2 声明方式

let s = Symbol()

typeof s
// "symbol"

变量s就是一个独一无二的值。typeof的结果说明s是 Symbol 数据类型。

既然是独一无二的,那么两个Symbol()就一定是不相等的:

let s1 = Symbol()
let s2 = Symbol()
console.log(s1)
console.log(s2)
console.log(s1 === s2) // false

Symbol函数可以接受一个字符串作为参数,表示对 Symbol 实例的描述,主要是为了在控制台显示,或者转为字符串时,比较容易区分。

值得注意的是即使描述相同,生成的Symbol也不会相同。

let s1 = Symbol('foo')
let s2 = Symbol('foo')
console.log(s1)
console.log(s2)
console.log(s1 === s2) // false



7.3 Symbol.for()

Symbol.for() 接受一个字符串作为参数,然后搜索有没有以该参数作为名称的 Symbol 值。如果有,就返回这个 Symbol 值,否则就新建一个以该字符串为名称的 Symbol 值,并将其注册到全局。

let s1 = Symbol.for('foo')
let s2 = Symbol.for('foo')
console.log(s1 === s2) // true



7.4 Symbol.for()与Symbol()的区别

  1. Symbol.for()与Symbol()这两种写法,都会生成新的 Symbol。它们的区别是,前者会被登记在全局环境中供搜索,后者不会。
  2. Symbol.for()不会每次调用就返回一个新的 Symbol 类型的值,而是会先检查给定的key是否已经存在,如果不存在才会新建一个值。
  3. Symbol.for()与Symbol()传入相同的参数,生成的Symbol也是不同的
let s1 = Symbol('foo');
let s2 = Symbol.for('foo');
console.log(s1 === s2); // false



7.5 Symbol.keyFor()

Symbol.keyFor()方法返回一个已登记的 Symbol 类型值的key。

const s1 = Symbol('foo')
console.log(Symbol.keyFor(s1)) // undefined

const s2 = Symbol.for('foo')
console.log(Symbol.keyFor(s2)) // foo



7.6 作为属性名

由于每一个 Symbol 值都是不相等的,这意味着 Symbol 值可以作为标识符,用于对象的属性名,就能保证不会出现同名的属性。这对于一个对象由多个模块构成的情况非常有用,能防止某一个键被不小心改写或覆盖。

const stu1 = Symbol('李四')
const stu2 = Symbol('李四')
const grade = {
    [stu1]: {
        address: 'yyy',
        tel: '222'
    },
    [stu2]: {
        address: 'zzz',
        tel: '333'
    },
}
console.log(grade)
console.log(grade[stu1])
console.log(grade[stu2])



7.7 属性遍历

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

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

const sym = Symbol('test')

let obj = {
  a: 1,
  [sym]: 2
}
// a
for (let key in obj) {
    console.log(key)
}

// a
for (let key of Object.keys(obj)) {
    console.log(key)
}

// Symbol(test)
for (let key of Object.getOwnPropertySymbols(obj)) {
    console.log(key)
}

// a Symbol(test)
for (let key of Reflect.ownKeys(obj)) {
    console.log(key)
}



7.8 消除魔术字符串

魔术字符串指的是,在代码之中多次出现、与代码形成强耦合的某一个具体的字符串或者数值。风格良好的代码,应该尽量消除魔术字符串,改由含义清晰的变量代替。

function getArea(shape) {
    let area = 0
    switch (shape) {
        case 'Triangle':
            area = 1
            break
        case 'Circle':
            area = 2
            break
    }
    return area
}
console.log(getArea('Triangle'))
const shapeType = {
    triangle: Symbol(),
    circle: Symbol()
}

function getArea(shape) {
    let area = 0
    switch (shape) {
        case shapeType.triangle:
            area = 1
            break
        case shapeType.circle:
            area = 2
            break
    }
    return area
}
console.log(getArea(shapeType.triangle))



8. String



8.1 Unicode 表示法

ES6 加强了对 Unicode 的支持,允许采用\uxxxx形式表示一个字符,其中xxxx表示字符的 Unicode 码点。

"\u0061"
// "a"

但是,这种表示法只限于码点在

\u0000~\uFFFF

之间的字符。超出这个范围的字符,必须用两个双字节的形式表示。

"\uD842\uDFB7"
// "𠮷"

"\u20BB7"
// " 7"



8.2 遍历接口

ES6 为字符串添加了遍历器接口,使得字符串可以被for…of循环遍历。

for (let codePoint of 'foo') {
  console.log(codePoint)
}
// "f"
// "o"
// "o"



8.3 模板字符串

  • 使用场景

为了解决下面问题引入了模板字符串:

  1. 字符串很长要换行
  2. 字符串中有变量或者表达式
  3. 字符串中有逻辑运算
`string text` 

`string text line 1
 string text line 2`

`string text ${expression} string text` 

这里的符号是反引号,也就是数字键 1 左边的键,不是单引号或者双引号

  • 标签模板

模板字符串的功能,不仅仅是上面这些。它可以紧跟在一个函数名后面,该函数将被调用来处理这个模板字符串。这被称为“标签模板”功能(tagged template)。

alert`hello`
// 等同于
alert(['hello'])

标签模板其实不是模板,而是函数调用的一种特殊形式。“标签”指的就是函数,紧跟在后面的模板字符串就是它的参数。

但是,如果模板字符里面有变量,就不是简单的调用了,而是会将模板字符串先处理成多个参数,再调用函数。

let a = 5;
let b = 10;

tag`Hello ${ a + b } world ${ a * b }`;
// 等同于
tag(['Hello ', ' world ', ''], 15, 50);

上面代码中,模板字符串前面有一个标识名tag,它是一个函数。整个表达式的返回值,就是tag函数处理模板字符串后的返回值。

函数tag依次会接收到多个参数。

function tag(stringArr, value1, value2){
  // ...
}

// 等同于

function tag(stringArr, ...values){
  // ...
}

相当于通过${}将字符串切开,

tag函数所有参数的实际值如下。

  1. 第一个参数:[‘Hello ‘, ’ world ‘, ‘’]
  2. 第二个参数: 15
  3. 第三个参数:50



8.4 String.prototype.fromCodePoint()

用于从 Unicode 码点返回对应字符,并且可以识别大于0xFFFF的字符。

// ES5
console.log(String.fromCharCode(0x20BB7))
// ஷ

// ES6
console.log(String.fromCodePoint(0x20BB7))
// 𠮷



8.5 String.prototype.includes()

ES5中可以使用indexOf方法来判断一个字符串是否包含在另一个字符串中,indexOf返回出现的下标位置,如果不存在则返回-1。

const str = 'isxxx';

console.log(str.indexOf('is')); // 0

ES6提供了includes方法来判断一个字符串是否包含在另一个字符串中,返回boolean类型的值。

const str = 'isxxx';

console.log(str.includes('xx')); // true



8.6 String.prototype.startsWith()

判断参数字符串是否在原字符串的头部, 返回boolean类型的值。

const str = 'isxxx';

console.log(str.startsWith('is')); // true



8.7 String.prototype.endsWith()

判断参数字符串是否在原字符串的尾部, 返回boolean类型的值。

const str = 'isxxx';

console.log(str.endsWith('xxx')); // true



8.8 String.prototype.repeat()

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

const str = 'isxxx';

const newStr = str.repeat(10);

console.log(newStr); // isxxxisxxxisxxxisxxxisxxxisxxxisxxxisxxxisxxxisxxx



9. Set



9.1 基本语法

  • 生成Set实例

初始化的参数必须是可遍历的,可以是数组或者自定义遍历的数据结构。

let a = new Set();
let b = new Set([1, 2, 3]);
  • 添加数据

Set 数据结构不允许数据重复,所以添加重复的数据是无效的

let a = new Set();
a.add(1);
a.add(2);
a.add(3).add(4).add(5);
  • 删除数据

删除数据分两种,一种是删除指定的数据,一种是删除全部数据。

let a = new Set();
a.add(1);
a.add(2);
a.add(3).add(4).add(5);

// 删除指定的数据
a.delete(1); // true

// 删除全部数据
a.clear();
  • 统计数据

Set 可以快速进行统计数据,如数据是否存在、数据的总数。

// 判断是否包含数据项,返回 true 或 false
s.has('hello') // true
// 计算数据项总数
s.size // 2
  • 数组去重
let arr = [1, 2, 3, 4, 2, 3];
let s = new Set(arr);
console.log(Array.from(s)); // [1, 2, 3, 4]



9.2 遍历方式

  • keys():返回键名的遍历器
  • values():返回键值的遍历器
  • entries():返回键值对的遍历器
  • forEach():使用回调函数遍历每个成员
  • for…of:可以直接遍历每个成员
let s = new Set(["hello", "goodbye"])
console.log(s.keys()) // SetIterator {"hello", "goodbye"}
console.log(s.values()) // SetIterator {"hello", "goodbye"}
console.log(s.entries()) // SetIterator {"hello" => "hello", "goodbye" => "goodbye"}
s.forEach(item => {
    console.log(item) // hello // goodbye
})

for (let item of s) {
    console.log(item)
}

for (let item of s.keys()) {
    console.log(item)
}

for (let item of s.values()) {
    console.log(item)
}

for (let item of s.entries()) {
    console.log(item[0], item[1])
}



9.3 WeakSet

WeakSet 结构与 Set 类似,也是不重复的值的集合。但是,它与 Set 有两个区别。

  • WeakSet 的成员只能是对象,而不能是其他类型的值。
  • WeakSet 没有size属性,没有办法遍历它的成员。
  • WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet 之中。



10. Map



10.1 基本语法

  • 实例化
let map = new Map([iterable])

Iterable 可以是一个数组或者其他 iterable 对象,其元素为键值对(两个元素的数组,例如: [[ 1, ‘one’ ], [ 2, ‘two’ ]])。 每个键值对都会添加到新的 Map。null 会被当做 undefined。

  • 添加数据

map中的key可以是任意数据类型。

let keyObj = {}
let keyFunc = function() {}
let keyString = 'a string'

// 添加键
map.set(keyString, "和键'a string'关联的值")
map.set(keyObj, '和键keyObj关联的值')
map.set(keyFunc, '和键keyFunc关联的值')
  • 删除数据
// 删除指定的数据
map.delete(keyObj)
// 删除所有数据
map.clear()
  • 统计数据
// 统计所有 key-value 的总数
console.log(map.size) //2
// 判断是否有 key-value
console.log(map.has(keyObj)) // true
  • 查询数据

get() 方法返回某个 Map 对象中的一个指定元素

console.log(map.get(keyObj)) // 和键keyObj关联的值



10.2 遍历方式

  • keys() 返回一个新的 Iterator 对象。它包含按照顺序插入 Map 对象中每个元素的 key 值
  • values() 方法返回一个新的 Iterator 对象。它包含按顺序插入Map对象中每个元素的 value 值
  • entries() 方法返回一个新的包含 [key, value] 对的 Iterator ? 对象,返回的迭代器的迭代顺序与 Map 对象的插入顺序相同
  • forEach() 方法将会以插入顺序对 Map 对象中的每一个键值对执行一次参数中提供的回调函数
  • for…of 可以直接遍历每个成员
map.forEach((value, key) => console.log(value, key))

for (let [key, value] of map) {
    console.log(key, value)
}

for (let key of map.keys()) {
    console.log(key)
}

for (let value of map.values()) {
    console.log(value)
}

for (let [key, value] of map.entries()) {
    console.log(key, value)
}



10.3 Map vs Object

  • 键的类型

一个Object的键只能是字符串或者 Symbols,但一个 Map 的键可以是任意值,包括函数、对象、基本类型。

  • 键的顺序

Map 中的键值是有序的,而添加到对象中的键则不是。因此,当对它进行遍历时,Map 对象是按插入的顺序返回键值。

  • 键值对的统计

你可以通过 size 属性直接获取一个 Map 的键值对个数,而 Object 的键值对个数只能手动计算。

  • 键值对的遍历

Map 可直接进行迭代,而 Object 的迭代需要先获取它的键数组,然后再进行迭代。

  • 性能

Map 在涉及频繁增删键值对的场景下会有些性能优势。



10.4 WeekMap

WeakMap与Map的区别有两点。

  • WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键名。
const map = new WeakMap()
map.set(1, 2)
// TypeError: 1 is not an object!
map.set(Symbol(), 2)
// TypeError: Invalid value used as weak map key
map.set(null, 2)
// TypeError: Invalid value used as weak map key
  • WeakMap的键名所指向的对象,不计入垃圾回收机制。



11. RegExp



11.1 y修饰符

ES6为正则表达式添加了y修饰符,叫做“粘连”(sticky)修饰符。

y修饰符的作用与g修饰符类似,也是全局匹配,后一次匹配都从上一次匹配成功的下一个位置开始。不同之处在于,g修饰符只要剩余位置中存在匹配就可,而y修饰符确保匹配必须从剩余的第一个位置开始,这也就是“粘连”的涵义。

const s = 'aaa_aa_a'
const r1 = /a+/g
const r2 = /a+/y

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

r1.exec(s) // ["aa"]
r2.exec(s) // null

上面代码有两个正则表达式,一个使用g修饰符,另一个使用y修饰符。这两个正则表达式各执行了两次,第一次执行的时候,两者行为相同,剩余字符串都是_aa_a。由于g修饰没有位置要求,所以第二次执行会返回结果,而y修饰符要求匹配必须从头部开始,所以返回null。

如果改一下正则表达式,保证每次都能头部匹配,y修饰符就会返回结果了。

const s = 'aaa_aa_a'
const r = /a+_/y

r.exec(s) // ["aaa_"]
r.exec(s) // ["aa_"]

上面代码每次匹配,都是从剩余字符串的头部开始。

进一步说,y修饰符号隐含了头部匹配的标志^。

const reg = /b/y
reg.exec('aba')
// null
console.log(reg.lastIndex)



11.2 u修饰符

  • i:表示不区分大小写(case-insensitive)模式,即在确定匹配项时忽略模式与字符串的大小写;
  • g:表示全局(global)模式,即模式将被应用于所有字符串,而非在发现第一个匹配项时立即

    停止;
  • m: 表示多行(multiline)模式,即在到达一行文本末尾时还会继续查找下一行中是否存在与模

    式匹配的项。

ES6为正则表达式添加了u修饰符,含义为“Unicode模式”,用来正确处理大于 \uFFFF 的Unicode字符。也就是说,会正确处理四个字节的UTF-16编码。

  1. 点字符

点(.)字符在正则表达式中,含义是除了换行符以外的任意单个字符。对于码点大于 0xFFFF 的 Unicode 字符,点字符不能识别,必须加上u修饰符。

let s = '𠮷';

/^.$/.test(s); // false

/^.$/u.test(s); // true

上面代码表示,如果不添加u修饰符,正则表达式就会认为字符串为两个字符,从而匹配失败。

  1. Unicode字符表示法

ES6新增了使用大括号表示Unicode字符,这种表示法在正则表达式中必须加上u修饰符,才能识别。

/\u{61}/.test('a') // false

/\u{61}/u.test('a') // true

/\u{20BB7}/u.test('𠮷') // true

上面代码表示,如果不加u修饰符,正则表达式无法识别\u{61}这种表示法,只会认为这匹配61个连续的u。

  1. 量词

使用u修饰符后,所有量词都会正确识别码点大于0xFFFF的Unicode字符

/a{2}/.test('aa'); // true
/a{2}/u.test('aa'); // true

/𠮷{2}/.test('𠮷𠮷'); // false
/𠮷{2}/u.test('𠮷𠮷'); // true
  1. 预定义模式

u修饰符也影响到预定义模式,能否正确识别码点大于0xFFFF的Unicode字符。

/^\S$/.test('𠮷'); // false
/^\S$/u.test('𠮷'); // true

上面代码的\S是预定义模式,匹配所有不是空格的字符。只有加了u修饰符,它才能正确匹配码点大于0xFFFF的Unicode字符。

利用这一点,可以写出一个正确返回字符串长度的函数。

function codePointLength(text) {
    const result = text.match(/[\s\S]/gu)
    return result ? result.length : 0
}

const s = '𠮷𠮷'

s.length // 4
codePointLength(s) // 2
  1. i修饰符

有些Unicode字符的编码不同,但是字型很相近,比如,\u004B与\u212A都是大写的K。

/[a-z]/i.test('\u212A'); // false
/[a-z]/iu.test('\u212A'); // false



12. Number



12.1 二进制与八进制

ES6 提供了二进制和八进制数值的新的写法,分别用前缀0b(或0B)和0o(或0O)表示。

const a = 0B0101
console.log(a)

const b = 0O777
console.log(b)



12.2 Number.isFinite()

  • Infinity 是表示正无穷大的数值。
  • -Infinity 是表示负无穷大的数值。

用来检查一个数值是否为有限的(finite),即不是Infinity。

Number.isFinite(15) // true
Number.isFinite(0.8) // true
Number.isFinite(NaN) // false
Number.isFinite(Infinity) // false
Number.isFinite(-Infinity) // false
Number.isFinite('foo') // false
Number.isFinite('15') // false
Number.isFinite(true) // false



12.3 Number.isNaN()

NaN 属性是代表非数字值的特殊值。该属性用于指示某个值不是数字。可以把 Number 对象设置为该值,来指示其不是数字值

用来检查一个值是否为NaN。

Number.isNaN(NaN) // true
Number.isNaN(15) // false
Number.isNaN('15') // false
Number.isNaN(true) // false
Number.isNaN(9 / NaN) // true
Number.isNaN('true' / 0) // true
Number.isNaN('true' / 'true') // true



12.4 Number.parseInt()

ES6 将全局方法parseInt()移植到Number对象上面,行为完全保持不变。 这样做的目的,是逐步减少全局性方法,使得语言逐步模块化。

// ES5的写法
parseInt('12.34') // 12

// ES6的写法
Number.parseInt('12.34') // 12



12.5 Number.parseFloat()

ES6 将全局方法parseFloat()移植到Number对象上面,行为完全保持不变。这样做的目的,是逐步减少全局性方法,使得语言逐步模块化。

// ES5的写法
parseFloat('123.45#') // 123.45

// ES6的写法
Number.parseFloat('123.45#') // 123.45



12.6 Number.isInteger()

用来判断一个数值是否为整数。

Number.isInteger(25) // true
Number.isInteger(-25) // true
Number.isInteger(0) // true
Number.isInteger(25.1) // false

Number.isInteger() // false
Number.isInteger(null) // false
Number.isInteger('15') // false
Number.isInteger(true) // false



12.7 Number.MAX_SAFE_INTEGER

Number.MAX_SAFE_INTEGER === Math.pow(2, 53) - 1 // true

Number.MAX_SAFE_INTEGER === 9007199254740991 // true



12.8 Number.MIN_SAFE_INTEGER

Number.MIN_SAFE_INTEGER === -Number.MAX_SAFE_INTEGER // true

Number.MIN_SAFE_INTEGER === -9007199254740991 // true



12.9 Number.isSafeInteger()

JavaScript 能够准确表示的整数范围在-2

53到2

53之间(不含两个端点),超过这个范围,无法精确表示这个值。

Math.pow(2, 53) // 9007199254740992

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

Number.isSafeInteger(Math.pow(2, 53)) // false



13. Math



13.1 Math.trunc()

方法用于去除一个数的小数部分,返回整数部分。

console.log(Math.trunc(5.5)) // 5
console.log(Math.trunc(-5.5)) // -5
console.log(Math.trunc(true)) // 1
console.log(Math.trunc(false)) // 0
console.log(Math.trunc(NaN)) // NaN
console.log(Math.trunc(undefined)) // NaN
console.log(Math.trunc()) // NaN



13.2 Math.sign()

方法用来判断一个数到底是正数、负数、还是零。对于非数值,会先将其转换为数值。

它会返回五种值。

  • 参数为正数,返回+1
  • 参数为负数,返回-1
  • 参数为 0,返回0
  • 参数为-0,返回-0
  • 其他值,返回NaN
console.log(Math.sign(5)) // 1
console.log(Math.sign(-5)) // -1
console.log(Math.sign(0)) // 0
console.log(Math.sign(NaN)) // NaN
console.log(Math.sign(true)) // 1
console.log(Math.sign(false)) // 0
console.log(Math.sign(5.2)) // 1
console.log(Math.sign(-5.2)) // -1



13.3 Math.cbrt()

方法用于计算一个数的立方根。

console.log(Math.cbrt(8)) // 2

console.log(Math.cbrt('imooc')) // NaN



14. Proxy

proxy在目标对象的外层搭建了一层拦截,外界对目标对象的某些操作,必须通过这层拦截

es2015出现了Proxy 代理器 Vue3.0中已经使用了proxy实现内部数据响应

代理可以理解为门卫,不管是要进去,还是要出去,都必须要经过他

proxy可以看做是 defineProperty 的升级版。



14.1 基本语法

let p = new Proxy(target, handler)

参数 含义
target 用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)
handler 一个对象,其属性是当执行一个操作时定义代理的行为的函数



14.2 Proxy支持拦截的操作

handler方法 拦截操作
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)



14.3 样例

  1. 监控get & set
const person = {
    name: "jack",
    age: 18,
};

const personProxy = new Proxy(person, {
    get(target, property) {
        // 判断代理目标对象中是否存在要访问的属性
        const re = target[property] ? target[property] : "没有定义";
        return re;
    },
    set(target, property, value) {
        // 内部可以做数据校验,检验age是不是整数
        if (property === "age") {
            if (!Number.isInteger(value)) {
                throw new TypeError(`${value} is not an int`);
            }
        }
        target[property] = value;
    },
});
// 通过代理对象写入sex属性和值
personProxy.sex = "man";
personProxy.age = 11;
console.log(personProxy.name); // jack
console.log(personProxy.sex); // man
console.log(personProxy.age); // 11
console.log(personProxy.address); // 没有定义
  1. deleteProperty 监控删除对象内某个属性
const person = {
    name: "jack",
    age: 18,
};
const proxy = new Proxy(person, {
    deleteProperty(target, property) {
        console.log("delete", property);
        // 内部这里不删除外部操作就算使用delete也是无法删除的
        delete target[property];
    },
});
// 外部操作,内部监控
delete proxy.age;
console.log(proxy); // {name: "jack"}
  1. proxy对数组的监视
const list = [];
const listProxy = new Proxy(list, {
    set(target, property, value) {
        console.log("set", property, value);
        target[property] = value;
        // 对象不return不会报错,数组监视set不return会报错TypeError:
        return true; // 表示设置成功
    },
});
listProxy.push(100);
// set 0 100
// set length 1



15. Reflect



15.1 设计目的

Reflect提供了统一的对象操作API,内部封装了一系列对对象的底层操作。esm的想法是逐步收编Object上的方法和其它对象的操作方法,如:Object.keys,in,delete。将这些对象的操作都收编到Reflect上面,这就是Reflect的意义。

与大多数全局对象不同,Reflect没有构造函数。你不能将其与一个new运算符一起使用,或者将Reflect对象作为一个函数来调用。Reflect的所有属性和方法都是静态的(就像Math对象)



15.2 常用方法

Reflect成员方法就是Proxy处理对象的默认实现

handler ⽅法 默认调⽤
get Reflect.get()
set Reflect.set()
has Reflect.has()
deleteProperty Reflect.delete()
getProperty Reflect.getPrototypeOf()
setProperty Reflect.setPrototypeOf()
isExtensible Reflect.isExtensible()
preventExtensions Reflect.preventExtensions()
getOwnPropertyDescriptor Reflect.getOwnPropertyDescriptor()
defineProperty Reflect.defineProperty()
ownKeys Reflect.ownKeys()
apply Reflect.apply()
construct Reflect.construct()



16. Promise

见博主Promise博文



17. Iterator

见博主Iterator博文



18. Generator

Generator 函数提供了一种异步编程解决方案,与Promise相比,它更加扁平化,贴近于我们的阅读习惯。

function* fn() {
    console.log("aaa", 1);
    return 123;
}
const g = fn();
console.log(g, 2); // Object [Generator] {}
// 生成器对象也实现了迭代器接口
console.log(g.next(), 3);
  • 生成器函数要搭配关键词yield
  • 生成器函数会返回一个生成器对象
  • 调用这个对象的next方法才会让这个函数的函数体开始执行
  • 遇到yield关键词函数执行就会暂停下来,yield后面的值会作为next的结果返回
function* foo() {
    console.log(1111);
    yield 100;
    console.log(2222);
    yield 200;
    console.log(3333);
    yield 300;
}
const generator = foo();
console.log(generator.next());
console.log(generator.next());
console.log(generator.next());

实现一个简单的发号器

function* creatIdMaker() {
    let id = 1;
    while (true) {
        yield id++;
    }
}
const inMaker = creatIdMaker();
console.log(inMaker.next().value); // 1
console.log(inMaker.next().value); // 2
console.log(inMaker.next().value); // 3

使用generator函数实现iterator方法

const todoS = {
    life: ["吃饭", "睡觉", "敲代码"],
    work: ["喝茶", "吃水果"],
    // each方法遍历对象内两个数组的值,不使用迭代器时
    each: function (callback) {
        const arr = [...this.life, ...this.work];
        for (const item of arr) {
            callback(item);
        }
    },
    // 使用生成器实现iterator
    [Symbol.iterator]: function* () {
        const arr = [...this.life, ...this.work];
        for (const item of arr) {
            yield item;
        }
    },
};
for (let item of todoS) {
    console.log(item);
}



19. Module

见博主Module博文



二、 ECMAScript2016(ES7)



1. Array.prototype.includes()

  • 基本用法
const arr = ['es6', 'es7', 'es8']
console.log(arr.includes('es6')) // true
console.log(arr.includes('es9')) // false
  • 接收俩个参数

要搜索的值和搜索的开始索引。第二个参数可选。从该索引处开始查找 searchElement。如果为负值,

const arr = ['es6', 'es7', 'es8']
console.log(arr.includes('es7', 1)) // true
console.log(arr.includes('es7', 2)) // false
console.log(arr.includes('es7', -1)) // false
console.log(arr.includes('es7', -2)) // true
  • 只能判断简单类型的数据,对于复杂类型的数据,比如对象类型的数组,二维数组,这些是无法判断的.
const arr = [1, [2, 3], 4]
arr.includes([2, 3]) //false
  • 可以查询NaN

js中 NaN === NaN 的结果是false, indexOf()也是这样处理的,但是includes()不是这样的。

const demo = [1, NaN, 2, 3]
demo.indexOf(NaN) //-1
demo.includes(NaN) //true



2. 幂运算符**

Math.pow() 函数返回基数(base)的指数(exponent)次幂。

console.log(Math.pow(2, 10)) // 1024

在 ES7 可以这样写了:

console.log(2 ** 10) // 1024



三、 ECMAScript2017(ES8)



1. async / await

async 函数是 Generator 函数的语法糖。

async函数对 Generator 函数的改进,体现在以下四点。

  1. 内置执行器。

Generator 函数的执行必须靠执行器,所以才有了co模块,而async函数自带执行器。也就是说,async函数的执行,与普通函数一模一样,只要一行。

asyncReadFile();

上面的代码调用了asyncReadFile函数,然后它就会自动执行,输出最后结果。这完全不像 Generator 函数,需要调用next方法,或者用co模块,才能真正执行,得到最后结果。

  1. 更好的语义。

async和await,比起星号和yield,语义更清楚了。async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。

  1. 更广的适用性。

co模块约定,yield命令后面只能是 Thunk 函数或 Promise 对象,而async函数的await命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时会自动转成立即 resolved 的 Promise 对象)。

  1. 返回值是 Promise。

async函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。你可以用then方法指定下一步的操作。

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



2. Object 扩展

ES8中对象扩展补充了两个静态方法,用于遍历对象:Object.values(),Object.entries()



2.1 Object.values()

Object.values() 返回一个数组,其元素是在对象上找到的可枚举属性值。属性的顺序与通过手动循环对象的属性值所给出的顺序相同(for…in,但是for…in还会遍历原型上的属性值)。

const obj = {
    name: 'imooc',
    web: 'www.imooc.com',
    course: 'es'
}

obj.__proto__.test = 1;

console.log(Object.values(obj))
// ["imooc", "www.imooc.com", "es"]

Object.values 是在对象上找到可枚举的属性的值,所以只要这个对象是可枚举的就可以,不只是 {} 这种形式。



2.2 Object.entries()

Object.entries()方法返回一个给定对象自身可枚举属性的键值对数组,其排列与使用 for…in 循环遍历该对象时返回的顺序一致。(区别在于 for-in 循环也枚举原型链中的属性)

let grade = {
    'lilei': 98,
    'hanmei': 87
}

for (let [key, value] of grade) {
    console.log(key, value) // Uncaught TypeError: grade is not iterable
}

我们知道 Object 是不可直接遍历的,上述代码足以说明直接遍历触发了错误。如果使用 Object.entries() 则可以完成遍历任务。

let grade = {
    'lilei': 98,
    'hanmei': 87
}

for (let [k, v] of Object.entries(grade)) {
    console.log(k, v)
    // lilei 98
    // hanmei 87
}



2.3 Object.getOwnPropertyDescriptors()

Object.defineProperty相关请查看博主关于Object.defineProperty的博文。

const data = {
    Portland: '78/50',
    Dublin: '88/52',
    Lima: '58/40'
};

Object.defineProperty(data, 'Lima', {
    enumerable: false
})

console.log(Object.getOwnPropertyDescriptor(data, 'Lima'));
// {value: "58/40", writable: true, enumerable: false, configurable: true}

这个是获取对象指定属性的描述符,如果想获取对象的所有属性的描述符:

console.log(Object.getOwnPropertyDescriptors(data));



3. String 扩展

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



3.1 String.prototype.padStart()

把指定字符串填充到字符串头部,返回新字符串。

const str = 'imooc'
console.log(str.padStart(5, '*')) // imooc
console.log(str.padStart(8, 'x')) // xxximooc
console.log(str.padEnd(8, 'y')) // imoocyyy
console.log(str.padStart(8)) //    imooc



3.2 String.prototype.padEnd()

方法会用一个字符串填充当前字符串(如果需要的话则重复填充),返回填充后达到指定长度的字符串。从当前字符串的末尾(右侧)开始填充。

const str1 = 'I am learning es in imooc'
console.log(str1.padEnd(30, '.')) // I am learning es in imooc.....

const str2 = '200'
console.log(str2.padEnd(5)) // "200  "



4. 尾逗号 Trailing commas

ES8 允许函数的最后一个参数有尾逗号(Trailing comma)。

function test(
  a,
  b,
  c,
){
  // xxxxx
}



四、 ECMAScript2018(ES9)



1. for await of

异步迭代器(for-await-of):循环等待每个Promise对象变为resolved状态才进入下一步

function Gen(time) {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
          console.log('Gen ', time)
            resolve(time)
        }, time)
    })
}

async function test() {
    let arr = [Gen(2000), Gen(100), Gen(3000)]
    for await (let item of arr) {
        console.log(Date.now(), item)
    }
}

test()
// Gen  100
// Gen  2000
// 1637745454960 2000
// 1637745454960 100
// Gen  3000
// 1637745455965 3000

整个代码块都不执行,等待 arr 当前的值(Promise状态)发生变化之后,才执行代码块的内容。



2. RegExp



2.1 dotAll 模式

dotAll 模式:它让 . 名副其实。

正则表达式中,点(.)是一个特殊字符,代表任意的单个字符,但是有两个例外。一个是四个字节的 UTF-16 字符,这个可以用u修饰符解决;另一个是行终止符(line terminator character)。

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

在 ES5 中我们都是这么解决的:

console.log(/foo[^]bar/.test('foo\nbar')) // true
// or
console.log(/foo[\s\S]bar/.test('foo\nbar')) // true

那如何判断当前正则是否使用了 dotAll 模式呢?

const re = /foo.bar/s // Or, `const re = new RegExp('foo.bar', 's')` .
console.log(re.test('foo\nbar')) // true
console.log(re.dotAll) // true
console.log(re.flags) // 's'



2.2 具名组匹配

我们在写正则表达式的时候,可以把一部分用()包裹起来,被包裹起来的这部分称作“分组捕获”。

console.log('2020-05-01'.match(/(\d{4})-(\d{2})-(\d{2})/))
// ["2020-05-01", "2020", "05", "01", index: 0, input: "2020-05-01", groups: undefined]

这个正则匹配很简单,按照 match 的语法,没有使用 g 标识符,所以返回值第一个数值是正则表达式的完整匹配,接下来的第二个值到第四个值是分组匹配(2020, 05, 01)。

此外 match 返回值还有几个属性,分别是 index、input、groups。

  • index [匹配的结果的开始位置]
  • input [搜索的字符串]
  • groups [一个捕获组数组 或 undefined(如果没有定义命名捕获组)

上文中重点看下 groups 的解释,这里提到了命名捕获组的概念,如果没有定义 groups 就是 undefined。很明显,我们上述的返回值就是 undefined 间接说明没有定义命名捕获分组。那什么是命名捕获分组呢?

console.log('2020-05-01'.match(/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/))
// ["2020-05-01", "2020", "05", "01", index: 0, input: "2020-05-01", groups: {…}]

这段代码的返回值 groups 已经是 Object 了,具体的值是:

groups: {
    year: "2020",
    month: "05",
    day: "01"
}

这个 Object 的 key 就是正则表达式中定义的,也就是把捕获分组进行了命名。想获取这些捕获可以这样做:

let t = '2020-05-01'.match(/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/)
// ["2020-05-01", "2020", "05", "01", index: 0, input: "2020-05-01", groups: {…}]
console.log(t.groups.year) // 2020
console.log(t.groups.month) // 05
console.log(t.groups.day) // 01



2.3 后行断言

在 ES9 之前 JavaScript 正则只支持先行断言,不支持后行断言。简单复习下先行断言的知识:

let test = 'hello world'
console.log(test.match(/hello(?=\sworld)/))
// ["hello", index: 0, input: "hello world", groups: undefined]

这段代码要匹配后面是 world 的 hello,但是反过来就不成:

let test = 'world hello'
console.log(test.match(/hello(?=\sworld)/))
// null

比如我们想判断前面是 world 的 hello,这个代码是实现不了的。在 ES9 就支持这个后行断言了:

let test = 'world hello'
console.log(test.match(/(?<=world\s)hello/))
// ["hello", index: 6, input: "world hello", groups: undefined]

(?<…)是后行断言的符号,(?..)是先行断言的符号,然后结合 =(等于)、!(不等)、\1(捕获匹配)。



3. Object Rest & Spread

在 ES9 新增 Object 的 Rest & Spread 方法,直接看下示例:

const input = {
  a: 1,
  b: 2
}

const output = {
  ...input,
  c: 3
}

console.log(output) // {a: 1, b: 2, c: 3}

这块代码展示了 spread 语法,可以把 input 对象的数据都拓展到 output 对象,这个功能很实用。

我们再来看下 Object rest 的示例:

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

let { a, ...rest } = input

console.log(a, rest) // 1 {b: 2, c: 3}



4. Promise.prototype.finally()

指定不管最后状态如何都会执行的回调函数。

Promise.prototype.finally() 方法返回一个Promise,在promise执行结束时,无论结果是fulfilled或者是rejected,在执行then()和catch()后,都会执行finally指定的回调函数。这为指定执行完promise后,无论结果是fulfilled还是rejected都需要执行的代码提供了一种方式,避免同样的语句需要在then()和catch()中各写一次的情况。



5. 字符串扩展



五、 ECMAScript2019(ES10)



1. Object.fromEntries()

方法 Object.fromEntries() 把键值对列表转换为一个对象,这个方法是和 Object.entries() 相对的。

Object.fromEntries([
    ['foo', 1],
    ['bar', 2]
])
// {foo: 1, bar: 2}

const map = new Map()
map.set('name', 'imooc')
map.set('course', 'es')
console.log(map)

const obj = Object.fromEntries(map)
console.log(obj)
// {name: "imooc", course: "es"}



2. String 扩展



2.1 String.prototype.trimStart()

trimStart() 方法从字符串的开头删除空格,trimLeft()是此方法的别名。

String.prototype.trimLeft.name === 'trimStart'

let str = '   foo  '
console.log(str.length) // 8
str = str.trimStart()
console.log(str.length) // 5



2.2 String.prototype.trimEnd()

trimEnd() 方法从一个字符串的右端移除空白字符,trimRight 是 trimEnd 的别名。

String.prototype.trimRight.name === 'trimEnd',一定要记住

let str = '   foo  '
console.log(str.length) // 8
str = str.trimEnd()
console.log(str.length) // 6



3. Array 扩展



3.1 Array.prototype.flat()

flat() 方法会按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回。

const newArray = arr.flat(depth)

  • depth: 指定要提取嵌套数组的结构深度,默认值为 1
const numbers = [1, 2, [3, 4, [5, 6]]]

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



3.2 Array.prototype.flatMap()

flatMap() 方法首先使用映射函数映射每个元素,然后将结果压缩成一个新数组。从方法的名字上也可以看出来它包含两部分功能一个是 map,一个是 flat(深度为1)。

const numbers = [1, 2, 3]
numbers.map(x => [x * 2]) // [[2], [4], [6]]
numbers.flatMap(x => [x * 2]) // [2, 4, 6]



4. 修订 Function.prototype.toString()

函数是对象,并且每个对象都有一个 .toString() 方法,因为它最初存在于Object.prototype.toString() 上。所有对象(包括函数)都是通过基于原型的类继承从它继承的。这意味着我们以前已经有 funcion.toString() 方法了。

Function.prototype.toString() 方法返回一个表示当前函数源代码的字符串。

这意味着还将返回注释、空格和语法详细信息。

function foo() {
    // es10新特性
    console.log('imooc')
}
console.log(foo.toString())



5. 可选的Catch Binding

在 ES10 之前我们都是这样捕获异常的:

try {
    // tryCode
} catch (err) {
    // catchCode
}

在这里 err 是必须的参数,在 ES10 可以省略这个参数:

try {
    console.log('Foobar')
} catch {
    console.error('Bar')
}



6. JSON扩展



6.1 JSON superset

什么是 JSON 超集?,简而言之就是让 ECMAScript 兼容所有JSON支持的文本。 ECMAScript 曾在标准 JSON.parse 部分阐明 JSON 确为其一个子集,但由于 JSON 内容可以正常包含 U+2028行分隔符 与 U+2029段分隔符,而ECMAScript 却不行。



6.2 JSON.stringify() 增强能力

JSON.stringify 在 ES10 修复了对于一些超出范围的 Unicode 展示错误的问题。因为 JSON 都是被编码成 UTF-8,所以遇到 0xD800–0xDFFF 之内的字符会因为无法编码成 UTF-8 进而导致显示错误。在 ES10 它会用转义字符的方式来处理这部分字符而非编码的方式,这样就会正常显示了。

// \uD83D\uDE0E  emoji 多字节的一个字符
console.log(JSON.stringify('\uD83D\uDE0E')) // 笑脸

// 如果我们只去其中的一部分  \uD83D 这其实是个无效的字符串
// 之前的版本 ,这些字符将替换为特殊字符,而现在将未配对的代理代码点表示为JSON转义序列
console.log(JSON.stringify('\uD83D')) // "\ud83d"



7. Symbol 扩展



7.1 Symbol.prototype.description

我们知道,Symbol 的描述只被存储在内部的 Description ,没有直接对外暴露,我们只有调用 Symbol 的 toString() 时才可以读取这个属性:

const name = Symbol('es')
console.log(name.toString()) // Symbol(es)
console.log(name) // Symbol(es)
console.log(name === 'Symbol(es)') // false
console.log(name.toString() === 'Symbol(es)') // true

现在可以通过 description 方法获取 Symbol 的描述:

const name = Symbol('es')
console.log(name.description) // es
console.log(name.description === 'es') // true



六、 ECMAScript2020(ES11)



1. String 扩展



1.1 String.prototype.matchAll()

matchAll() 方法返回一个包含所有匹配正则表达式及分组捕获结果的迭代器

语法: str.matchAll(regexp)

const string = "I am learning JavaScript not Java.";
const re = /Java[a-z]*/gi;
let result = string.matchAll(re);
for (match of result) {
  console.log(match);
}



2. Dynamic Import



2.1 import()

import(specifier)

上面代码中,import函数的参数specifier,指定所要加载的模块的位置。import命令能够接受什么参数,import()函数就能接受什么参数,两者区别主要是后者为动态加载。

import()返回一个 Promise 对象。下面是一个例子。

const main = document.querySelector('main');

import(`./section-modules/${someVariable}.js`)
  .then(module => {
    module.loadPageInto(main);
  })
  .catch(err => {
    main.textContent = err.message;
  });

import()函数可以用在任何地方,不仅仅是模块,非模块的脚本也可以使用。它是运行时执行,也就是说,什么时候运行到这一句,就会加载指定的模块。另外,import()函数与所加载的模块没有静态连接关系,这点也是与import语句不相同。import()类似于 Node 的require方法,区别主要是前者是异步加载,后者是同步加载。



3. BigInt

在 ES10 增加了新的原始数据类型:BigInt,表示一个任意精度的整数,可以表示超长数据,可以超出2的53次方。

Js 中 Number类型只能安全的表示-(2^53-1)至 2^53-1 范的值

console.log(2 ** 53) // es7 幂运算符
console.log(Number.MAX_SAFE_INTEGER) // 最大值-1

使用 BigInt 有两种方式:

  1. 方式一:数字后面增加n
const bigInt = 9007199254740993n
console.log(bigInt)
console.log(typeof bigInt) // bigint

console.log(1n == 1) // true
console.log(1n === 1) // false
  1. 方式二:使用 BigInt 函数
const bigIntNum = BigInt(9007199254740993n)
console.log(bigIntNum)



4. Promise.allSettled()

Promise.allSettled()方法接受一个数组作为参数,数组的每个成员都是一个 Promise 对象,并返回一个新的 Promise 对象。只有等到参数数组的所有 Promise 对象都发生状态变更(不管是fulfilled还是rejected),返回的 Promise 对象才会发生状态变更。

该方法返回的新的 Promise 实例,一旦发生状态变更,状态总是fulfilled,不会变成rejected。状态变成fulfilled后,它的回调函数会接收到一个数组作为参数,该数组的每个成员对应前面数组的每个 Promise 对象。

results的每个成员是一个对象,对象的格式是固定的,对应异步操作的结果。

// 异步操作成功时
{status: 'fulfilled', value: value}

// 异步操作失败时
{status: 'rejected', reason: reason}

成员对象的status属性的值只可能是字符串fulfilled或字符串rejected,用来区分异步操作是成功还是失败。如果是成功(fulfilled),对象会有value属性,如果是失败(rejected),会有reason属性,对应两种状态时前面异步操作的返回值。



5. globalThis

Javascript 在不同的环境获取全局对象有不通的方式:

  • node 中通过 global
  • web 中通过 window, self 等.

globalThis 提供了一个标准的方式来获取不同环境下的全局 this 对象(也就是全局对象自身)。不像 window 或者 self 这些属性,它确保可以在有无窗口的各种环境下正常工作。所以,你可以安心的使用 globalThis,不必担心它的运行环境。为便于记忆,你只需要记住,全局作用域中的 this 就是 globalThis。

console.log(globalThis)



6. 可选链 Optional chaining

让我们在查询具有多层级的对象时,不再需要进行冗余的各种前置校验。

const user = {
    address: {
        street: 'xx街道',
        getNum() {
            return '80号'
        }
    }
}

在之前的语法中,想获取到深层属性或方法,不得不做的前置校验,否则很容易命中 Uncaught TypeError: Cannot read property… 这种错误,这极有可能让你整个应用挂掉。

const street = user && user.address && user.address.street
const num = user && user.address && user.address.getNum && user.address.getNum()
console.log(street, num)

用了 Optional Chaining ,上面代码会变成

const street2 = user?.address?.street
const num2 = user?.address?.getNum?.()
console.log(street2, num2)

可选链中的 ? 表示如果问号左边表达式有值, 就会继续查询问号后面的字段。根据上面可以看出,用可选链可以大量简化类似繁琐的前置校验操作,而且更安全。



7. 空值合并运算符(Nullish coalescing Operator)

空值合并运算符(??)是一个逻辑运算符。当左侧操作数为 null 或 undefined 时,其返回右侧的操作数。否则返回左侧的操作数。

当我们查询某个属性时,经常会遇到,如果没有该属性就会设置一个默认的值。

const b = 0 // 或者null undefined false
const a = b || 5
console.log(a)

空值合并运算符 ?? 我们仅在第一项为 null 或 undefined 时设置默认值

// false 0  无效
const a = b ?? 123
console.log(a)



七、 ECMAScript2021(ES12)



1. String.prototype.replaceAll

const str = '1 2 12 3 4';
console.log(str.replace('2', 'T')); // 1 T 12 3 4
console.log(str.replaceAll('2', 'T')); // 1 T 1T 3 4
console.log(str); // 1 2 12 3 4



2. Promise.any()

该方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例返回。

只要参数实例有一个变成fulfilled状态,包装实例就会变成fulfilled状态;如果所有参数实例都变成rejected状态,包装实例就会变成rejected状态。

Promise.any()跟Promise.race()方法很像,只有一点不同,就是Promise.any()不会因为某个 Promise 变成rejected状态而结束,必须等到所有参数 Promise 变成rejected状态才会结束。

Promise.any()抛出的错误,不是一个一般的 Error 错误对象,而是一个 AggregateError 实例。它相当于一个数组,每个成员对应一个被rejected的操作所抛出的错误。

Promise.any([Promise.resolve(1), Promise.resolve(2), Promise.resolve(2)]).then(value => console.log(value)); // 1
Promise.any([Promise.reject(1), Promise.reject(2), Promise.reject(2)]).then(undefined, err => console.log(err)); // AggregateError: All promises were rejected



3. Logical Assignment Operators 逻辑赋值操作符

a ||= b; // 等同于 a || (a = b);

a &&= b; // 等同于 a && (a = b);

a ??= b; // 等同于 a ?? (a = b);



4. WeakRefs



4.1 WeakRef

当将一个对象分配给一个变量时,它指向存储这个对象的值的那块内存(强引用)。如果程序不再引用这个对象,garbage collector 会销毁它并回收内存。WeakRef 的一个实例创建了一个对给定对象的引用,如果该对象仍然在内存中,则返回该对象;如果目标对象已经被垃圾回收,则返回未定义的对象。

const obj = { spec: "ES2021" };
const objWeakRef = new WeakRef(obj);
objWeakRef.deref(); // 如果对象obj被回收则返回undefined,否则就返回obj



4.2 FinalizationRegistry

FinalizationRegistry 的实例在注册的目标对象被垃圾收集后触发回调函数。

const obj = { spec: "ES2021" };
const registry = new FinalizationRegistry(value => { 
    console.log(`${value}`); // 被回收则执行
}); 
registry.register(obj, "ECMAScript 2021"); // 对象obj被回收时会执行此回调函数

值得注意的是,官方提示要尽量避免使用 WeakRef 和 FinalizationRegistry,垃圾回收机制依赖于 JavaScript 引擎的实现,不同的引擎或是不同版本的引擎可能会有所不同。



5. Numeric separators 数字分隔符

const x = 1000000000000
const y = 1_000_000_000_000
console.log(x === y) // true



八、 参考



版权声明:本文为letianxf原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。