重学ES6基础语法(六)

2,264 阅读13分钟

本系列博客为ES6基础语法的使用及总结,如有错误,欢迎指正。 重学ES6基础语法(六)主要包括 GeneratorSet/MapProxy等。

异步编程

Javascript语言的执行环境是"单线程"(single thread)。

所谓"单线程",就是指一次只能完成一件任务。如果有多个任务,就必须排队,前面一个任务完成,再执行后面一个任务,以此类推。

Javascript语言将任务的执行模式分成两种:同步(Synchronous)和异步(Asynchronous)。

"同步模式"就是上一段的模式,后一个任务等待前一个任务结束,然后再执行,程序的执行顺序与任务的排列顺序是一致的、同步的;
"异步模式"则完全不同,每一个任务有一个或多个回调函数(callback),前一个任务结束后,不是执行后一个任务,而是执行回调函数,后一个任务则是不等前一个任务结束就执行,所以程序的执行顺序与任务的排列顺序是不一致的、异步的。

"异步模式"非常重要。在浏览器端,耗时很长的操作都应该异步执行,避免浏览器失去响应,最好的例子就是Ajax操作。在服务器端,"异步模式"甚至是唯一的模式,因为执行环境是单线程的,如果允许同步执行所有http请求,服务器性能会急剧下降,很快就会失去响应。

异步编程的方法

通过回调函数

优点是简单,容易理解和部署;缺点是不利于代码的阅读和维护,各个部分之间高度耦合(Coupling),流程混乱,而且每个任务只能指定一个回调函数。

通过事件监听

任务的执行不取决于代码的顺序,而取决于某个事件是否发生。

通过事件监听,可以绑定多个事件,每个事件可以指定多个回调函数,而且可以"去耦合"(Decoupling),有利于实现模块化。缺点是整个程序都要变成事件驱动型,运行流程会变得很不清晰。

采用发布/订阅模式

我们假定,存在一个"信号中心",某个任务执行完成,就向信号中心"发布"(publish)一个信号,其他任务可以向信号中心"订阅"(subscribe)这个信号,从而知道什么时候自己可以开始执行。这就叫做"发布/订阅模式"(publish-subscribe pattern),又称"观察者模式"(observer pattern)。

这种方法的性质与"事件监听"类似,但是明显优于后者。因为我们可以通过查看"消息中心",了解存在多少信号、每个信号有多少订阅者,从而监控程序的运行。

Promise

不多赘述,这里有说

Generator

Generator 函数是 ES6 提供的一种异步编程解决方案

顾名思义,它是一个生成器,它也是一个状态机,内部拥有值及相关的状态。生成器返回一个迭代器Iterator对象,我们可以通过这个迭代器,手动地遍历相关的值、状态,保证正确的执行顺序。

所谓生成器,其实就是一个对象,它每次能生成一系列值中的一个。要创建生成器,可以让函数通过 yield 操作符返回某个特殊的值。对于使用 yield 操作符返回值的函数,调用它时就会创建并返回一个新的 Generator 实例。然后,在这个实例上调用 next()方法就能取得生成器的第一个值。此时,执行的是原来的函数,但执行流到 yield 语句就会停止,只返回特定的值。从这个角度看,yield 与 return 很相似。如果再次调用 next()方法,原来函数中位于 yield 语句后的代码会继续执行,直到再次遇见 yield 语句时停止执行。

1.用法

Generator 函数不同于普通函数,是可以暂停执行的,所以函数名之前要加星号,以示区别。

整个 Generator 函数就是一个封装的异步任务,或者说是异步任务的容器。异步操作需要暂停的地方,都用 yield 关键字注明。

  • yield关键字可以让 Generator内部的逻辑能够切割成多个部分。
function* listNum() {
    let i = 1;
    yield i;
    i++;
    yield i;
    i++;
    yield i;
}
const num = listNum();
console.log(listNum());

  • 通过调用迭代器对象的next方法执行一个部分代码,执行哪个部分就会返回哪个部分定义的状态
const num = listNum();
console.log(num.next());
console.log(num.next());
console.log(num.next());

如上代码,定义了一个listNum的生成器函数,调用之后返回了一个迭代器对象(即num)

