7 set和map数据结构 7.1 Set基础 ES6提供了新的数据结构Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。
Set本身是一个构造函数,用来生成Set数据结构。
1 2 3 4 5 6 7 8 var  s = new  Set ();[2 , 3 , 5 , 4 , 5 , 2 , 2 ].map(x  =>  s.add(x)); for  (let  i of  s) {  console .log(i); } 
 
上面代码通过add方法向Set结构加入成员,结果表明Set结构不会添加重复的值。
初始化 
Set函数可以接受一个数组(或类似数组的对象)作为参数,用来初始化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 var  set = new  Set ([1 , 2 , 3 , 4 , 4 ]);[...set] var  items = new  Set ([1 , 2 , 3 , 4 , 5 , 5 , 5 , 5 ]);items.size  function  divs  ( )  {  return  [...document.querySelectorAll('div' )]; } var  set = new  Set (divs());set.size  divs().forEach(div  =>  set.add(div)); set.size  
 
向Set加入值的时候,不会发生类型转换,所以5和”5”是两个不同的值。 Set内部判断两个值是否不同,使用的算法叫做“Same-value equality”,它类似于精确相等运算符(===),主要的区别是NaN等于自身,而精确相等运算符认为NaN不等于自身。
7.2 Set实例的属性和方法 1. Set属性 
Set.prototype.constructor:构造函数,默认就是Set函数。 
Set.prototype.size:返回Set实例的成员总数。 
 
2. Set操作数据 
Set实例的方法分为两大类:操作方法(用于操作数据)和遍历方法(用于遍历成员)。下面先介绍四个操作方法。
add(value):添加某个值,返回Set结构本身。 
delete(value):删除某个值,返回一个布尔值,表示删除是否成功。 
has(value):返回一个布尔值,表示该值是否为Set的成员。 
clear():清除所有成员,没有返回值。 
 
1 2 3 4 5 6 7 8 9 10 11 s.add(1 ).add(2 ).add(2 ); s.size  s.has(1 )  s.has(2 )  s.has(3 )  s.delete(2 ); s.has(2 )  
 
去除数组重复成员的一种方法
1 2 3 4 5 6 7 8 function  dedupe (array )  {  return  Array .from(new  Set (array)); } dedupe([1 , 1 , 2 , 3 ])  [...(new  Set (array))] 
 
3. 遍历操作 
keys():返回键名的遍历器 
values():返回键值的遍历器 
entries():返回键值对的遍历器 
forEach():使用回调函数遍历每个成员 
 
(1)keys(),values(),entries() 
由于Set结构没有键名,只有键值(或者说键名和键值是同一个值),所以key方法和value方法的行为完全一致。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 let  set = new  Set (['red' , 'green' , 'blue' ]);for  (let  item of  set.keys()) {  console .log(item); } for  (let  item of  set.values()) {  console .log(item); } for  (let  item of  set.entries()) {  console .log(item); } 
Set结构的实例默认可遍历,它的默认遍历器生成函数就是它的values方法。 这意味着,可以省略values方法,直接用for…of循环遍历Set。
1 2 3 4 5 6 7 8 let  set = new  Set (['red' , 'green' , 'blue' ]);for  (let  x of  set) {  console .log(x); } 
 
(2)forEach() 
Set结构的实例的forEach方法,用于对每个成员执行某种操作,没有返回值。
1 2 3 4 5 let  set = new  Set ([1 , 2 , 3 ]);set.forEach((value, key ) =>  console .log(value * 2 ) ) 
 
而且,数组的map和filter方法也可以用于Set了。
1 2 3 4 5 6 7 let  set = new  Set ([1 , 2 , 3 ]);set = new  Set ([...set].map(x  =>  x * 2 )); let  set = new  Set ([1 , 2 , 3 , 4 , 5 ]);set = new  Set ([...set].filter(x  =>  (x % 2 ) == 0 )); 
 
7.3 WeakSet WeakSet结构与Set类似,也是不重复的值的集合。但是,它与Set有两个区别。
如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于WeakSet之中。这个特点意味着,无法引用WeakSet的成员,因此WeakSet是不可遍历的。
WeakSet是一个构造函数,可以使用new命令,创建WeakSet数据结构。
var ws = new WeakSet();
 
WeakSet结构有以下三个方法
WeakSet.prototype.add(value):向WeakSet实例添加一个新成员。 
WeakSet.prototype.delete(value):清除WeakSet实例的指定成员。 
WeakSet.prototype.has(value):返回一个布尔值,表示某个值是否在WeakSet实例之中。 
 
1 2 3 4 5 6 7 8 9 10 11 12 var  ws = new  WeakSet ();var  obj = {};var  foo = {};ws.add(window ); ws.add(obj); ws.has(window );  ws.has(foo);     ws.delete(window ); ws.has(window );     
 
7.4 Map 它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。
1 2 3 4 5 6 7 8 9 var  m = new  Map ();var  o = {p : 'Hello World' };m.set(o, 'content' ) m.get(o)  m.has(o)  m.delete(o)  m.has(o)  
 
作为构造函数,Map也可以接受一个数组作为参数。该数组的成员是一个个表示键值对的数组。
1 2 3 4 5 6 7 8 9 10 var  map = new  Map ([  ['name' , '张三' ],   ['title' , 'Author' ] ]); map.size  map.has('name' )  map.get('name' )  map.has('title' )  map.get('title' )  
 
