ES6--Set、Map、Symbol、Proxy及Reflect

九、Set和Map数据结构

Set

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

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

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的遍历器对象。

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

WeakMap结果与Map结构基本一致,区别在于WeakMap只接受对象作为键名(null除外),而且其键名所指向的对象不计入垃圾回收机制(弱类型)。

十、Iterator和for…of循环

ES6之前表示“集合”的数据结构,主要是数组对象,ES6中新增了MapSet。需要一种统一的接口机制来处理所有不同的数据结构。遍历器(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接口的场景

一些场景会默认调用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)其他场合

由于数组的遍历都会调用遍历器接口,所以任何接受数组作为参数的场合其实都调用了遍历器接口。

  • for…of
  • Array.from(arguments)
  • Map([[‘a’, 1], [‘b’, 2]])、Set([1, 2, 3])、WeakMap()、WeakSet()
  • Promise.all()、Promise.race()

十一、Symbol

​ 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"

常用Symbol值

(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

Proxy可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写,属于一种“元编程”。(注意,ES5无法模拟该特性)。

元编程重点在于:在一个程序的内容、运行环境、配置等都不做任何修改的情况下,可以通过其他程序对其进行读取或修改。

Getter/Setter

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

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)

  • 当目标对象被Object.preventExtensions()禁用,该监听方法不能返回false;
  • 当属性的configurable配置是false时,该监听方法不能返回false。
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对象

Proxy.revocable方法返回一个可取消的 Proxy 实例。

let {proxy, revoke} = Proxy.revocable(target, handler);
  • proxy为可解除Proxy对象
  • revoke为解除方法

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

Proxy使用场景

(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

Reflect对象的设计目的有这样几个:

  • 将Object对象的一些明显属于语言层面的方法,放到Reflect对象上。现阶段,某些方法同时在Object和Reflect对象上部署,未来的新方法将只部署在Reflect对象上。
  • 更加有用的返回值,修改某些Object方法的返回结果,让其变得更合理。比如,Object.defineProperty(obj, name, desc)在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj, name, desc)则会返回false。
  • 函数操作,让Object操作都变成函数行为。某些Object操作是命令式,比如name in obj和delete obj[name],而Reflect.has(obj, name)和Reflect.deleteProperty(obj, name)让它们变成了函数行为。
  • Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。这就让Proxy对象可以方便地调用对应的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);
  }
});

你可能感兴趣的:(JavaScript,set,proxy,symbol,ES6)