调用next方法后,函数内执行第一条yield语句,输出当前的状态done(迭代器是否遍历完成)以及相应值(一般为yield关键字后面的运算结果)

每调用一次next,则执行一次yield语句,并在该处暂停。

2.Generator 函数遍历数组

2.1 可以发现,如果不调用next方法,那么函数中封装的代码不会立即被执行(下例中的console.log(arr);没有运行)

let arr = [
  {name: 'zs',age: 38,gender: 'male'},
  {name: 'yw',age: 48,gender: 'male'},
  {name: 'lc',age: 28,gender: 'male'},
];
function* loop(arr) {
  console.log(arr);
  for(let item of arr){
    yield item;
  }
}
let repoGen = loop(arr);
console.log(repoGen);

2.2 只有开始调用next方法,生成器函数里面的代码才开始执行

let repoGen = loop(arr);
console.log(repoGen.next());
console.log(repoGen.next());
console.log(repoGen.next());

2.3 当遍历完之后,done标志变为true时,再打印生成器函数,可以发现它的状态已经变为了closed

let repoGen = loop(arr);
console.log(repoGen);
console.log(repoGen.next());
console.log(repoGen.next());
console.log(repoGen.next());
console.log(repoGen.next());
console.log(repoGen);

3.Generator函数和普通函数区别

3.1 调用Generator函数后,无论函数有没有返回值,都会返回一个迭代器对象
3.2 调用Generator函数后,函数中封装的代码不会立即被执行

4.next()调用中的传参

在调用next方法的时候可以传递一个参数, 这个参数会传递给上一个yield
注意:第一次调用next()时是不能传参的,只能从第二次开始

4.1 第一次调用next之后返回值one为1,但在第二次调用next的时候one其实是undefined的,因为generator不会自动保存相应变量值,我们需要手动的指定,这时two值为NaN,在第三次调用next的时候执行到yield 3 * two,通过传参将上次yield返回值two设为2,得到结果

function* showNumbers() {
   var one = yield 1;
   var two = yield 2 * one;
   yield 3 * two;
 }

 var show = showNumbers();

 console.log(show.next().value);// 1
 console.log(show.next().value);// NaN
 console.log(show.next(2).value); // 6

4.2 解析我写不出来(只能意会不能言传...)

function* gen() {
  console.log("123");
  let res = yield "aaa";

  console.log(res);
  console.log("567");
  yield 1 + 1;

  console.log("789");
  yield true;
}
let it = gen();
console.log(it.next()); //先输出123,再输出{value: "aaa", done: false}
console.log(it.next("666")); //传递参数给res,输出666;再输出567;再输出{value: 2, done: false}
console.log(it.next()); //{value: true, done: false}
console.log(it.next()); //{value: undefined, done: true}

5.Generator函数应用场景

5.1 让函数返回多个值

 function* calculate(a, b) {
     yield a + b;
     yield a - b;
 }
 let it = calculate(10, 5);
 console.log(it.next().value);
 console.log(it.next().value);

5.2 用同步的流程来表示异步的操作(用来处理ajax请求的工作流)
5.3 由于Generator函数就是遍历器生成函数,因此可以把Generator赋值给对象的Symbol.iterator属性,从而使得该对象具有Iterator接口。

6.for...of

for...of循环可以自动遍历Generator函数时生成的Iterator对象,且此时不再需要调用next方法。

function *foo() {
  yield 1;
  yield 2;
  yield 3;
  yield 4;
  yield 5;
  return 6;
}

for (let v of foo()) {
  console.log(v);
}
// 1 2 3 4 5

上面代码使用for...of循环,依次显示5个yield语句的值。这里需要注意,一旦next方法的返回对象的done属性为truefor...of循环就会中止,且不包含该返回对象,所以上面代码的return语句返回的6,不包括在for...of循环之中。

Set数据结构

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

与数组的不同之处:Set数据结构不能通过索引来获取元素。

1.用法

Set本身是一个构造函数,用来生成Set数据结构。

格式:new Set([iterable]);

let list = new Set();
console.log(list);

