es6

ES6

1. letconst

var是传统上的函数作用域。ES6推荐使用let声明局部变量,const声明常量,两者都为块级作用域。

  1. let用法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    var name = 'andy';

    {
    // let声明局部变量
    let name = 'qiqi';
    console.log(name); // qiqi
    }

    console.log(name); // andy
  2. const用法

    1
    2
    3
    4
    5
    const name = 'andy';
    name = 'qiqi'; // 报错,不可被修改

    const person = { name: 'andy' };
    person.name = 'qiqi'; // 可以被修改

const声明的变量都会被认为是常量,即它的值被设置完成后就不能再修改了,但是如果const是一个对象,对象所包含的值是可以被修改的。抽象点说,就是对象所指向的地址没有变。

以下几点需要注意:

  • let关键字声明的变量不具备变量提升特性;
  • letconst声明只在最靠近的一个块中有效;
  • const在声明时必须被赋值

2. 函数相关

2.1 箭头函数

ES6中,箭头函数是函数的一种简写形式。使用括号包裹参数,跟随一个 =>,紧接着是函数体;

箭头函数的三个特点:

  • 不需要function关键字来创建函数
  • 可以省略return关键字
  • 没有自己的this,继承当前上下文的this
1
[1, 2, 3].map(x => x + 1);

2.2 函数默认值

在参数括号内直接设置默认值

1
2
3
4
function Person(name = 'andy', age = 12) {
this.name = name;
this.age = age;
}

2.3 Spread/Rest操作符

1
2
3
4
5
6
// restParams代表剩下所有的参数
function print(a, b, ...restParams) {
console.log(restParams);
}

print(1, 2, 3, 4); // [3, 4]

3. 模板字符串

ES6之前我们用+来拼接字符串,但是在ES6中,可以使用``反引号来使用模板字符串

1
2
3
4
5
6
7
8
9
// ES5
var name = 'andy';
var age = '12'
var person = '我是' + name + '我今年' + age + '岁了'

// ES6
const name = 'andy';
const age = '12';
const person = `我是${name}我今年${age}岁了`;

4. 对象和数组的解构

  1. 对象解构

    1
    2
    3
    4
    5
    6
    7
    8
    const person = {
    name: 'andy',
    age: 11,
    }

    const { name, age } = person;
    console.log(name); // andy;
    console.log(age): // 12;
  2. 数组解构

    1
    2
    3
    4
    const persons = ['andy', 'qiqi'];
    const [boy, girl] = persons;
    console.log(boy); // andy;
    console.log(girl); // qiqi;

5. for...infor...of

  1. for...in:用来遍历对象中的属性

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // 对象的遍历
    const person = {
    name: 'andy',
    age: 12
    }

    for(let key in person) {
    console.log(key); // name, age
    }

    // 数组的遍历
    const ages = [12, 13];

    for(let key in ages) {
    console.log(key); // 0, 1
    }
  2. for...of:用来遍历一个迭代器

    1
    2
    3
    4
    5
    6
    // 数组的遍历
    const ages = [12, 13];

    for(let key of ages) {
    console.log(key); // 12, 13
    }

    for...of不能用来遍历对象,这是因为ES6中引入了Iterator,只有提供了Iterator接口的数据类型才可以使用for...of来循环遍历,而Array, Set, Map,某些类数组默认都提供了Iterator接口,所以它们可以使用for...of来进行遍历。

6. class

6.1 语法糖

ES6中支持class语法,不过,ES6class不是新的对象继承模型,它只是原型链的语法糖表现形式,它的绝大部分功能,ES5都可以做到。新的class写法只是让对象原型的写法更加清晰,更像面向对象编程的语法而已。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Person {
constructor() {
console.log('i am andy');
}

run() {
console.log('run');
}
}

// 使用new命令对类进行实例化,跟构造函数的用法完全一致
const person = new Person(); // i am andy
person.run(); // run