Map构造函数接受数组作为参数,实际上执行的是下面的算法。1 2 3 4 5 6 var  items = [  ['name' , '张三' ],   ['title' , 'Author' ] ]; var  map = new  Map ();items.forEach(([key, value] ) =>  map.set(key, value)); 
如果对同一个键多次赋值,后面的值将覆盖前面的值。
只有对同一个对象的引用,Map结构才将其视为同一个键。
1 2 3 4 var  map = new  Map ();map.set(['a' ], 555 ); map.get(['a' ])  
 
 
7.5 Map属性和操作方法 1. 属性 
(1)size属性 
size属性返回Map结构的成员总数。
1 2 3 4 5 let  map = new  Map ();map.set('foo' , true ); map.set('bar' , false ); map.size  
 
(2)set(key, value) 
set方法设置key所对应的键值,然后返回整个Map结构。如果key已经有值,则键值会被更新,否则就新生成该键。
1 2 3 4 5 var  m = new  Map ();m.set("edition" , 6 )         m.set(262 , "standard" )      m.set(undefined , "nah" )     
 
(3)get(key) 
get方法读取key对应的键值,如果找不到key,返回undefined。
1 2 3 4 5 6 var  m = new  Map ();var  hello = function ( )  {console .log("hello" );}m.set(hello, "Hello ES6!" )  m.get(hello)   
 
(4)has(key) 
has方法返回一个布尔值,表示某个键是否在Map数据结构中。
1 2 3 4 5 6 7 8 9 10 var  m = new  Map ();m.set("edition" , 6 ); m.set(262 , "standard" ); m.set(undefined , "nah" ); m.has("edition" )      m.has("years" )        m.has(262 )            m.has(undefined )      
 
(5)delete(key) 
delete方法删除某个键,返回true。如果删除失败,返回false。
1 2 3 4 5 6 var  m = new  Map ();m.set(undefined , "nah" ); m.has(undefined )      m.delete(undefined ) m.has(undefined )        
 
(6)clear() 
clear方法清除所有成员,没有返回值。
1 2 3 4 5 6 7 let  map = new  Map ();map.set('foo' , true ); map.set('bar' , false ); map.size  map.clear() map.size  
 
2. 遍历方法 
Map原生提供三个遍历器生成函数和一个遍历方法。
keys():返回键名的遍历器。 
values():返回键值的遍历器。 
entries():返回所有成员的遍历器。 
forEach():遍历Map的所有成员。 
 
Map的遍历顺序就是插入顺序。
(1) keys(),values(),entries() 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 let  map = new  Map ([  ['F' , 'no' ],   ['T' ,  'yes' ], ]); for  (let  key of  map.keys()) {  console .log(key); } for  (let  value of  map.values()) {  console .log(value); } for  (let  item of  map.entries()) {  console .log(item[0 ], item[1 ]); } for  (let  [key, value] of  map.entries()) {  console .log(key, value); } for  (let  [key, value] of  map) {  console .log(key, value); } 
 
(2) 转换数组  Map结构转为数组结构,比较快速的方法是结合使用扩展运算符(…)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 let  map = new  Map ([  [1 , 'one' ],   [2 , 'two' ],   [3 , 'three' ], ]); [...map.keys()] [...map.values()] [...map.entries()] [...map] 
 
(3) 利用数组 
结合数组的map方法、filter方法,可以实现Map的遍历和过滤(Map本身没有map和filter方法)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 let  map0 = new  Map ()  .set(1 , 'a' )   .set(2 , 'b' )   .set(3 , 'c' ); let  map1 = new  Map (  [...map0].filter(([k, v] ) =>  k < 3 ) ); let  map2 = new  Map (  [...map0].map(([k, v] ) =>  [k * 2 , '_'  + v])     ); 
 
(4) forEach 
Map还有一个forEach方法,与数组的forEach方法类似,也可以实现遍历。
1 2 3 map.forEach(function (value, key, map )  {   console .log("Key: %s, Value: %s" , key, value); }); 
 
forEach方法还可以接受第二个参数,用来绑定this。
1 2 3 4 5 6 7 8 9 var  reporter = {  report: function (key, value )  {     console .log("Key: %s, Value: %s" , key, value);   } }; map.forEach(function (value, key, map )  {   this .report(key, value); }, reporter); 
 
上面代码中,forEach方法的回调函数的this,就指向reporter。
3. 与其他数据结构的互相转换 
(1)Map转为数组
前面已经提过,Map转为数组最方便的方法,就是使用扩展运算符(…)。1 2 3 let  myMap = new  Map ().set(true , 7 ).set({foo : 3 }, ['abc' ]);[...myMap] 
(2)数组转为Map
将数组转入Map构造函数,就可以转为Map。1 2 new  Map ([[true , 7 ], [{foo : 3 }, ['abc' ]]])
(3)Map转为对象
如果所有Map的键都是字符串,它可以转为对象。
1 2 3 4 5 6 7 8 9 10 11 function  strMapToObj (strMap )  {  let  obj = Object .create(null );   for  (let  [k,v] of  strMap) {     obj[k] = v;   }   return  obj; } let  myMap = new  Map ().set('yes' , true ).set('no' , false );strMapToObj(myMap) 
 