let color = new Set(['red', 'yellow', 'green']);
console.log(color);

2.Set数据结构常用方法

2.1 add(value)

在Set对象尾部添加一个元素。返回该Set对象。

注意:添加相同的成员会被忽略

let list = new Set();
[1,2,3,4,5].map(item => list.add(item));
console.log(list);

2.2 .size

返回Set实例的成员总数。

let list = new Set();
[1,2,3,4,5].map(item => list.add(item));
console.log(list.size); //5

2.3 delete(value)

删除某个值,返回一个布尔值,表示删除是否成功。

let list = new Set();
[1,2,3,4,5].map(item => list.add(item));
console.log(list.delete(5)); //true

2.4 .clear

移除Set对象内的所有元素,没有返回值。

let list = new Set();
[1,2,3,4,5].map(item => list.add(item));
list.clear();
console.log(list); //Set(0) {}

2.5 has(value)

返回一个布尔值,表示该值在Set中存在与否。

let list = new Set();
[1,2,3,4,5].map(item => list.add(item));
console.log(list.has(1)); //true

3.Set是可以遍历的

3.1 .values() 返回一个新的迭代器对象,该对象包含Set对象中的按插入顺序排列的所有元素的值。

let list = new Set();
[1,2,3,4,5].map(item => list.add(item));
console.log(list.values());

3.2 调用遍历器的next()方法实现遍历

let list = new Set();
[1,2,3,4,5].map(item => list.add(item));
let it = list.values();
console.log(it.next());
console.log(it.next());
console.log(it.next());
console.log(it.next());
console.log(it.next());
console.log(it.next());

3.3 Set方法部署了Iterator接口,所以也可以使用for of来遍历

for(let key of list){
  console.log(key);
}

利用forEach遍历

list.forEach((item,key,ownSet) => {
  console.log(item, key, ownSet);
})

4.利用Set进行数组去重

面试的时候经常有问:用ES5方法和ES6分别实现数组去重

先把数组转为Set结构实现去重,因为Set是可以遍历的,所以再使用扩展运算符将Set转为数组

let arr = [1,2,4,6,4,3,6,8];
let numberSet = new Set(arr); //数组转Set
console.log(numberSet);
let uniqueArr = [...numberSet]; //Set转数组
console.log(uniqueArr);

优雅写法:

Array.prototype.unique = function () {
    return [...new Set(this)];
};

WeakSet

WeakSet结构与Set类似,也是不重复的值的集合。

WeakSet结构有以下三个方法。

.add(value):向WeakSet实例添加一个新成员。
.delete(value):清除WeakSet实例的指定成员。
.has(value):返回一个布尔值,表示某个值是否在WeakSet实例之中。

WeakSet与Set的区别

  • WeakSet的成员只能是对象,而不能是其他类型的值。
let lucy = {region:'America',age: 18};
let lily = {region:'Canada',age: 20};

let person = new WeakSet([lucy,lily]);
person.add('lucas');
console.log(person); //Invalid value used in weak set
  • WeakSet是不可遍历的。
for(let key of person){
  console.log(key); //person is not iterable
}
  • WeakSet没有size属性,没有办法遍历它的成员。
  • WeakSet没有clear()方法,但是具有自己清除的作用,避免内存泄漏。
let lucy = {region:'America',age: 18};
let lily = {region:'Canada',age: 20};

let person = new WeakSet([lucy,lily]);
console.log(person);

将lily对象置为null之后,前后两次打印的都是只有lucy对象

let lucy = {region:'America',age: 18};
let lily = {region:'Canada',age: 20};

let person = new WeakSet([lucy,lily]);
console.log(person);
lily = null;
console.log(person);

Map数据结构

Map 对象保存键值对。任何值(对象或者原始值) 都可以作为一个键或一个值。

Map 类型,也称为简单映射,只有一个目的:保存一组键值对儿。开发人员通常都使用普通对象来保存键值对儿,但问题是那样做会导致键容易与原生属性混淆。简单映射能做到键和值与对象属性分离,从而保证对象属性的安全存储。

ES6提供的Map数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object结构提供了“字符串—值”的对应,Map结构提供了“值—值”的对应,是一种更完善的Hash结构实现。如果你需要“键值对”的数据结构,Map比Object更合适。