构造函数的prototype属性,在ES6的类上面继续存在。事实上,类的所有方法都定义在类的prototype上。

6.2 属性不可枚举

另外,类内部所有定义的方法,都是不可枚举的,这点与ES5不一样,如果是以ES5定义的构造函数,则其内部的属性都是可枚举的:

1
2
3
4
5
6
7
8
9
10
11
12
class Person {
constructor() {
console.log('andy');
}

run() {
console.log('run');
}
}

Object.keys(Person.prototype); // []
Object.getOwnPropertyNames(Person.prototype); // ['constructor', 'run']

6.3 constructor方法

constructor()方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。一个类必须有constructor方法,如果没有显式定义,系统会自动加上一个默认的constructor方法。

constructor方法默认返回实例对象this,也可以指定返回另外一个对象。

1
2
3
4
5
6
7
8
class Person {
constructor() {
return { name: 'andy' };
}
}

const aa = new Person();
console.log(aa); // { name: 'andy' }

6.4 原型

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Person {
constructor(name, age) {
// name和age定义在this对象上,即实例上
this.name = name;
this.age = age;
}

// run方法定义在原型上
run() {
console.log('run');
}
}

const person = new Person('andy', 12);

person.hasOwnProperty('name'); // true
person.hasOwnProperty('age'); // true
person.hasOwnProperty('run'); // false

person.__proto__.hasOwnProperty('run'); // true

__proto__并不是语言本身的特性,这是各大厂商具体实现时添加的私有属性,目前虽然在很多现代浏览器的JS引擎中都提供了这个私有属性,但依旧不建议在生产中使用该属性,避免对环境产生依赖。生产环境中,可以使用getPrototypeOf方法来获取实例对象的原型,然后再来为原型添加方法/属性。

6.5 取值函数(getter)与存值函数(setter)

ES5一样,在类内部可以使用getset关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Person {
constructor(name) {
this._name = name;
}

get name() {
return this._name;
}

set name(name) {
this._name = name;
}
}

6.7 属性表达式

类的属性名,可以采用表达式:

1
2
3
4
5
6
7
8
9
const action = 'run';

class Person {
constructor() {}

[action]() {
console.log('run');
}
}

6.8 静态方法

static用来定义类的静态方法,类的静态方法不会被实例所继承,只能通过类直接调用。如果静态方法中有this,则此this指向这个类,而不是其实例,

子类可以继承父类的静态方法。

静态方法可以与非静态方法重名。

6.9 类的继承

Class可以通过extends关键字实现继承,这比ES5通过修改原型链实现继承要清晰和方便。

子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类自己的this对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的梳理属性和方法。如果不调用super方法,子类就得不到 this对象。

如果super作为对象,用在静态方法之中,这是super将指向父类,而不是父类的原型对象。

有几点值得注意:

  • 类内部默认就是严格模式,所以不需要使用use strict指定运行模式;
  • 类的声明不会提升,如果要使用某个class,必须在使用之前定义它,否则会报错;
  • 在类中定义函数不需要使用function关键字;
  • 类可以通过extends继承一个父类,但是子类的constructor中需要执行super()函数;
  • 类必须使用new关键字创建实例,不可直接执行,这点与ES5的构造函数不同;

7. module

ES6之前,JavaScript一直没有模块(module)体系,无法将一个大程序拆分成互相依赖的小文件,再用简单的方法拼装起来。不过社区指定了一些模块加载方案,最主要的有CommonJSAMD两种,前者用于服务器,后者用于浏览器。在ES6中提供了一个标准的模块加载方案,主要通过exportimport实现:

7.1 加载时机(编译时or运行时)

7.1.1 运行时加载

CommonJSAMD模块,都只能在运行时确定模块的依赖关系。比如,CommonJS模块就是对象,输入时必须查找对象属性:

1
2
3
4
5
6
7
// CommonJS模块
let { stat, readfile } = require('fs');