(4)对象转为Map
1 2 3 4 5 6 7 8 9 10 function  objToStrMap (obj )  {  let  strMap = new  Map ();   for  (let  k of  Object .keys(obj)) {     strMap.set(k, obj[k]);   }   return  strMap; } objToStrMap({yes : true , no : false }) 
 
(5)Map转为JSON
Map转为JSON要区分两种情况。一种情况是,Map的键名都是字符串,这时可以选择转为对象JSON。
1 2 3 4 5 6 7 function  strMapToJson (strMap )  {  return  JSON .stringify(strMapToObj(strMap)); } let  myMap = new  Map ().set('yes' , true ).set('no' , false );strMapToJson(myMap) 
 
另一种情况是,Map的键名有非字符串,这时可以选择转为数组JSON。
1 2 3 4 5 6 7 function  mapToArrayJson (map )  {  return  JSON .stringify([...map]); } let  myMap = new  Map ().set(true , 7 ).set({foo : 3 }, ['abc' ]);mapToArrayJson(myMap) 
 
(6)JSON转为Map
JSON转为Map,正常情况下,所有键名都是字符串。
1 2 3 4 5 6 function  jsonToStrMap (jsonStr )  {  return  objToStrMap(JSON .parse(jsonStr)); } jsonToStrMap('{"yes":true,"no":false}' ) 
 
但是,有一种特殊情况,整个JSON就是一个数组,且每个数组成员本身,又是一个有两个成员的数组。这时,它可以一一对应地转为Map。这往往是数组转为JSON的逆操作。
1 2 3 4 5 6 function  jsonToMap (jsonStr )  {  return  new  Map (JSON .parse(jsonStr)); } jsonToMap('[[true,7],[{"foo":3},["abc"]]]' ) 
 
7.6 WeakMap WeakMap结构与Map结构基本类似,唯一的区别是它只接受对象作为键名(null除外),不接受其他类型的值作为键名,而且键名所指向的对象,不计入垃圾回收机制。
WeakMap应用的典型场合就是DOM节点作为键名
1 2 3 4 5 6 7 8 9 let  myElement = document .getElementById('logo' );let  myWeakmap = new  WeakMap ();myWeakmap.set(myElement, {timesClicked : 0 }); myElement.addEventListener('click' , function ( )  {   let  logoData = myWeakmap.get(myElement);   logoData.timesClicked++; }, false ); 
 
上面代码中,myElement是一个 DOM 节点,每当发生click事件,就更新一下状态。我们将这个状态作为键值放在 WeakMap 里,对应的键名就是myElement。一旦这个 DOM 节点删除,该状态就会自动消失,不存在内存泄漏风险。
 
8 Proxy 和 Reflect 8.1 Proxy 在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截
ES6 原生提供 Proxy 构造函数,用来生成 Proxy 实例。
var proxy = new Proxy(target, handler);
 
new Proxy()表示生成一个Proxy实例, 
target参数表示所要拦截的目标对象, 
handler参数也是一个对象,用来定制拦截行为。 
 
Proxy接受两个参数。
第一个参数是所要代理的目标对象(上例是一个空对象),即如果没有Proxy的介入,操作原来要访问的就是这个对象; 
第二个参数是一个配置对象,对于每一个被代理的操作,需要提供一个对应的处理函数,该函数将拦截对应的操作。 
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 var  obj = new  Proxy ({}, {  get: function  (target, key, receiver )  {     console .log(`getting ${key} !` );     return  Reflect .get(target, key, receiver);   },   set: function  (target, key, value, receiver )  {     console .log(`setting ${key} !` );     return  Reflect .set(target, key, value, receiver);   } }); obj.count = 1  ++obj.count 
 
1 2 3 4 5 6 7 8 9 var  proxy = new  Proxy ({}, {  get: function (target, property )  {     return  35 ;   } }); proxy.time  proxy.name  proxy.title  
 
如果handler没有设置任何拦截,那就等同于直接通向原对象。
1 2 3 4 5 var  target = {};var  handler = {};var  proxy = new  Proxy (target, handler);proxy.a = 'b' ; target.a  
 
Proxy 实例也可以作为其他对象的原型对象。 
1 2 3 4 5 6 7 8 var  proxy = new  Proxy ({}, {  get: function (target, property )  {     return  35 ;   } }); let  obj = Object .create(proxy);obj.time  
 
上面代码中,proxy对象是obj对象的原型,obj对象本身并没有time属性,所以根据原型链,会在proxy对象上读取该属性,导致被拦截。
Proxy 支持的拦截操作一览。
1. get(target, propKey, receiver) 
拦截对象属性的读取
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 var  person = {  name: "张三"  }; var  proxy = new  Proxy (person, {  get: function (target, property )  {     if  (property in  target) {       return  target[property];     } else  {       throw  new  ReferenceError ("Property \""  + property + "\" does not exist." );     }   } }); proxy.name  proxy.age  
 
上面代码表示,如果访问目标对象不存在的属性,会抛出一个错误。如果没有这个拦截函数,访问不存在的属性,只会返回undefined。
get方法可以继承。
1 2 3 4 5 6 7 8 9 let  proto = new  Proxy ({}, {  get(target, propertyKey, receiver) {     console .log('GET ' +propertyKey);     return  target[propertyKey];   } }); let  obj = Object .create(proto);obj.xxx  
 