1.用法

  • 语法:new Map([iterable])
  • 参数:Iterable 可以是一个数组或者其他iterable对象,其元素为键值对(两个元素的数组,例如: [[ 1, 'one' ],[ 2, 'two' ]])。 每个键值对都会添加到新的 Map。null 会被当做 undefined。
const people = new Map();
people.set('lucy',18);
people.set('lily',20);
console.log(people);

2.Map数据结构常用方法

2.1 .size
size属性返回Map结构的成员总数。

2.2 set(key, value)
set方法设置key所对应的键值,然后返回整个Map结构。如果key已经有值,则键值会被更新,否则就新生成该键。

2.3 get(key)
get方法读取key对应的键值,如果找不到key,返回undefined。

2.4 has(key)
has方法返回一个布尔值,表示某个键是否在Map数据结构中。

2.5 delete(key)
delete方法删除某个键,返回true。如果删除失败,返回false。

2.6 .clear()
clear方法清除所有成员,没有返回值。

3.Map是可以遍历的

const people = new Map();
people.set('lucy',18);
people.set('lily',20);
people.set({},3);
for(let key of people){
  console.log(key);
}

people.forEach((value,key,map) => {
  console.log(value, key, map);
})

4.Map和Object的区别

Map的键(即key)可以是任意类型值,可以是一个对象,可以是一个函数等

  • 一个Object的键只能是字符串或者Symbols,但一个Map的键可以是任意值,包括函数、对象、基本类型。
  • Map 中的键值是有序的,而添加到对象中的键则不是。因此,当对它进行遍历时,Map 对象是按插入的顺序返回键值。
const people = new Map();
people.set('lucy',18);
people.set('lily',20);
people.set({},3);
console.log(people);

5.与其他数据结构的互相转换

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

const people = new Map();
people.set('lucy',18);
people.set('lily',20);
people.set({},3);
let arr = [...people];
console.log(arr);

5.2 数组转为Map
将数组转入Map构造函数,就可以转为Map。

new Map([[true, 7], [{foo: 3}, ['abc']]])

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

function strMapToObj(strMap) {
  let obj = Object.create(null);
  for (let [k,v] of strMap) {
    obj[k] = v;
  }
  return obj;
}

5.4 对象转为Map

function objToStrMap(obj) {
  let strMap = new Map();
  for (let k of Object.keys(obj)) {
    strMap.set(k, obj[k]);
  }
  return strMap;
}

WeakMap

WeakMap与Map的区别在于:

  • WeakMap它只接受对象作为键名(null除外),不接受其他类型的值作为键名
  • WeakMap不能遍历
  • WeakMap的元素在其他地方没有被引用时,垃圾回收机制会自动清理掉该元素

WeakMap与Map在API上的区别主要是两个:

  • 没有遍历操作(即没有key()values()entries()方法),也没有size属性;
  • 无法清空,即不支持clear方法。这与WeakMap的键不被计入引用、被垃圾回收机制忽略有关。因此,WeakMap只有四个方法可用:get()set()has()delete()

Proxy

Proxy 对象用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等)。 帮助我们重写对象上的一些默认的方法,定义自己的业务逻辑。

在需要公开 API,而同时又要避免使用者直接操作底层数据的时候,可以使用代理。

1.用法

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

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

2.Proxy实例的方法

get()

用于拦截某个属性的读取操作

const person = {name: 'ghk',age: 18};
const personProxy = new Proxy(person,{
get(target, key){
  console.log(target, key); //{name: "zs", age: 18} "name"
  return target[key].toUpperCase();
},
});
personProxy.name = 'zs';
console.log(personProxy); //Proxy {name: "zs", age: 18}
console.log(personProxy.name); //ZS

set()

set方法用来拦截某个属性的赋值操作。

const person = {name: 'ghk',age: 18};
const personProxy = new Proxy(person,{
set(target, key, value){
  if(typeof value === 'string'){
    target[key] = value.trim();
  }
}
});
personProxy.string = '    this is a test   ';
console.log(personProxy.string); //THIS IS A TEST