// 等同于
let _fs = require('fs');
let stat = _fs.stat;
let readfile = _fs.readfile;

上面代码实质是整体加载fs模块,生成一个对象_fs,然后再从这个对象上面读取3个方法。这种加载成为“运行时加载”,因为只有运行时才能得到这个对象,导致完全没办法在编译时做“静态优化”。

7.1.2 编译时加载

ES6模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。ES6模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入:

1
2
// ES6模块
import { stat, readfile } from 'fs';

上面代码的是实质是从fs模块加载两个方法,其他方法不加载。这种加载成为“编译时加载”,或者静态加载,即ES6可以在编译时就完成模块加载,效率要比CommonJS模块的加载方式高。当然,这也导致了没法引用ES6模块本身,因为它不是对象。

除了静态加载带来的各种好处,ES6模块还有以下好处:

  • 不再需要UMD模块格式了,将来服务器和浏览器都会支持ES6模块格式。目前,通过各种工具库,其实已经做到了这一点。
  • 将来浏览器新API就能用模块格式提供,不再必须做成全局变量或者navigator对象的属性。
  • 不再需要对象作为命名空间,未来这些功能可以通过模块提供。

ES6的模块自动采用严格模式,不管你有没有在模块头部加上'use strict'

7.2 export

export命令主要用于规定模块的对外接口。

1
2
3
// 方法1
export var name = 'andy';
export var age = 12;
1
2
3
4
5
// 方法2
var name = 'andy';
var age = 12;

export { name, age };

上面两种export的使用方法都是正确的,ES6都会将当前文件视为一个模块,里边使用export对外暴露了两个变量。从可读性上来说更推荐方法2的写法,因为这种写法把所有的输出同一放文件最后,能一目了然看到当前文件都输出了哪些变量。

不只是变量,export也可用于输出方法和类:

1
2
3
4
5
function run() {
// code...
}

export { run };
1
2
3
4
5
class Person {
// code ...
}

export { Person };

通常情况下,export输出的变量或者方法名就是该文件模块对外暴露的属性,但是也可以用as关键字进行重命名。

1
2
3
4
5
6
7
8
function v1() {};
function v2() {};

export {
v1 as getAge,
v2 as getName,
v2 as getInfo,
}

另外,export输出的变量,与其对应的值是动态绑定关系,即通过该变量,可以获取到模块内部实时的值。这一点与CommonJS完全不同,CommonJS输出的是值的缓存,不存在动态更新。

最后,export模块只能放模块顶层,不能放入块级作用域或者方法中,否则会报错,这是因为处于条件代码块之中的export,没法做静态优化。

1
2
3
4
5
6
function getName() {
export var name = 1;
}

getName();
// 会报错

7.3 import

import命令主要用于引入其他模块提供的功能。使用export进行导出的接口,在其他文件中可以使用import进行引入。需要注意的是import引入的接口名必须与export导出的接口名一致,否则会报错,当然,也可以通过as关键字进行重命名:

1
2
3
4
5
// 正常引入
import { getNamem, age } from 'person';

// 通过as关键字进行重命名
import { getName as getInfo } from 'person';

上面代码,通过import导入的age是一个属性接口,是只读的,如果在当前文件修改age的值是不被允许的,但是如果age是一个对象的话,是可以修改其属性的,不过需要注意的一点是,这里的修改会反应到person模块中,当其他模块引入了age这个接口后,也会获取更新后的值,所以这种做法是不被推荐的。除非你想做一些全局变量。

注意:import命令具有提升效果,会提升到整个模块的头部,首先执行:

1
2
3
foo();

import { foo } from 'my_module';

上面的代码不会报错,因为import的执行早于foo。这种行为的本质是,import命令是编译阶段执行的,在代码之前。也正是因为import是静态执行,所以不能使用变量和表达式,这些只能在运行时才能拿得到结果的语法解构。

最后,import语句会执行所加载的模块,如下:

