ES6提供了新的数据结构Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。之前的博文曾阐述过使用ES5实现JavaScript数据结构-集合。
new Set([iterable]);
var items = new Set([1,2,3,4,5,5,5,5]);
console.log(items.size); // 5
console.log(items); // Set {1, 2, 3, 4, 5}
console.log([...items]); // [1, 2, 3, 4, 5]
方法 | 说明 |
---|---|
add(value) | 在Set对象尾部添加一个元素,返回该Set对象 |
clear() | 移除Set对象内的所有元素,没有返回值 |
delete(value) | 移除Set的中与这个值相等的元素,返回一个布尔值,表示删除是否成功 |
has(value) | 返回一个布尔值,表示该值是否为Set的成员 |
keys()/values() | 返回一个新的迭代器对象,该对象包含Set对象中的按插入顺序排列的所有元素的值 |
entries() | 返回一个新的迭代器对象,该对象包含Set对象中的按插入顺序排列的所有元素的值的[value, value]数组 |
forEach(callbackFn[, thisArg]) | 按照插入顺序,为Set对象中的每一个值调用一次callBackFn。如果提供了thisArg参数,回调中的this会是这个参数 |
示例:去除数组的重复成员
var ary = [1, 2, 3, 3, 2, 1, "1"];
[...new Set(ary)]; // [1, 2, 3, "1"]
注意:向Set加入值的时候,不会发生类型转换,所以5和”5”是两个不同的值。
WeakSet结构与Set类似,WeakMap结构与Map结构基本类似。WeakSet的成员只能是对象;WeakSet中的对象都是弱引用,即垃圾回收机制不考虑WeakSet中对象的引用,其意味着WeakSet不可遍历、不会引发内存泄漏。
var ws = new WeakSet();
var obj = {};
var foo = {};
ws.add(window);
ws.add(obj);
ws.has(window); // true
ws.has(foo); // false,foo尚未添加到集合中
ws.delete(window); //从集合中删除窗口
ws.has(window); // false,窗口已被删除
Map数据结构是一个简单的键/值映射。解决了对象中只能用字符串当键的限制(对象和原始值都可以用作键或值)。
方法 | 说明 |
---|---|
size | 返回成员总数 |
set(key, value) | 返回整个Map结构。如果key已经有值,则键值会被更新,否则就新生成该键。 |
get(key) | 读取key对应的键值,如果找不到key,返回undefined |
has(key) | 返回一个布尔值,表示某个键是否在Map数据结构中 |
delete(key) | 删除某个键,返回true。如果删除失败,返回false |
clear() | 清除所有成员,没有返回值 |
keys() | 返回由key组成的新Iterator对象 |
values() | 返回由value组成的新Iterator对象 |
entries() | 返回由[key, value]组成的新Iterator对象 |
forEach() | 遍历Map所有成员 |
let map = new Map();
map.set('stringkey', '123');
map.set('objectKey', {a: '123'});
for(let item of map.entries()) {
console.log(item);
}
注意: map.entries()
和map
在迭代中效果一致;区别是前者返回新的Iterator对象(具有next()方法),而map只是部署了Symbol.iterator接口所有可以遍历。通过map[Symbol.iterator]()
可以获取map的遍历器对象。
(1)Map转为数组
let myMap = new Map().set(true, 7).set({foo: 3}, ['abc']);
[...myMap]
(2)数组转为Map
new Map([[true, 7], [{foo: 3}, ['abc']]])
(3)Map转为对象
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
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。
function strMapToJson(strMap) {
return JSON.stringify(strMapToObj(strMap));
}
let myMap = new Map().set('yes', true).set('no', false);
strMapToJson(myMap);
另一种情况是,Map的键名有非字符串,这时可以选择转为数组JSON。
function mapToArrayJson(map) {
return JSON.stringify([...map]);
}
let myMap = new Map().set(true, 7).set({foo: 3}, ['abc']);
mapToArrayJson(myMap);
(6)JSON转为Map
键名都是字符串
function jsonToStrMap(jsonStr) {
return objToStrMap(JSON.parse(jsonStr));
}
jsonToStrMap('{"yes":true,"no":false}');
WeakMap结果与Map结构基本一致,区别在于WeakMap只接受对象作为键名(null除外),而且其键名所指向的对象不计入垃圾回收机制(弱类型)。
ES6之前表示“集合”的数据结构,主要是数组和对象,ES6中新增了Map和Set。需要一种统一的接口机制来处理所有不同的数据结构。遍历器(Iterator)就是这样一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制。
Iterator的作用有三个:一是为各种数据结构,提供一个统一的、简便的访问接口;二是使得数据结构的成员能够按某种次序排列;三是ES6创造了一种新的遍历命令for…of循环,Iterator接口主要供for…of消费。
function makeIterator(ary){
var nextIndex = 0;
return {
next: function(){
return nextIndex < ary.length ?
{ value: ary[nextIndex++], done: false } :
{ value: undefined, done: true };
}
};
}
var it = makeIterator(["1", "2"]);
it.next(); // Object {value: "1", done: false}
it.next(); // Object {value: "2", done: false}
it.next(); // Object {value: undefined, done: true}
遍历器(Iterator)本质上是一个指针对象,指向当前数据结构的起始位置。第一次调用对象的next方法,指针指向数据结构的第一个成员;第二次调用next方法,指针指向数据结构的第二个成员;一次类推,直到指向数据结构的结束位置。
在ES6中,有些数据结构原生具备Iterator接口(比如数组),即不用任何处理,就可以被for…of循环遍历,有些就不行(比如对象)。原因在于,这些数据结构原生部署了Symbol.iterator属性。在ES6中,有三类数据结构原生具备Iterator接口:数组、某些类似数组的对象、Set和Map结构。调用Symbol.iterator接口,就会返回一个遍历器对象。
let arr = ['a', 'b', 'c'];
let iter = arr[Symbol.iterator]();
iter.next();
一些场景会默认调用Iterator接口(即Symbol.iterator方法)
(1)解构赋值
let set = new Set();
set.add(1).add(2);
let [x, y] = set;
console.log(x, y); // 1 2
(2)扩展运算符
let str = 'hello';
console.log([...str]); // ["h", "e", "l", "l", "o"]
(3)其他场合
由于数组的遍历都会调用遍历器接口,所以任何接受数组作为参数的场合其实都调用了遍历器接口。
ES6引入了一种新的原始数据类型Symbol,表示独一无二(互不等价)的值。Symbol出现之前,我们会经常遇到多个不相同的库操作的DOM属性相同,导致第三方库无法正常运行。Symbol解决了“对象属性名都是字符串、数字,这容易造成属性名的冲突”的问题。
Symbol([description]) // 并不是构造函数,不能使用new
很多开发者会误认为Symbol值与我们为其设定的描述有关,其实这个描述仅仅起到了描述的作用,而不会对Symbol值本身起到任何的改变作用。
let s = Symbol();
typeof s; // "symbol"
const a = Symbol('123');
const b = Symbol('123');
console.log(a == b); // false
Symbol是一种值类型而非引用类型。其意味着将Symbol值作为函数形式传递时,将会进行复制值传递而非引用传递。同时需要注意:Symbol作为属性名,该属性不会出现在for…in、for…of循环中,也不会被Object.keys()
、Object.getOwnPropertyNames()
返回。但是,它也不是私有属性,有一个Object.getOwnPropertySymbols
方法,可以获取指定对象的所有Symbol属性名。
var obj = {};
var a = Symbol('a');
var b = Symbol.for('b');
obj[a] = 'Hello';
obj[b] = 'World';
var objectSymbols = Object.getOwnPropertySymbols(obj);
objectSymbols; // [Symbol(a), Symbol(b)]
Symbol.for()
接受一个字符串作为参数,然后搜索有没有以该参数作为名称的Symbol值。如果有,就返回这个Symbol值,否则就新建并返回一个以该字符串为名称的Symbol值。
const a = Symbol.for('123');
const b = Symbol.for('123');
console.log(a === b); // true
Symbol.keyFor方法返回一个已登记的Symbol类型值的key。
var s1 = Symbol.for("foo");
Symbol.keyFor(s1) // "foo"
(1)Symbol.iterator
@@iterator
用于为对象定义一个方法并返回一个属于所对应对象的迭代器,该迭代器会被for-of语句使用。
let arr = ['a', 'b', 'c'];
let iter = arr[Symbol.iterator]();
iter.next() // { value: 'a', done: false }
iter.next() // { value: 'b', done: false }
iter.next() // { value: 'c', done: false }
iter.next() // { value: undefined, done: true }
class RangeIterator {
constructor(start, stop) {
this.start = start;
this.stop = stop;
}
[Symbol.iterator]() {
return this;
}
next() {
if(this.start < this.stop) {
this.start++;
return {done: false, value: this.start};
}else {
return {done: true, value: undefined};
}
}
}
for(let index of new RangeIterator(0, 3)){
console.log(index); // 1 2 3
}
(2)Symbol.hasInstance
对象的Symbol.hasInstance
属性,指向一个内部方法。当其他对象使用instanceof
运算符,判断是否为该对象的实例时,会调用这个方法。
class Even {
static [Symbol.hasInstance](obj) {
return Number(obj) % 2 === 0;
}
}
1 instanceof Even; // false
2 instanceof Even; // true
(3)Symbol.match
对象的Symbol.match
属性,指向一个函数。当执行str.match(myObject)
时,如果该属性存在,会调用它,返回该方法的返回值。
"ligang".match(/g/) // ["g", index: 2, input: "ligang"]
// 等价于
(/g/)[Symbol.match]("ligang") // ["g", index: 2, input: "ligang"]
class Matcher {
constructor(value){
this.value = value;
}
[Symbol.match](str) {
return this.value.indexOf(str);
}
}
'g'.match(new Matcher('ligang')); // 2
(4)Symbol.replace
对象的Symbol.replace
属性,指向一个方法,当该对象被String.prototype.replace
方法调用时,会返回该方法的返回值。
String.prototype.replace(searchValue, replaceValue)
// 等同于
searchValue[Symbol.replace](this, replaceValue)
(5)Symbol.search
对象的Symbol.search
属性,指向一个方法,当该对象被String.prototype.search
方法调用时,会返回该方法的返回值。
String.prototype.search(regexp)
// 等同于
regexp[Symbol.search](this)
(6)Symbol.split
对象的Symbol.split
属性,指向一个方法,当该对象被String.prototype.split
方法调用时,会返回该方法的返回值。
String.prototype.split(separator, limit)
// 等同于
separator[Symbol.split](this, limit)
Proxy可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写,属于一种“元编程”。(注意,ES5无法模拟该特性)。
元编程重点在于:在一个程序的内容、运行环境、配置等都不做任何修改的情况下,可以通过其他程序对其进行读取或修改。
ES5中,可以通过Getter/Setter定义其运行的逻辑或将要返回的值。
var obj = {
_name: '',
prefix: '',
get name() {
return this.prefix + this._name;
},
set name(val) {
this._name = val;
}
};
obj.name = 'ligang';
console.log(obj.name); // 'ligang'
obj.prefix = 'hello ';
console.log(obj.name); // 'hello ligang'
Getter/Setter不能对变量的所有行为进行拦截,Proxy提供了这样的能力。
Proxy并不是以语法形成使用,而是一种包装器的形式使用。
Syntax: new Proxy(target, handler)
target
参数表示所要拦截的目标对象handler
参数也是一个对象,用来定制拦截行为属性键(监听参数) | 监听内容 |
---|---|
has(target, propKey) | 监听in语句的使用 |
get(target, propKey, receiver) | 监听目标对象的属性读取 |
set(target, propKey, value, receiver) | 监听目标对象的属性赋值 |
deleteProperty(target, propKey) | 监听delete语句对目标对象的删除属性行为 |
ownKeys(target) | 监听Object.getOwnPropertyNames()的读取 |
apply(target, object, args) | 监听目标函数的调用行为 |
construct(target, args) | 监听目标构造函数利用new而生成实例的行为 |
getPrototypeOf(target) | 监听Object.getPrototypeOf()的读取 |
setPrototypeOf(target, proto) | 监听Object.setPrototypeOf()的调用 |
isExtensible(target) | 监听Object.isExtensible()的读取 |
preventExtensions(target) | 监听Object.preventExtensions()的读取 |
getOwnPropertyDescriptor(target, propKey) | 监听Object.getOwnPropertyDescriptor()的读取 |
defineProperty(target, propKey) | 监听Object.defineProperty()的调用 |
let obj = {
foo: 1,
bar: 2
};
(1)has(target, propKey)
let p = new Proxy(obj, {
has(target, propKey) {
console.log(`检查属性${propKey}是否在目标对象中`);
return propKey in target;
}
});
'foo' in p;
Object.preventExtensions(obj);
Object.preventExtensions(obj);
let p2 = new Proxy(obj, {
has(target, propKey) {
console.log(`检查属性${propKey}是否在目标对象中`);
return false;
}
});
console.log('foo' in p2); // TypeError: 'has' on proxy: trap returned falsish for property 'foo' but the proxy target is not extensible
(2)get(target, propKey, receiver)
当目标对象被读取的属性的configurable和writable属性为false时,监听方法最后返回值必须与目标对象的原属性值一直。
(3)construct(target, args)
该监听方法所返回值必须是一个对象,否则会抛出一个TypeError错误。
Proxy.revocable
方法返回一个可取消的 Proxy 实例。
let {proxy, revoke} = Proxy.revocable(target, handler);
Proxy.revocable
的一个使用场景是,目标对象不允许直接访问,必须通过代理访问,一旦访问结束,就收回代理权,不允许再次访问。
let target = {
bar: 1
};
let handler = {};
let {proxy, revoke} = Proxy.revocable(target, handler);
proxy.foo = 123;
proxy.foo // 123
revoke(); // 当执行revoke函数之后,再访问Proxy实例,就会抛出一个错误
proxy.foo; // TypeError: Cannot perform 'get' on a proxy that has been revoked
proxy.bar; // TypeError: Cannot perform 'get' on a proxy that has been revoked
(1)对象属性自动填充
const obj = {};
obj.foo.name = 'foo'; // Uncaught TypeError: Cannot set property 'name' of undefined
const p = new Proxy(obj, {
get(target, propKey, receiver) {
if(!(propKey in target)) {
target[propKey] = obj; // 注意,obj为空
}
return target[propKey];
}
});
p.bar.name = 'bar';
(2)只读视图
const NOPE = () => {
throw new Error('Cannot modify the readonly data');
};
function readonly(data) {
return new Proxy(data, {
set: NOPE,
deleteProperty: NOPE,
setPrototypeOf: NOPE,
preventExtensions: NOPE,
defineProperty: NOPE
})
}
const data = {a: 1};
const readonlyData = readonly(data);
readonlyData.a = 2; // Error: Cannot modify the readonly data
delete readonlyData.a; // Error: Cannot modify the readonly data
(3)入侵式测试框架
通过Proxy在定义方法和逻辑代码之间建立一个隔离层。
import api from './api'
/** * 对目标api进行包装 */
export default hook(api, {
methodName(fn) {
return function(...args) {
console.time('methodName');
return fn(...args).then((...args) => {
console.timeEnd('methodName');
return Promise.resolve(...args);
});
}
}
})
/** * 对目标对象的get行为进行监听和干涉 */
function hook(obj, cases) {
return new Proxy(obj, {
get(target, prop) {
if((prop in cases) && (typeof target[prop] === 'function')) {
const fn = target[prop];
return cases[prop](fn);
}
return target[prop];
}
})
}
Reflect对象的设计目的有这样几个:
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);
}
});