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开始递增。