1
2
import 'lodash';
import 'lodash'

上面的代码仅仅执行lodash模块,但是不输入任何值。如果多次重复执行同义句import,那么只会执行一次,而不会执行多次,即import是一个单例模式(Singleton)。

7.4 模块的整体加载

除了指定某项输入值,还可以整体加载一个模块,要使用*指定一个对象,所有的输出值都会加载到这个对象上。

1
import * as person from 'person';

7.5 export default

export default用于为模块指定默认输出,

1
2
3
export default function getName() {
// code ...
}

上面的代码未当前文件模块默认导出了一个方法getName,由此方法进行导出,引入的时候不用关心接口名,可以直接重命名:

1
import getUserName from 'person';

本质上,一个模块只能有一个export default,这也是为了通过export default导出的接口不需要使用大括号的原因。另一个本质是通过export default导出的接口默认接口名为default,即使是export default后边跟了方法名或者变量名,其与不跟的情况是一样的,所以在外部引入的时候可以随意重命名该接口。

一个模块文件只能有一个export default,但是同时可以存在多个export,所以引入时可以同时引入:

1
2
3
4
5
export default React;
export {
Component,
createRef,
}
1
import React, { Component } from 'react';

7.6 exportimport结合

1
2
3
4
5
6
// 方法1
export { getName, getAge } from 'person';

// 可以理解为
import { getName, getAge } from 'person';
export { getName, getAge };

上面代码方法1中,exportimport结合使用,需要注意的是,此种引入导出方法,getNamegetAge并没有在此文件中引入,只是对外转发了这两个方法,所以此文件不能使用者两个方法。

  1. 模块的接口改名,可以采用下面方法:
1
export { getAge as getInfo } from 'person';
  1. 模块的整体输出:
1
export * from 'person';
  1. 默认接口的写法如下:
1
export { default } from 'person';
  1. 具体接口名改为默认接口:
1
export { getName as default } from 'person';
  1. 默认接口改为具体接口名:
1
export { default as getName } from 'person';
  1. 整体导出改为接口名:

    ES2020之前,这种import语句,没有复合写法:

1
import * as personMethod from 'person';

ES2020补上了这种写法:

1
2
3
4
5
export * as personMethod from 'person';

// 等同于
import * as personMethod from 'person';
export { personMethon };

7.7 模块的继承

模块也是可以继承的。

1
2
3
export * from 'circle';
export var name = 'andy';
export default function getName(){}

8. MapSet

8.1 Map

8.1.1 基本用法

ES6提供了一个新的数据结构Set,它类似于数组,但是成员的值都是唯一的,没有重复值。

1
2
3
4
5
6
7
const set = new Set();

set.add(1);
set.add(2);
set.add(3);

console.log(set.size);
1
2
const set = new Set([1, 2, 3, 4, 5]);
const docSet = new Set(document.querySelectorAll('div'));

Set加入值的时候,不会发生类型装换,所以5和’5’是两个不同的值。Set内部判断两个值是否相等,使用的算法类似于’===’运算符,主要区别是向Set加入值时认为NaN等于自身,而精确相当运算认为NaN不等于NaN,另外,两个对象总是不等的。

8.1.2 Set实例的属性和方法

  • Set.prototype.constructoySet的构造函数;
  • Set.prototype.size:返回Set实例的成员总数;
  • Set.prototype.add(value):添加某个值,返回Set解构本身;
  • Set.prototype.delete(value):删除某个值,返回一个布尔值,表示删除是否成功;
  • Set.prototype.has(value):返回一个布尔值,表示该值是否是Set的成员;
  • Set.prototype.clear():删除所有成员,没有返回值;
  • Set.prototype.keys():返回键名的遍历器;
  • Set.prototype.values():返回兼职的遍历器;
  • Set.prototype.entries():返回键值对的遍历器;
  • Set.prototype.forEach():使用回调函数遍历每个成员;

