本系列博客为ES6基础语法的使用及总结,如有错误,欢迎指正。
重学ES6之出来混迟早要还的(六)主要包括 Generator、Set/Map、Proxy等。
其他笔记:
重学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
属性为true
,for...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
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。