2.set(target, propKey, value, receiver) 
set方法用来拦截某个属性的赋值操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 let  validator = {  set: function (obj, prop, value )  {     if  (prop === 'age' ) {       if  (!Number .isInteger(value)) {         throw  new  TypeError ('The age is not an integer' );       }       if  (value > 200 ) {         throw  new  RangeError ('The age seems invalid' );       }     }          obj[prop] = value;   } }; let  person = new  Proxy ({}, validator);person.age = 100 ; person.age  person.age = 'young'   person.age = 300   
 
3. has(target, propKey) 
has方法用来拦截HasProperty操作,即判断对象是否具有某个属性时,这个方法会生效。典型的操作就是in运算符。
下面的例子使用has方法隐藏某些属性,不被in运算符发现。
1 2 3 4 5 6 7 8 9 10 11 var  handler = {  has (target, key) {     if  (key[0 ] === '_' ) {       return  false ;     }     return  key in  target;   } }; var  target = { _prop : 'foo' , prop : 'foo'  };var  proxy = new  Proxy (target, handler);'_prop'  in  proxy 
 
4. deleteProperty(target, propKey) 
deleteProperty方法用于拦截delete操作,如果这个方法抛出错误或者返回false,当前属性就无法被delete命令删除。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 var  handler = {  deleteProperty (target, key) {     invariant(key, 'delete' );     return  true ;   } }; function  invariant  (key, action )  {  if  (key[0 ] === '_' ) {     throw  new  Error (`Invalid attempt to ${action}  private "${key} " property` );   } } var  target = { _prop : 'foo'  };var  proxy = new  Proxy (target, handler);delete  proxy._prop
 
上面代码中,deleteProperty方法拦截了delete操作符,删除第一个字符为下划线的属性会报错。5. ownKeys(target) 
拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy),返回一个数组。该方法返回对象所有自身的属性,而Object.keys()仅返回对象可遍历的属性。
6. getOwnPropertyDescriptor(target, propKey) 
getOwnPropertyDescriptor方法拦截Object.getOwnPropertyDescriptor,返回一个属性描述对象或者undefined。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 var  handler = {  getOwnPropertyDescriptor (target, key) {     if  (key[0 ] === '_' ) {       return ;     }     return  Object .getOwnPropertyDescriptor(target, key);   } }; var  target = { _foo : 'bar' , baz : 'tar'  };var  proxy = new  Proxy (target, handler);Object .getOwnPropertyDescriptor(proxy, 'wat' )Object .getOwnPropertyDescriptor(proxy, '_foo' )Object .getOwnPropertyDescriptor(proxy, 'baz' )
 
对于第一个字符为下划线的属性名会返回undefined。
7. defineProperty(target, propKey, propDesc) 
Object.defineProperty(obj, prop, descriptor)
obj 被定义或修改属性的对象;
 
prop 要定义或修改的属性名称;
 
descriptor 对属性的描述;
 
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 var  o = {}; Object .defineProperty(o, 'a' , {  value: 37 ,   writable: true ,   enumerable: true ,   configurable: true  }); var  bValue = 38 ;Object .defineProperty(o, 'b' , {  get: function ( )  { return  bValue; },   set: function (newValue )  { bValue = newValue; },   enumerable: true ,   configurable: true  }); o.b;  Object .defineProperty(o, 'conflict' , {  value: 0x9f91102 ,   get: function ( )  { return  0xdeadbeef ; } }); 
 
defineProperty方法拦截了Object.defineProperty操作。
1 2 3 4 5 6 7 8 9 var  handler = {  defineProperty (target, key, descriptor) {     return  false ;   } }; var  target = {};var  proxy = new  Proxy (target, handler);proxy.foo = 'bar'  
 
上面代码中,defineProperty方法返回false,导致添加新属性会抛出错误。
8. preventExtensions(target) 
拦截Object.preventExtensions(proxy),返回一个布尔值。
9. getPrototypeOf(target) 
拦截Object.getPrototypeOf(proxy),返回一个对象。
10. isExtensible(target) 
拦截Object.isExtensible(proxy),返回一个布尔值。
11. setPrototypeOf(target, proto) 
拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。
如果目标对象是函数,那么还有两种额外操作可以拦截。
1 2 3 4 5 6 7 8 9 10 11 12 var  twice = {  apply (target, ctx, args) {     return  Reflect .apply(...arguments) * 2 ;   } }; function  sum  (left, right )  {  return  left + right; }; var  proxy = new  Proxy (sum, twice);proxy(1 , 2 )  proxy.call(null , 5 , 6 )  proxy.apply(null , [7 , 8 ])  
 
上面代码中,每当执行proxy函数(直接调用或call和apply调用),就会被apply方法拦截。12. apply(target, object, args) 
apply方法拦截函数的调用、call和apply操作
apply方法可以接受三个参数,分别是目标对象、目标对象的上下文对象(this)和目标对象的参数数组。
13. construct(target, args) 
construct方法用于拦截new命令,下面是拦截对象的写法。
construct方法可以接受两个参数。
target: 目标对象 
args:构建函数的参数对象 
 
