乐趣区

重学ES6之出来混迟早要还的六

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

其他笔记:
重学 ES6 之出来混迟早要还的(一)
重学 ES6 之出来混迟早要还的(二)
重学 ES6 之出来混迟早要还的(三)
重学 ES6 之出来混迟早要还的(四)
重学 ES6 之出来混迟早要还的(五)

异步编程

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

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

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

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

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

异步编程的方法

通过回调函数

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

通过事件监听

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

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

采用发布 / 订阅模式

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

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

Promise

不多赘述,这里有说

Generator

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

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

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 对象保存键值对。任何值(对象或者原始值) 都可以作为一个键或一个值。

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

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
退出移动版