ES6语法——Set、Map、遍历器、代理、Reflect

http://es6.ruanyifeng.com/阮一峰ES6

目录

十、Set和Map数据结构

    1、Set    2、WeakSet    3、Map    4、WeakMap

十一、Iterator和for...of循环

    1、Iterator的部署和调用    2、默认调用Iterator的场合    3、遍历器对象的return(),throw()    4、for...of循环

十二、Proxy和Reflect

    1、Proxy代理器    2、Reflect

十、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

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章