construct方法返回的必须是一个对象,否则会报错。
1 2 3 4 5 6 7 8 9 10 var  p = new  Proxy (function ( )  {}, {  construct: function (target, args )  {     console .log('called: '  + args.join(', ' ));     return  { value : args[0 ] * 10  };   } }); new  p(1 ).value
 
 
8.2 Proxy案例 1. 抽离校验模块 
这个示例演示了如何使用 Proxy 保障数据类型的准确性:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 let  numericDataStore = {      count: 0 ,     amount: 1234 ,     total: 14  }; numericDataStore = new  Proxy (numericDataStore, {       set(target, key, value, proxy) {         if  (typeof  value !== 'number' ) {             throw  Error ("Properties in numericDataStore can only be numbers" );         }         return  Reflect .set(target, key, value, proxy);     } }); numericDataStore.count = "foo" ; numericDataStore.count = 333 ; 
使用 Proxy 则可以将校验器从核心逻辑分离出来自成一体
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 function  createValidator (target, validator )  {      return  new  Proxy (target, {         _validator: validator,         set(target, key, value, proxy) {                          if  (target.hasOwnProperty(key)) {                                  let  validator = this ._validator[key];                                  if  (!!validator(value)) {                     return  Reflect .set(target, key, value, proxy);                 } else  {                     throw  Error (`Cannot set ${key}  to ${value} . Invalid.` );                 }             } else  {                 throw  Error (`${key}  is not a valid property` )             }         }     }); } const  personValidators = {      name(val) {         return  typeof  val === 'string' ;     },     age(val) {         return  typeof  age === 'number'  && age > 18 ;     } } class  Person   {      constructor (name, age) {         this .name = name;         this .age = age;         return  createValidator(this , personValidators);     } } const  bill = new  Person('Bill' , 25 );bill.name = 0 ;   bill.age = 'Bill' ;   bill.age = 15 ; 
 
2. 私有属性 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 let  api = {      _apiKey: '123abc456def' ,     getUsers: function ( ) { },      getUser: function (userId ) { },      setUser: function (userId, config ) { } }; const  RESTRICTED = ['_apiKey' ];api = new  Proxy (api, {       get(target, key, proxy) {                  if (RESTRICTED.indexOf(key) > -1 ) {             throw  Error (`${key}  is restricted. Please see api documentation for further info.` );         }         return  Reflect .get(target, key, proxy);     },     set(target, key, value, proxy) {                   if (RESTRICTED.indexOf(key) > -1 ) {             throw  Error (`${key}  is restricted. Please see api documentation for further info.` );         }         return  Reflect .get(target, key, value, proxy);     } }); console .log(api._apiKey);api._apiKey = '987654321' ; 
 
第二种方法是使用 has 拦截 in 操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 var  api = {      _apiKey: '123abc456def' ,     getUsers: function ( ) { },      getUser: function (userId ) { },      setUser: function (userId, config ) { } }; const  RESTRICTED = ['_apiKey' ];api = new  Proxy (api, {       has(target, key) {         return  (RESTRICTED.indexOf(key) > -1 ) ?             false  :             Reflect .has(target, key);     } }); console .log("_apiKey"  in  api);  for  (var  key in  api) {      if  (api.hasOwnProperty(key) && key === "_apiKey" ) {         console .log("This will never be logged because the proxy obscures _apiKey..." )     } } 
 
3. 访问日志 
对于那些调用频繁、运行缓慢或占用执行环境资源较多的属性或接口,开发者会希望记录它们的使用情况或性能表现,这个时候就可以使用 Proxy 充当中间件的角色,轻而易举实现日志功能:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 let  api = {      _apiKey: '123abc456def' ,     getUsers: function ( )  {  },     getUser: function (userId )  {  },     setUser: function (userId, config )  {  } }; function  logMethodAsync (timestamp, method )  {      setTimeout(function ( )  {         console .log(`${timestamp}  - Logging ${method}  request asynchronously.` );     }, 0 ) } api = new  Proxy (api, {       get: function (target, key, proxy )  {         var  value = target[key];                  return  function ( )  {                          logMethodAsync(new  Date (), key);                                       return  Reflect .apply(value, target, arguments );         };     } }); api.getUsers();  api.getUser();  
 
4. 预警和拦截 
假设你不想让其他开发者删除 noDelete 属性,还想让调用 oldMethod 的开发者了解到这个方法已经被废弃了,或者告诉开发者不要修改 doNotChange 属性,那么就可以使用 Proxy 来实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 let  dataStore = {      noDelete: 1235 ,     oldMethod: function ( )  { },     doNotChange: "tried and true"  }; const  NODELETE = ['noDelete' ];  const  NOCHANGE = ['doNotChange' ];const  DEPRECATED = ['oldMethod' ];  dataStore = new  Proxy (dataStore, {       set(target, key, value, proxy) {         if  (NOCHANGE.includes(key)) {             throw  Error (`Error! ${key}  is immutable.` );         }         return  Reflect .set(target, key, value, proxy);     },     deleteProperty(target, key) {         if  (NODELETE.includes(key)) {             throw  Error (`Error! ${key}  cannot be deleted.` );         }         return  Reflect .deleteProperty(target, key);     },     get(target, key, proxy) {         if  (DEPRECATED.includes(key)) {             console .warn(`Warning! ${key}  is deprecated.` );         }         var  val = target[key];                  return  typeof  val === 'function'  ?             function (...args )  {                 Reflect .apply(target[key], target, args);             } :             val;     } }); dataStore.doNotChange = "foo" ;   delete  dataStore.noDelete;  dataStore.oldMethod(); 
 