由于Set解构没有键名,只有键值(或者说键名和键值是同一个值),所以keys方法和values方法的行为完全一致。

8.2 WeakSet

8.2.1 含义

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

  1. WeakSet的成员只能是对象,而不能是其他类型的值;
  2. WeakSet中的对象都是弱引用,即垃圾回收机制不考虑WeakSet对该对象的引用。如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet 之中。

鉴于WeakSet的特殊机制,所以其内部只适合临时存放一组对象,或者跟对象绑定的信息(比如DOM节点信息) ,只要这些对象在外部小时,它在WeakSet里面的引用就会自动消失。所以WeakSet的成员是不适合应用的,因为它随时会消失。另外,由于WeakSet内部有多少个成员,取决于内部垃圾回收机制什么时候运行,运行前后的成员数可能是不同的,而垃圾回收机制何时运行是不可预测的,所以ES6规定WeakSet不可遍历。

8.2.2 方法

  • WeakSet.prototype.add(value):向WeakSet实例添加一个成员;
  • WeakSet.prototype.delete(value):删除一个成员;
  • WeakSet.prototype.has(value):返回一个布尔值,表示某个值是否在WeakSet实例之中。

8.3 Map

8.3.1 基础含义

JavaScript的对象,本质上是键值对的集合,但是传统上只能用字符串作为key值。而ES6解除了这种限制,它的键值可以为任何类型的值,实现了真正意义上的值-值的集合。

1
2
3
4
5
6
7
8
9
const map = new Map();
const obj = { name: 'andy' };

map.set(obj, 'person');
map.get(obj); // person

map.has(obj); // true
map.delete(obj); // true
map.has(obj); // false

Map构造函数接受数组以及任何具有Iterator接口的数据结构作为参数,生成一个新的Map实例。

  1. 数组作为参数
1
2
3
4
5
6
const items = [
['name', 'andy'],
['age', 12]
];

const map = new Map(items);
  1. set实例作为参数
1
2
3
4
5
6
const set = new Set([
['name', 'andy'],
['age', 12]
])

const map = new Map(set);
  1. map实例作为参数
1
2
3
4
5
const params = new Map();
params.set('namd', 'andy');
params.set('age', 12);

const map = new Map(params);

由上可知,Map的键实际上跟内存地址绑定的,只要内存地址不一样,就视为两个键。这就解决了同名属性碰撞的问题。Map同样将NaN视为同一个键。

8.3.2 方法

  • Map.prototype.size:返回Map解构的成员总数;
  • Map.prototype.set(key, value):设置一组键值对;
  • Map.prototype.get(key):返回key对应的value值;
  • Map.prototype.has(key):判断是否在当前map对象中;
  • Map.prototype.delete(key):删除某个key值;
  • Map.prototype.clear():清除整个map
  • Map.prototype.keys():返回键名的遍历器;
  • Map.prototype.values():返回键值的遍历器;
  • Map.prototype.entries():返回所有成员的遍历器;
  • Map.prototype.forEach():遍历Map的所有成员;

Map转为数组最方便的方法,就是使用扩展运算符(…)

8.3.3 数据结构互相转换

  1. Map转为数组

    1
    2
    3
    4
    5
    6
    const map = new Map([
    ['name', 'andy'],
    ['age', 12]
    ]);

    console.log([...map]);
  2. 数组转为Map

    1
    2
    3
    4
    const map = new Map([
    ['name', 'andy'],
    ['age', 12]
    ]);
  3. Map转对象

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    function mapToObj(map) {
    var result = {};

    for (let [key, value] of map) {
    result[key] = value;
    }

    return result;
    }

    var map = new Map([
    ['name', 'andy'],
    ['age', 12]
    ])

    var bb = mapToObj(map);
  4. 对象转Map

    对象转Map可以通过Object.entries,当然也可以自己实现一个转换函数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 方法一
    const person = { name: 'andy', age: 1 };
    const map = new Map(Object.entries(person));

    // 方法二
    function objToMap(obj) {
    var map = new Map();

    for (let key of Object.keys(obj)) {
    map.set(key, obj[key]);
    }

    return map;
    }
  5. MapJSON

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

    1
    2
    3
    function mapToJSON(map) {
    return JSON.stringify(mapToObj(map));
    }
    1
    2
    3
    function mapToJSON(map) {
    return JSON.stringify([...map]);
    }
  6. JSONMap

    正常情况下,JSON都可以转为Map

    1
    2
    3
    function JSONToMap(json) {
    return objToMap(JSON.parse(json));
    }

