http://es6.ruanyifeng.com/阮一峰ES6
目录
1、Set 2、WeakSet 3、Map 4、WeakMap
1、Iterator的部署和调用 2、默认调用Iterator的场合 3、遍历器对象的return(),throw() 4、for...of循环
十、Set和Map数据结构
优先使用Map,如果主要保证数据的唯一性,使用Set。尽量不使用数组和对象。
1、Set
Set类似于数组,但是成员的值都是唯一的,没有重复的值。Set本身是一个构造函数,用来生成Set数据结构。Set函数可以接受一个数组或具有iterable接口的其它数据结构作为参数(如querySelectorAll选择的DOM元素列表),用来初始化。
向Set加入值时,不会发生类型转换,类似于===且NaN等于自身,所以5和"5"是两个不同的值。另外,两个对象总是不相等的。
const set=new Set([1,2,2,3,4,4,4]);
console.log([...set]); // [1, 2, 3, 4]
(1)Set实例的属性
Set.prototype.constructor:构造函数,默认就是Set函数。
Set.prototype.size:返回Set实例的成员总数。
(2)Set实例的操作方法(用于操作数据)
add(value):添加某个值,返回Set结构本身。
delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
has(value):返回一个布尔值,表示该值是否为Set的成员。
clear():清除所有成员,没有返回值。
const s=new Set();
s.add(1).add(2).add(2);
s.size // 2
s.has(1) // true
s.has(3) // false
s.delete(2);
s.has(2) // false
s.clear();
(3)Set实例的遍历方法(用于遍历成员)
keys(),values(),entries():返回键名、键值、键值对的遍历器对象。Set结构的键名和键值是同一个值,keys和values方法的行为完全一致。
forEach():使用回调函数遍历每个成员,没有返回值。回调函数的参数依次为键值、键名、集合本身。第二个参数表示绑定处理函数内部的this对象。
Set的遍历顺序就是插入顺序。如使用Set保存一个回调函数列表,调用时就能保证按照添加顺序调用。
Set结构的实例默认可遍历,它的默认遍历器生成函数就是它的values方法。可以省略values方法,直接用for...of循环遍历Set。
let set=new Set(['red','green','blue']);
for(let item of set.keys()){
console.log(item);
} // red green blue
for(let item of set.entries()) {
console.log(item);
} // ["red", "red"] ["green", "green"] ["blue", "blue"]
用于去除数组的重复成员。实现并集(Union)、交集(Intersect)和差集(Difference)。
// 去除数组的重复成员
[...new Set(array)]
Array.from(new Set(array))
let a=new Set([1, 2, 3]);
let b=new Set([4, 3, 2]);
// 并集
let union=new Set([...a,...b]); // Set {1, 2, 3, 4}
// 交集
let intersect=new Set([...a].filter(x=>b.has(x))); // set {2, 3}
// 差集
let difference=new Set([...a].filter(x=>!b.has(x))); // Set {1}
如果想在遍历操作中,同步改变原来的Set结构。
// 使原Set中每个值变为2倍[2,4,6]
let set=new Set([1,2,3]);
set=new Set([...set].map(val=>val*2)); // 方法一
set=new Set(Array.from(set,val=>val*2)); // 方法二
2、WeakSet
WeakSet与Set有两个区别。首先,WeakSet的成员只能是对象,不能是其它类型的值。其次,WeakSet中的对象都是弱引用,如果其它对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,只要这些对象在外部消失,它在WeakSet里面的引用就会自动消失。
WeakSet不可遍历,没有size属性。由于WeakSet内部有多少个成员,取决于垃圾回收机制有没有运行,运行前后很可能成员个数是不一样的,而垃圾回收机制何时运行是不可预测的。
WeakSet是一个构造函数,可以使用new命令,创建WeakSet数据结构。WeakSet可以接受一个数组或类似数组的对象(任何具有Iterable接口的对象)作为参数。该数组的所有成员,都会自动成为WeakSet实例对象的成员。
const a=[[1,2],[3,4]];
const ws1=new WeakSet(a); // WeakSet {[1, 2], [3, 4]}
const b=[3,4];
const ws2=new WeakSet(b); // Uncaught TypeError: Invalid value used in weak set(…)
WeakSet.prototype.add(value):向WeakSet实例添加一个新成员。
WeakSet.prototype.delete(value):清除WeakSet实例的指定成员。
WeakSet.prototype.has(value):返回一个布尔值,表示某个值是否在WeakSet实例之中。
WeakSet 适合临时存放一组对象,以及存放跟对象绑定的信息。如储存DOM节点,而不用担心这些节点从文档移除时,会引发内存泄漏。
3、Map
Map数据结构类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。Object结构提供了“字符串—值”的对应,Map结构提供了“值—值”的对应,是一种更完善的Hash结构实现。
Map可以接受一个数组或其它具有Iterator接口、且每个成员都是一个双元素的数组的数据结构作为参数。Set和Map都可以用来生成新的Map。
const map=new Map([
['name','张三'],
['title','Author']
]);
map.size // 2
map.has('name') // true
map.get('name') // "张三"
只有对同一个对象的引用,Map结构才将其视为同一个键。下面代码的set和get方法,键值相同但内存地址是不一样的。Map的键实际上是跟内存地址绑定的,解决了同名属性碰撞(clash)的问题,扩展别人的库时,如果使用对象作为键名,就不用担心属性同名。
如果Map的键是一个简单类型的值(数字、字符串、布尔值),则只要两个值严格相等,Map 将其视为一个键。0和-0是一个键,布尔值true和字符串true是不同的键,undefined和null是不同的键,NaN是同一个键。
const map=new Map();
map.set(['a'],555);
map.get(['a']) // undefined
(1)Map实例的属性和操作方法
size属性返回Map结构的成员总数。
set(key, value)方法设置键名key对应的键值为value,返回整个Map结构,可以采用链式写法。如果key已经有值,则键值会被更新,否则就新生成该键。
get(key)方法读取key对应的键值,如果找不到key,返回undefined。
has(key)方法返回一个布尔值,表示某个键是否在当前Map对象之中。
delete(key)方法删除某个键,返回true。如果删除失败,返回false。
clear()方法清除所有成员,没有返回值。
const m=new Map();
const o={p:'Hello World'};
m.size // 0
m.set(o,'content1').set(o,"content2");
m.get(o) // "content2"
m.get(123); // undefined
m.has(o) // true
m.delete(o) // true
m.has(o) // false
(2)Map实例的遍历方法
keys(),values(),entries():返回键名、键值、所有成员的遍历器对象。
forEach():遍历Map的所有成员。回调函数的参数依次为键值、键名、集合本身。第二个参数表示绑定处理函数内部的this对象。
Map的遍历顺序就是插入顺序。Map结构的默认遍历器接口(Symbol.iterator属性)是entries方法。
const map=new Map([
['F','no'],
['T','yes'],
]);
for(let key of map.keys()){
console.log(key);
} // "F" "T"
for(let value of map.values()){
console.log(value);
} // "no" "yes"
// 等同于使用map.entries()
for(let [key,value] of map){
console.log(key, value);
} // "F" "no" "T" "yes"
(3)与其他数据结构的互相转换
Map->数组,使用扩展运算符(...)。
数组->Map,将数组传入Map构造函数new Map()。
Map->对象,如果所有Map的键都是字符串,它可以无损地转为对象。非字符串的键名会被转成字符串,再作为对象的键名。
function strMapToObj(strMap){
let obj=Object.create(null);
for(let [k,v] of strMap){
obj[k]=v;
}
return obj;
}
function objToStrMap(obj){
let strMap=new Map();
for(let k of Object.keys(obj)){
strMap.set(k,obj[k]);
}
return strMap;
}
Map->JSON,如果Map的键名都是字符串,转为对象JSON。Map的键名有非字符串,转为数组JSON。
JSON->Map,正常情况下,所有键名都是字符串。但如果整个JSON就是一个数组,且每个数组成员本身,又是一个有两个成员的数组,可以一一对应地转为Map。
function strMapToJson(strMap){
return JSON.stringify(strMapToObj(strMap));
}
function mapToArrayJson(map) {
return JSON.stringify([...map]);
}
function jsonToStrMap(jsonStr){
return objToStrMap(JSON.parse(jsonStr));
}
function jsonToMap(jsonStr){
return new Map(JSON.parse(jsonStr));
}
4、WeakMap
WeakMap结构与Map结构类似,也是用于生成键值对的集合。区别有两点,WeakMap只接受对象作为键名(null除外),WeakMap的键名所指向的对象,不计入垃圾回收机制。
参考SetMap。
十一、Iterator和for...of循环
JavaScript表示“集合”的数据结构Array、Object、Map和Set,还可以组合使用它们定义自己的数据结构。遍历器(Iterator)是一种接口,为各种数据结构提供一个统一的访问机制,即for...of循环,使得数据结构的成员能够按某种次序排列。任何数据结构只要部署Iterator接口,就可以完成遍历操作,称这种数据结构是“可遍历的”(iterable)。
Iterator的遍历过程:创建一个指针对象,指向当前数据结构的起始位置;不断调用指针对象的next方法,直到它指向数据结构的结束位置。每一次调用next方法,都会返回一个对象包含当前成员的信息,value属性是当前成员的值,done属性是一个布尔值,表示遍历是否结束。(像是数据结构中的链表结构)
1、Iterator的部署和调用
ES6规定,默认的Iterator接口部署在数据结构的Symbol.iterator属性,该属性是一个函数,即当前数据结构默认的遍历器生成函数。执行这个函数,就会返回一个遍历器。
有些数据结构原生具备Iterator接口,称为部署了遍历器接口,不用任何处理就可以被for...of循环遍历。Array、Map、Set、String、TypedArray、函数的arguments对象、NodeList对象。
let arr=['a','b','c'];
let iter=arr[Symbol.iterator]();
console.log(iter.next()); // {value: "a", done: false}
console.log(iter.next()); // {value: "b", done: false}
console.log(iter.next()); // {value: "c", done: false}
console.log(iter.next()); // {value: undefined, done: true}
Object对象默认没有部署遍历器接口,因为对象属性的遍历顺序不确定,且对象是非线性的数据结构,遍历器是一种线性处理。
自定义Iterator接口时,done:false和value:undefined属性可以省略。
const obj={
data:["hello","world"],
[Symbol.iterator](){
const self=this;
let index=0;
return {
next(){
if(index<self.data.length){
return {value:self.data[index++]};
}else{
return {done:true};
}
}
}
}
}
for(let item of obj){
console.log(item);
}
// "hello"
// "world"
对于类似数组的对象(存在数值键名和length属性),部署Iterator接口的简便方法,就是Symbol.iterator方法直接引用数组的Iterator接口。
let iterable={
0:'a',
1:'b',
2:'c',
length:3,
[Symbol.iterator]:Array.prototype[Symbol.iterator]
};
Symbol.iterator方法的最简单实现是使用Generator函数。
let obj={
* [Symbol.iterator](){ // [Symbol.iterator]:function*()
yield 'hello';
yield 'world';
}
};
2、默认调用Iterator的场合
默认调用Iterator接口,即Symbol.iterator方法。
(1)for...of循环
(2)解构赋值:对数组和 Set 结构进行解构赋值时
(3)扩展运算符...:将任何部署了Iterator接口的数据结构,转为数组
(4)yield*:yield*后面跟的是一个可遍历的结构
(5)其它
由于数组的遍历会调用遍历器接口,所以任何接受数组作为参数的场合,都调用了遍历器接口。for...of、Array.from()、Map(), Set(), WeakMap(), WeakSet()、Promise.all()、Promise.race()等。
3、遍历器对象的return(),throw()
遍历器对象的return和throw方法是否部署是可选的。
如果for...of循环提前退出(出错或有break语句),就会调用return方法。如果一个对象在完成遍历前,需要清理或释放资源,就可以部署return方法。return方法必须返回一个对象。
throw方法主要是配合Generator函数使用,一般的遍历器对象用不到这个方法。
4、for...of循环
十二、Proxy和Reflect
1、Proxy代理器
Proxy代理,“代理”某些操作,修改某些操作的默认行为。在目标对象之前架设一层“拦截”,外界对该对象的访问都必须先通过这层拦截,可以对外界的访问进行过滤和改写。Proxy实际上重载了点运算符,即用自己的定义覆盖了语言的原始定义。
ES6原生提供Proxy构造函数,用来生成Proxy实例。target参数表示要拦截的目标对象,handler参数表示用来定制拦截行为的对象。如果handler没有设置任何拦截,就等同于直接通向原对象。
要使得Proxy起作用,必须针对Proxy实例进行操作,而不是针对目标对象进行操作。
var proxy=new Proxy(target,handler);
var proxy=new Proxy({},{
get:function(target,property){
return 35;
}
});
// 要使得Proxy起作用,必须针对Proxy实例进行操作,而不是针对目标对象进行操作。
proxy.time // 35
// Proxy实例也可以作为其他对象的原型对象。obj对象本身没有time属性,根据原型链会在proxy对象上读取该属性,导致被拦截。
let obj=Object.create(proxy);
obj.time // 35
// 将Proxy对象,设置到object.proxy属性,从而可以在object对象上调用。
var object={proxy:new Proxy(target,handler)};
(1)get(target, propKey, receiver)
拦截某个属性的读取操作,如proxy.foo和proxy['foo']。可以接受三个参数,依次为目标对象、属性名和proxy实例本身(操作行为所针对的对象),最后一个参数可选。
get方法可以继承。
如果一个属性不可配置(configurable)且不可写(writable),则Proxy不能修改该属性,否则通过Proxy对象访问该属性会报错。
(2)set(target, propKey, value, receiver)
拦截某个属性的赋值操作,如proxy.foo=v和proxy['foo']=v。可以接受四个参数,依次为目标对象、属性名、属性值和Proxy实例本身,其中最后一个参数可选,返回一个布尔值。严格模式下,set代理返回false或者undefined,没有返回true,都会报错。
可以数据绑定,即每当对象发生变化时,会自动更新DOM。
如果目标对象自身的某个属性,不可写且不可配置,那么set方法将不起作用。
(3)has(target, propKey)
拦截HasProperty操作,而不是HasOwnProperty操作,判断对象是否具有某个属性时,这个方法会生效。典型的操作是in运算符,但是对for...in循环不生效。接受两个参数,分别是目标对象、需查询的属性名,返回一个布尔值。
如果原对象不可配置或者禁止扩展,这时has拦截会报错。
(4)deleteProperty(target, propKey)
拦截delete proxy[propKey]操作,如果这个方法抛出错误或者返回false,当前属性就无法被delete命令删除。
目标对象自身的不可配置(configurable)的属性,不能被该方法删除,否则报错。
(5)ownKeys(target)
拦截对象自身属性的读取操作,包括Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in循环。返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。
使用Object.keys方法时,有三类属性会被ownKeys方法自动过滤,不会返回。包括目标对象上不存在的属性、属性名为Symbol值、不可遍历(enumerable)的属性。
(6)其它
getOwnPropertyDescriptor(target, propKey):拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性描述对象或undefined。
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)。
let obj={ // 原始对象存储真实的数据
time:"2018-12-24",
name:'yc',
_r:123
};
let proxy=new Proxy(obj,{ // 代理器
get(target,key){
if(key in target){
return target[key];
}else{ // 如果没有拦截函数,访问不存在的属性,只会返回undefined
throw new ReferenceError("Property \""+key+"\" does not exist.");
}
},
set(target,key,value){
if(key==="time"){ // 只有time属性可以赋值修改
target[key]=value;
}
return true;
},
has(target,key){
if(key[0]==='_'){
return false
}
return key in target;
},
deleteProperty(target,key){
if(key[0]==='_'){
return false;
}
delete target[key];
return true;
},
ownKeys(target){
return Reflect.ownKeys(target).filter(key=>key[0]!='_');
}
});
console.log(proxy.time); // "2018-12-24"
console.log(proxy.age); // Uncaught ReferenceError: Property "age" does not exist.
proxy.time="2019-12-25";
console.log(proxy.time); // "2019-12-25"
proxy.name="hsg";
console.log(proxy.name); // "yc"
console.log("name" in proxy); // true
console.log("_r" in proxy); // false
for(let key in proxy){
console.log(key); // time name _r has不起作用
}
delete proxy.time;
delete proxy._r;
console.log(proxy); // {name: "yc", _r: 123}
console.log(Object.keys(proxy)); // ["name"]
2、Reflect
Reflect对象与Proxy对象一样,也是ES6为了操作对象而提供的新API。Reflect对象的设计目的:将Object对象的一些明显属于语言内部的方法放到Reflect对象上;修改某些Object方法的返回结果,让其变得更合理; 让Object操作都变成函数行为;Reflect对象与Proxy对象的方法一一对应。
(1)Reflect.get(target, name, receiver),查找并返回target对象的name属性,如果没有该属性,则返回undefined。
(2)Reflect.set(target, name, value, receiver),设置target对象的name属性等于value。
(3)Reflect.has(obj, name),对应name in obj里面的in运算符。
(4)Reflect.deleteProperty(obj, name),等同于delete obj[name],用于删除对象的属性。如果删除成功或被删除的属性不存在,返回true;删除失败,被删除的属性依然存在,返回false。
(5)Reflect.ownKeys (target),用于返回对象的所有属性,基本等同于Object.getOwnPropertyNames与Object.getOwnPropertySymbols之和。
(6)其它
let obj={
time:"2018-12-24",
name:'yc',
_r:123,
[Symbol('foo')]:456
};
console.log(Reflect.get(obj,"name")); // "yc"
Reflect.set(obj,"name","hsg");
console.log(Reflect.get(obj,"name")); // "hsg"
console.log(Reflect.has(obj,"name")); // true
Reflect.deleteProperty(obj,"name");
console.log(Reflect.get(obj,"name")); // undefined
console.log(Object.getOwnPropertyNames(obj)); // ["time", "_r"]
console.log(Object.getOwnPropertySymbols(obj)); // [Symbol(foo)]
console.log(Reflect.ownKeys(obj)); // ["time", "_r", Symbol(foo)]
如果Proxy对象和Reflect对象联合使用,前者拦截操作,后者完成默认行为。
// 用Proxy和Reflect实现和业务解耦的校验模块
function validator(target,validator){
return new Proxy(target,{
_validator:validator,
set(target,key,value,proxy){
if(target.hasOwnProperty(key)){
let va=this._validator[key];
if(!!va(value)){
return Reflect.set(target,key,value,proxy);
}else{
throw Error(`不能设置${key}到${value}`);
}
}else{
throw Error(`${key}不存在`);
}
}
})
}
const personValidator={
name(val){
return typeof val==='string'
},
age(val){
return typeof val==='number'&&val>=18
}
}
class Person{
constructor(name,age){
this.name=name;
this.age=age;
return validator(this,personValidator);
}
}
const person=new Person("yc",25);
console.log(person); // {name: "yc", age: 25}
person.name=23;
console.log(person); // Uncaught Error: 不能设置name到23