8.3 this 问题 虽然 Proxy 可以代理针对目标对象的访问,但它不是目标对象的透明代理,即不做任何拦截的情况下,也无法保证与目标对象的行为一致。主要原因就是在 Proxy 代理的情况下,目标对象内部的this关键字会指向 Proxy 代理。
1 2 3 4 5 6 7 8 9 10 11 const  target = {  m: function  ( )  {     console .log(this  === proxy);   } }; const  handler = {};const  proxy = new  Proxy (target, handler);target.m()  proxy.m()   
 
上面代码中,一旦proxy代理target.m,后者内部的this就是指向proxy,而不是target。
8.4 Reflect Reflect对象与Proxy对象一样,也是ES6为了操作对象而提供的新API。Reflect对象的设计目的有这样几个。
(1) 将Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上。现阶段,某些方法同时在Object和Reflect对象上部署,未来的新方法将只部署在Reflect对象上。
(2) 修改某些Object方法的返回结果,让其变得更合理。比如,Object.defineProperty(obj, name, desc)在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj, name, desc)则会返回false。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 try  {  Object .defineProperty(target, property, attributes);    } catch  (e) {    } if  (Reflect .defineProperty(target, property, attributes)) {   } else  {    } 
 
(3) 让Object操作都变成函数行为。某些Object操作是命令式,比如name in obj和delete obj[name],而Reflect.has(obj, name)和Reflect.deleteProperty(obj, name)让它们变成了函数行为。
1 2 3 4 5 'assign'  in  Object  Reflect .has(Object , 'assign' ) 
 
(4)Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。这就让Proxy对象可以方便地调用对应的Reflect方法,完成默认行为,作为修改行为的基础。也就是说,不管Proxy怎么修改默认行为,你总可以在Reflect上获取默认行为。
1 2 3 4 5 6 7 8 9 Proxy (target, {  set: function (target, name, value, receiver )  {     var  success = Reflect .set(target,name, value, receiver);     if  (success) {       log('property '  + name + ' on '  + target + ' set to '  + value);     }     return  success;   } }); 
 
上面代码中,Proxy方法拦截target对象的属性赋值行为。它采用Reflect.set方法将值赋值给对象的属性,然后再部署额外的功能。
下面是另一个例子。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 var  loggedObj = new  Proxy (obj, {  get(target, name) {     console .log('get' , target, name);     return  Reflect .get(target, name);   },   deleteProperty(target, name) {     console .log('delete'  + name);     return  Reflect .deleteProperty(target, name);   },   has(target, name) {     console .log('has'  + name);     return  Reflect .has(target, name);   } }); 
 
上面代码中,每一个Proxy对象的拦截操作(get、delete、has),内部都调用对应的Reflect方法,保证原生行为能够正常执行。添加的工作,就是将每一个操作输出一行日志。
有了Reflect对象以后,很多操作会更易读。
1 2 3 4 5 Function .prototype.apply.call(Math .floor, undefined , [1.75 ]) Reflect .apply(Math .floor, undefined , [1.75 ]) 
 
9 Iterator和for…of循环 9.1 Iterator(遍历器) 遍历器(Iterator)就是这样一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署Iterator接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。
Iterator的作用有三个:
一是为各种数据结构,提供一个统一的、简便的访问接口; 
二是使得数据结构的成员能够按某种次序排列; 
三是ES6创造了一种新的遍历命令for…of循环,Iterator接口主要供for…of消费。 
 
Iterator的遍历过程是这样的。
(1)创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。
 
(2)第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员。
 
(3)第二次调用指针对象的next方法,指针就指向数据结构的第二个成员。
 
(4)不断调用指针对象的next方法,直到它指向数据结构的结束位置。
 
 
每一次调用next方法,都会返回数据结构的当前成员的信息。具体来说,就是返回一个包含value和done两个属性的对象。其中,
value属性是当前成员的值, 
done属性是一个布尔值,表示遍历是否结束。 
 
下面是一个模拟next方法返回值的例子。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 var  it = makeIterator(['a' , 'b' ]);it.next()  it.next()  it.next()  function  makeIterator (array )  {  var  nextIndex = 0 ;   return  {     next: function ( )  {       return  nextIndex < array.length ?         {value : array[nextIndex++], done : false } :         {value : undefined , done : true };     }   }; } 
 
对于遍历器对象来说,done: false和value: undefined属性都是可以省略的,因此上面的makeIterator函数可以简写成下面的形式。
1 2 3 4 5 6 7 8 9 10 function  makeIterator (array )  {  var  nextIndex = 0 ;   return  {     next: function ( )  {       return  nextIndex < array.length ?         {value : array[nextIndex++]} :         {done : true };     }   }; } 
 
凡是部署了Symbol.iterator属性的数据结构,就称为部署了遍历器接口。调用这个接口,就会返回一个遍历器对象。
一个数据结构只要具有Symbol.iterator属性,就可以认为是“可遍历的”(iterable)。
1 2 3 4 5 6 7 let  arr = ['a' , 'b' , 'c' ];let  iter = arr[Symbol .iterator]();iter.next()  iter.next()  iter.next()  iter.next()  
 