8.4 WeakMap

8.4.1 含义

WeakMapMap解构类似,也是用于生成键值对的集合。他们也有两个区别点

  1. WeakMap只接受对象作为键名(null除外),其他类型不接受。
  2. WeakMap的键名所指向的对象,不计入垃圾回收机制。

WeakMap的设计目的在于,有时我们想在对啊ing上面存放一些数据,但是这会行程对于这个对啊ing的引用。一旦不再需要这个对象,我们要必须删除这个应用,否则垃圾回收机制就不会释放其占用的内存。

它最适用的情况还是DOM的处理

1
2
3
4
5
6
const wm = new WeakMap();

const element = document.getElementById('example');

wm.set(element, 'some information');
wm.get(element) // "some information"

8.4.2 语法

WeakMapMap在API上的区别主要有两个;

  1. 没有遍历操作,即没有keysvaluesentries方法,也没有size属性,这也是因为不知道何时才会运行垃圾回收机制,其内部的数量有可能会变化。
  2. 无法清空,即不支持clear方法

8.4.3 API

WeakMap只有四个方法可以用:

  1. get()
  2. set()
  3. has()
  4. delete()

9. Proxy

9.1 基础

Proxy用于修改某些默认的操作行为。可以理解为,在目标对象之前架设一层“拦截”,外接对该对象的访问,都必须经过这层拦截。Proxy这个词原意是代理,用在这里表示由他来“代理”某些操作,可以译为“代理器”。

1
2
3
4
5
6
7
8
9
10
const obj = new Proxy({}, {
get(target, propKey, receiver) {
console.log('get:', target, propKey);
return Reflect.get(target, propKey, receiver);
},
set(target, propKey, value, receiver) {
console.log('set:', target, propKey, value, receiver);
Reflect.set(target, propKey, value);
}
})

Proxy对象的所有用法,都是上面这种形式,不同的只是handler参数的写法,其中,new Proxy()表示生成一个Proxy实例,target参数表示所要拦截的目标对象,handler参数用来定制拦截行为。

9.2 拦截操作

Proxy支持的拦截操作一共有13中:

  • get(target, propKey, receiver):拦截对象属性的读取;
  • set(target, propKey, value, receiver):拦截对象属性的设置;
  • has(target, propKey):拦截propKey in proxy的操作,返回一个布尔值;
  • delete(target, propKey):拦截delete proxy[propKey]的操作,返回一个布尔值;
  • ownKeys(target):拦截Object.getOwnPropertyNames(proxy)Object.getOwnPropertySymbols(proxy)Object.keys(proxy)for...in循环,返回一个包含目标对象所有自身属性的数组;
  • 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),返回一个对象;
  • setPrototypeOf(target, proto):拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值;
  • isExtensible(target):拦截Object.isExtensible(proxy),返回一个布尔值;
  • apply(target, object, args):拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)proxy.call(object, ...args)proxy.apply(...)
  • construct(target, args):拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(...args)

9.3 优势