上面代码中,变量arr是一个数组,原生就具有遍历器接口,部署在arr的Symbol.iterator属性上面。所以,调用这个属性,就得到遍历器对象。
9.2 添加Iterator接口 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 let  obj = {  data: [ 'hello' , 'world'  ],   [Symbol .iterator]() {     const  self = this ;     let  index = 0 ;     return  {       next() {         if  (index < self.data.length) {           return  {             value: self.data[index++],             done: false            };         } else  {           return  { value : undefined , done : true  };         }       }     };   } }; 
 
对于类似数组的对象(存在数值键名和length属性),部署Iterator接口,有一个简便方法,就是Symbol.iterator方法直接引用数组的Iterator接口。
1 2 3 4 5 NodeList.prototype[Symbol .iterator] = Array .prototype[Symbol .iterator]; NodeList.prototype[Symbol .iterator] = [][Symbol .iterator]; [...document.querySelectorAll('div' )]  
 
下面是类似数组的对象调用数组的Symbol.iterator方法的例子。
1 2 3 4 5 6 7 8 9 10 let  iterable = {  0 : 'a' ,   1 : 'b' ,   2 : 'c' ,   length: 3 ,   [Symbol .iterator]: Array .prototype[Symbol .iterator] }; for  (let  item of  iterable) {  console .log(item);  } 
 
注意,普通对象部署数组的Symbol.iterator方法,并无效果。
1 2 3 4 5 6 7 8 9 10 let  iterable = {  a: 'a' ,   b: 'b' ,   c: 'c' ,   length: 3 ,   [Symbol .iterator]: Array .prototype[Symbol .iterator] }; for  (let  item of  iterable) {  console .log(item);  } 
 
 
9.3 遍历器的return() 遍历器对象除了具有next方法,还可以具有return方法和throw方法。
return方法的使用场合是,如果for…of循环提前退出(通常是因为出错,或者有break语句或continue语句),就会调用return方法。 如果一个对象在完成遍历前,需要清理或释放资源,就可以部署return方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function  readLinesSync (file )  {  return  {     next() {       if  (file.isAtEndOfFile()) {         file.close();         return  { done : true  };       }     },     return () {       file.close();       return  { done : true  };     },   }; } 
 
上面代码中,函数readLinesSync接受一个文件对象作为参数,返回一个遍历器对象,其中除了next方法,还部署了return方法。下面,我们让文件的遍历提前返回,这样就会触发执行return方法。
1 2 3 4 for  (let  line of  readLinesSync(fileName)) {  console .log(line);   break ; } 
 
注意,return方法必须返回一个对象,这是Generator规格决定的。
 
9.4 for…of循环 for…of循环可以使用的范围包括数组、Set 和 Map 结构、某些类似数组的对象,以及字符串。
1. 数组 
1 2 3 4 5 6 7 8 9 10 11 12 const  arr = ['red' , 'green' , 'blue' ];for (let  v of  arr) {  console .log(v);  } const  obj = {};obj[Symbol .iterator] = arr[Symbol .iterator].bind(arr); for (let  v of  obj) {  console .log(v);  } 
 
上面代码中,空对象obj部署了数组arr的Symbol.iterator属性,结果obj的for…of循环,产生了与arr完全一样的结果。
2. Set和Map结构 
Set和Map结构也原生具有Iterator接口,可以直接使用for…of循环。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 var  engines = new  Set (["Gecko" , "Trident" , "Webkit" , "Webkit" ]);for  (var  e of  engines) {  console .log(e); } var  es6 = new  Map ();es6.set("edition" , 6 ); es6.set("committee" , "TC39" ); es6.set("standard" , "ECMA-262" ); for  (var  [name, value] of  es6) {  console .log(name + ": "  + value); } 
 
3. 计算生成的数据结构 
有些数据结构是在现有数据结构的基础上,计算生成的。比如,ES6的数组、Set、Map都部署了以下三个方法,调用后都返回遍历器对象。
entries() 返回一个遍历器对象,用来遍历[键名, 键值]组成的数组。对于数组,键名就是索引值;对于Set,键名与键值相同。Map结构的iterator接口,默认就是调用entries方法。 
keys() 返回一个遍历器对象,用来遍历所有的键名。 
values() 返回一个遍历器对象,用来遍历所有的键值。 
 
1 2 3 4 5 6 7 let  arr = ['a' , 'b' , 'c' ];for  (let  pair of  arr.entries()) {  console .log(pair); } 
 
4. 类似数组的对象 
类似数组的对象包括好几类。下面是for…of循环用于字符串、DOM NodeList对象、arguments对象的例子。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 let  str = "hello" ;for  (let  s of  str) {  console .log(s);  } let  paras = document .querySelectorAll("p" );for  (let  p of  paras) {  p.classList.add("test" ); } function  printArgs ( )  {  for  (let  x of  arguments ) {     console .log(x);   } } printArgs('a' , 'b' ); 
 
并不是所有类似数组的对象都具有iterator接口,一个简便的解决方法,就是使用Array.from方法将其转为数组。
1 2 3 4 5 6 7 8 9 10 11 let  arrayLike = { length : 2 , 0 : 'a' , 1 : 'b'  };for  (let  x of  arrayLike) {  console .log(x); } for  (let  x of  Array .from(arrayLike)) {  console .log(x); } 
 
9.5 对象 对于普通的对象,for…of结构不能直接使用,会报错,必须部署了iterator接口后才能使用。但是,这样情况下,for…in循环依然可以用来遍历键名。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 var  es6 = {  edition: 6 ,   committee: "TC39" ,   standard: "ECMA-262"  }; for  (let  e in  es6) {  console .log(e); } for  (let  e of  es6) {  console .log(e); } 
 
上面代码表示,对于普通的对象,for…in循环可以遍历键名,for…of循环会报错。
Object.keys() 方法会返回一个由一个给定对象的自身可枚举属性组成的数组,数组中属性名的排列顺序和使用
1 2 3 4 5 6 7 8 9 10 11 var  arr = ['a' , 'b' , 'c' ];console .log(Object .keys(arr)); var  obj = { 0 : 'a' , 1 : 'b' , 2 : 'c'  };console .log(Object .keys(obj)); var  anObj = { 100 : 'a' , 2 : 'b' , 7 : 'c'  };console .log(Object .keys(anObj)); 
 
一种解决方法是,使用Object.keys方法将对象的键名生成一个数组,然后遍历这个数组。
1 2 3 4 5 6 7 let  arrayLike = { length : 2 , 0 : 'a' , 1 : 'b'  };for  (var  key of  Object .keys(arrayLike)) {  console .log(key + ": "  + arrayLike[key]); } 
 
10 Generator 函数 Generator函数是ES6提供的一种异步编程解决方案,语法行为与传统函数完全不同。
两个特征:
一是,function关键字与函数名之间有一个星号; 
二是,函数体内部使用yield语句,定义不同的内部状态 
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 function * helloWorldGenerator ( )  {  yield  'hello' ;   yield  'world' ;   return  'ending' ; } var  hw = helloWorldGenerator();hw.next() hw.next() hw.next() hw.next() 
 
第一次调用,Generator函数开始执行,直到遇到第一个yield语句为止。next方法返回一个对象,它的value属性就是当前yield语句的值hello,done属性的值false,表示遍历还没有结束。
 
第二次调用,Generator函数从上次yield语句停下的地方,一直执行到下一个yield语句。next方法返回的对象的value属性就是当前yield语句的值world,done属性的值false,表示遍历还没有结束。
 
第三次调用,Generator函数从上次yield语句停下的地方,一直执行到return语句(如果没有return语句,就执行到函数结束)。next方法返回的对象的value属性,就是紧跟在return语句后面的表达式的值(如果没有return语句,则value属性的值为undefined),done属性的值true,表示遍历已经结束。
 
第四次调用,此时Generator函数已经运行完毕,next方法返回对象的value属性为undefined,done属性为true。以后再调用next方法,返回的都是这个值。
 
 
由于Generator函数返回的遍历器对象,只有调用next方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。yield语句就是暂停标志。
遍历器对象的next方法的运行逻辑如下。
(1)遇到yield语句,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。
 
(2)下一次调用next方法时,再继续往下执行,直到遇到下一个yield语句。
 
(3)如果没有再遇到新的yield语句,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值。
 
(4)如果该函数没有return语句,则返回的对象的value属性值为undefined。
 
 
需要注意的是,yield语句后面的表达式,只有当调用next方法、内部指针指向该语句时才会执行,因此等于为JavaScript提供了手动的“惰性求值”(Lazy Evaluation)的语法功能。
1 2 3 4 5 6 7 8 9 function * f ( )  {  console .log('执行了!' ) } var  generator = f();setTimeout(function  ( )  {   generator.next() }, 2000 ); 
 
上面代码中,函数f如果是普通函数,在为变量generator赋值时就会执行。但是,函数f是一个Generator函数,就变成只有调用next方法时,函数f才会执行。另外需要注意,yield语句不能用在普通函数中,否则会报错。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 var  arr = [1 , [[2 , 3 ], 4 ], [5 , 6 ]];var  flat = function * (a )  {  var  length = a.length;   for  (var  i = 0 ; i < length; i++) {     var  item = a[i];     if  (typeof  item !== 'number' ) {       yield * flat(item);     } else  {       yield  item;     }   } }; for  (var  f of  flat(arr)) {  console .log(f); } 
 
10.3 与Iterator接口 由于Generator函数就是遍历器生成函数,因此可以把Generator赋值给对象的Symbol.iterator属性,从而使得该对象具有Iterator接口。
1 2 3 4 5 6 7 8 var  myIterable = {};myIterable[Symbol .iterator] = function * ( )  {   yield  1 ;   yield  2 ;   yield  3 ; }; [...myIterable]  
 
上面代码中,Generator函数赋值给Symbol.iterator属性,从而使得myIterable对象具有了Iterator接口,可以被…运算符遍历了。
Generator函数执行后,返回一个遍历器对象。该对象本身也具有Symbol.iterator属性,执行后返回自身。
10.4 next方法的参数 yield句本身没有返回值,或者说总是返回undefined。next方法可以带一个参数,该参数就会被当作上一个yield语句的返回值。
1 2 3 4 5 6 7 8 9 10 11 12 function * f ( )  {  for (var  i=0 ; true ; i++) {     var  reset = yield  i;     if (reset) { i = -1 ; }   } } var  g = f();g.next()  g.next()  g.next(true )  
 
上面代码先定义了一个可以无限运行的Generator函数f,如果next方法没有参数,每次运行到yield语句,变量reset的值总是undefined。当next方法带一个参数true时,当前的变量reset就被重置为这个参数(即true),因此i会等于-1,下一轮循环就会从-1开始递增。