Proxy出世之前,我们用Object.defineProperty来实现一个对象操作的拦截。Proxy相对于Object.defineProperty可谓是一个升级版,那么它究竟有什么优势呢:

  1. 支持数组,Proxy本身支持对数组的拦截,不需要再对数组进行重载,省去了众多hack

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    let arr = [1,2,3]
    let proxy = new Proxy(arr, {
    get (target, key, receiver) {
    console.log('get', key)
    return Reflect.get(target, key, receiver)
    },
    set (target, key, value, receiver) {
    console.log('set', key, value)
    return Reflect.set(target, key, value, receiver)
    }
    })
    proxy.push(4)
    // 能够打印出很多内容
    // get push (寻找 proxy.push 方法)
    // get length (获取当前的 length)
    // set 3 4 (设置 proxy[3] = 4)
    // set length 4 (设置 proxy.length = 4)
  2. 针对对象,Proxy针对的是整个对象,而非对象中的某个属性,所以也就不需要对keys进行遍历;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    let obj = {
    name: 'Eason',
    age: 30
    }
    let handler = {
    get (target, key, receiver) {
    console.log('get', key)
    return Reflect.get(target, key, receiver)
    },
    set (target, key, value, receiver) {
    console.log('set', key, value)
    return Reflect.set(target, key, value, receiver)
    }
    }
    let proxy = new Proxy(obj, handler)
    proxy.name = 'Zoe' // set name Zoe
    proxy.age = 18 // set age 18
  3. 嵌套支持,本质上,Proxy 也是不支持嵌套的,这点和 Object.defineProperty() 是一样的。因此也需要通过逐层遍历来解决。Proxy 的写法是在 get 里面递归调用 Proxy 并返回,代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    let obj = {
    info: {
    name: 'eason',
    blogs: ['webpack', 'babel', 'cache']
    }
    }
    let handler = {
    get (target, key, receiver) {
    console.log('get', key)
    // 递归创建并返回
    if (typeof target[key] === 'object' && target[key] !== null) {
    return new Proxy(target[key], handler)
    }
    return Reflect.get(target, key, receiver)
    },
    set (target, key, value, receiver) {
    console.log('set', key, value)
    return Reflect.set(target, key, value, receiver)
    }
    }
    let proxy = new Proxy(obj, handler)
    // 以下两句都能够进入 set
    proxy.info.name = 'Zoe'
    proxy.info.blogs.push('proxy')
  4. Proxy提供了比Object.defineProperty更多的拦截方法,扩展了很多功能。

10. Reflect

10.1 基础

Reflect对象与Proxy对象一样,也是ES6为了操作对象而提供的新API

Reflect对象的设计目的有以下几个:

  1. Object对象的一些明显属于语言内部的方法,放到Reflect上,现阶段,某些方法同时在ObjectReflect对象上部署,未来的新方法将只在Reflect对象上部署。也就是说,从Reflect对象上可以拿到语言内部的方法;
  2. 修改某些Object方法的返回结果,让其变得更合理。比如,Object.defineProperty(obj, name, desc)在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj, name, desc)则会返回false
  3. Object操作都变成函数行为。某些Object操作是命令式,比如name in objdelete obj[name],而Reflect.has(obj, name)Reflect.deleteProperty(obj, name)让它们变成了函数行为。
  4. Proxy对象的方法一一对应。只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。这就让Proxy对象可以方便地调用对应的Reflect方法,完成默认行为,作为修改行为的基础。也就是说,不管Proxy怎么修改默认行为,你总可以在Reflect上获取默认行为。

10.2 静态方法

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

11. Promise

11.1 基础

所谓Promise,简单说是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise是一个对象,从它可以获取异步操作的消息。

1
2
3
4
5
6
7
8
const promise = new Promise(function(resolve, reject) {

if (){
resolve(value);
} else {
reject(error);
}
});

11.2 方法

  • Promise.all()

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

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

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

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

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

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

  • Promise.race()

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

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

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

  • Promise.allSettled()

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

  • Promise.any()

ES2021 引入了Promise.any()方法。该方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例返回。只要参数实例有一个变成fulfilled状态,包装实例就会变成fulfilled状态;如果所有参数实例都变成rejected状态,包装实例就会变成rejected状态。

Generator函数