【前端学习】ES6(五)Symbol、Set 和 Map、Reflect

3 篇文章 0 订阅
1 篇文章 0 订阅
本文深入介绍了JavaScript中的Symbol类型,包括其唯一性、不可变性以及作为对象属性名的特性。此外,还详细讲解了Set、Map和WeakMap数据结构,它们分别用于存储唯一值、键值对以及弱引用键值对,展示了如何使用这些数据结构进行操作,如添加、删除、遍历成员。最后提到了Reflect对象,它是语言内部方法的集合,与Proxy对象对应,提供了一种更可控的操作对象的方式。
摘要由CSDN通过智能技术生成

Symbol

一、简介

ES5 的对象属性名都是字符串,这容易造成属性名的冲突。想为一个对象添加新的方法(mixin 模式),新方法的名字就有可能与现有方法产生冲突。如果有一种机制,保证每个属性的名字都是独一无二的就好了,这样就从根本上防止属性名的冲突。这就是 ES6 引入Symbol的原因。
ES6 引入了一种新的原始数据类型Symbol,表示独一无二的值。它是 JavaScript 语言的第七种数据类型,前六种是:undefined、null、布尔值(Boolean)、字符串(String)、数值(Number)、对象(Object)

let s = Symbol();
typeof s // "symbol"
let a = Symbol('a')
let b = Symbol('a')
console.log(a==b,a===b) // false false

上面代码中,变量s就是一个独一无二的值。typeof运算符的结果,表明变量s是 Symbol 数据类型,而不是字符串之类的其他类型。a和b虽然传的都是相同参数,但是两个不相等。
Symbol函数前不能使用new命令,因为生成的 Symbol 是一个原始类型的值,不是对象。
基本上,它是一种类似于字符串的数据类型。
Symbol函数可以接受一个字符串作为参数,表示对 Symbol 实例的描述,主要是为了在控制台显示,或者转为字符串时,比较容易区分,可见不需要给Symbol传参数。

let s1 = Symbol('foo');
let s2 = Symbol('bar');
s1 // Symbol(foo)
s2 // Symbol(bar)
s1.toString() // "Symbol(foo)"
s2.toString() // "Symbol(bar)"

如果 Symbol 的参数是一个对象,就会调用该对象的toString方法,将其转为字符串,然后才生成一个 Symbol 值。

const obj = {
  toString() {
    return 'abc';
  }
};
const sym = Symbol(obj);
sym // Symbol(abc)

Symbol函数的参数只是表示对当前 Symbol 值的描述,因此相同参数的Symbol函数的返回值是不相等的。(这里强调的和上面一样)

// 没有参数的情况
let s1 = Symbol();
let s2 = Symbol();
s1 === s2 // false
// 有参数的情况
let s1 = Symbol('foo');
let s2 = Symbol('foo');
s1 === s2 // false

Symbol 值不能与其他类型的值进行运算,会报错。
Symbol 值可以显式转为字符串,Symbol 值也可以转为布尔值,但是不能转为数值。

二、Symbol.prototype.description

创建 Symbol 的时候,可以添加一个描述。读取这个描述需要将 Symbol 显式转为字符串,即下面的写法。ES2019 提供了一个实例属性description,直接返回 Symbol 的描述。

const sym = Symbol('foo');
console.log(String(sym)) // "Symbol(foo)"
console.log(sym.toString()) // "Symbol(foo)"
console.log(sym.description) // foo

三、作为属性名的 Symbol

由于每一个 Symbol 值都是不相等的,这意味着 Symbol 值可以作为标识符,用于对象的属性名,就能保证不会出现同名的属性。这对于一个对象由多个模块构成的情况非常有用,能防止某一个键被不小心改写或覆盖

let mySymbol = Symbol();
// 第一种写法
let a = {};
a[mySymbol] = 'Hello!';
// 第二种写法
let a = {
  [mySymbol]: 'Hello!'
};
// 第三种写法
let a = {};
Object.defineProperty(a, mySymbol, { value: 'Hello!' });
// 以上写法都得到同样结果
a[mySymbol] // "Hello!"

上面代码通过方括号结构和Object.defineProperty,将对象的属性名指定为一个 Symbol 值。
注意,Symbol 值作为对象属性名时,不能用点运算符。

const mySymbol = Symbol();
const a = {};
a.mySymbol = 'Hello!';
a[mySymbol] // undefined
a['mySymbol'] // "Hello!"

上面代码中,因为点运算符后面总是字符串,所以不会读取mySymbol作为标识名所指代的那个值,导致a的属性名实际上是一个字符串,而不是一个 Symbol 值。
同理,在对象的内部,使用 Symbol 值定义属性时,Symbol 值必须放在方括号之中。

let s = Symbol();
let obj = {
  [s]: function (arg) { ... }
};
obj[s](123);
// 增强的对象写法
let obj = {
  [s](arg) { ... }
};

常量使用 Symbol 值最大的好处,就是其他任何值都不可能有相同的值。
还有一点需要注意,Symbol 值作为属性名时,该属性还是公开属性,不是私有属性。

四、属性名的遍历

Symbol 作为属性名,遍历对象的时候,该属性不会出现在for…in、for…of循环中,也不会被Object.keys()、Object.getOwnPropertyNames()、JSON.stringify()返回。但是,它也不是私有属性。有一个Object.getOwnPropertySymbols()方法,可以获取指定对象的所有 Symbol 属性名。该方法返回一个数组,成员是当前对象的所有用作属性名的 Symbol 值。
由于以 Symbol 值作为键名,不会被常规方法遍历得到。我们可以利用这个特性,为对象定义一些非私有的、但又希望只用于内部的方法。

let size = Symbol('size');
class Collection {
  constructor() {
    this[size] = 0;
  }
  add(item) {
    this[this[size]] = item;
    this[size]++;
  }
  static sizeOf(instance) {
    return instance[size];
  }
}
let x = new Collection();
Collection.sizeOf(x) // 0
x.add('foo');
Collection.sizeOf(x) // 1
Object.keys(x) // ['0']
Object.getOwnPropertyNames(x) // ['0']
Object.getOwnPropertySymbols(x) // [Symbol(size)]

上面代码中,对象x的size属性是一个 Symbol 值,所以Object.keys(x)、Object.getOwnPropertyNames(x)都无法获取它。这就造成了一种非私有的内部方法的效果。

五、Symbol.for(),Symbol.keyFor()

我们希望重新使用同一个 Symbol 值,Symbol.for()方法可以做到这一点。它接受一个字符串作为参数,然后搜索有没有以该参数作为名称的 Symbol 值。如果有,就返回这个 Symbol 值,否则就新建一个以该字符串为名称的 Symbol 值,并将其注册到全局。

let s1 = Symbol.for('foo');
let s2 = Symbol.for('foo');
s1 === s2 // true

上面代码中,s1和s2都是 Symbol 值,但是它们都是由同样参数的Symbol.for方法生成的,所以实际上是同一个值。
Symbol.for()与Symbol()这两种写法,都会生成新的 Symbol。它们的区别是,前者会被登记在全局环境中供搜索,后者不会。Symbol.for()不会每次调用就返回一个新的 Symbol 类型的值,而是会先检查给定的key是否已经存在,如果不存在才会新建一个值。

var symb1 = Symbol('123')
var symb2 = Symbol.for('123')
var symb3 = Symbol.for('123')
var symb4 = Symbol.keyFor(symb2)
console.log(symb1 === symb2) // false 因为没登记
console.log(symb2 === symb3) // true
console.log(symb4) // 123

六、Symbol内置的值

Set和Map数据结构

一、Set

ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。

const s =new Set();
[2,3,5,4,5,2,2].forEach(x => s.add(x));
for(let i of s){
  console.log(i);
}
// 2 3 5 4

上面代码通过**add()**方法向 Set 结构加入成员,结果表明 Set 结构不会添加重复的值。
Set函数可以接受一个数组(或者具有 iterable 接口的其他数据结构)作为参数,用来初始化。

// 例一
var set=new Set([1,2,3,4,4]);
[...set]
// [1, 2, 3, 4]
// 例二
const items =new Set([1,2,3,4,5,5,5,5]);
items.size // 5
// 例三
var set=new Set(document.querySelectorAll('div'));
set.size // 56
// 例四
function* generator(){
    yield 1 
    yield 2 
    yield 3
    yield 4
}
var set = new Set(generator())
set.size // 4

在 Set 内部,两个NaN是相等的,两个对象总是不相等的。

var set=new Set();
let a =NaN;
let b =NaN;
set.add(a);
set.add(b);
set// Set {NaN}
var set=new Set();
set.add({});
set.size // 1
set.add({});
set.size // 2

Set实例的属性:
Set.prototype.constructor:构造函数,默认就是Set函数。
Set.prototype.size:返回Set实例的成员总数。
Set实例的方法:
操作方法:
Set.prototype.add(value):添加某个值,返回 Set 结构本身。
Set.prototype.delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
Set.prototype.has(value):返回一个布尔值,表示该值是否为Set的成员。
Set.prototype.clear():清除所有成员,没有返回值。
(Array.from方法可以将 Set 结构转为数组。)
数组去重方法

function dedupe(array){
returnArray.from(newSet(array));
}
dedupe([1,1,2,3])// [1, 2, 3]

遍历方法:
Set.prototype.keys():返回键名的遍历器
Set.prototype.values():返回键值的遍历器
Set.prototype.entries():返回键值对的遍历器
Set.prototype.forEach():使用回调函数遍历每个成员

let set = new Set()
set.add({})
set.add({})
console.log(set.entries().next()) // "value":[{},{}],"done":false}
console.log(set.entries().next()) // "value":[{},{}],"done":false}

遍历方法和数组几乎一样,只是键名就是值的toString
数组的map和filter方法也可以间接用于 Set 了。

let set=newSet([1,2,3]);
set=newSet([...set].map(x => x *2));
// 返回Set结构:{2, 4, 6}
let set=newSet([1,2,3,4,5]);
set=newSet([...set].filter(x =>(x %2)==0));
// 返回Set结构:{2, 4}

集合运算

let a =newSet([1,2,3]);
let b =newSet([4,3,2]);
// 并集
let union=newSet([...a,...b]);
// Set {1, 2, 3, 4}
// 交集
let intersect =newSet([...a].filter(x => b.has(x)));
// set {2, 3}
// (a 相对于 b 的)差集
let difference =newSet([...a].filter(x =>!b.has(x)));
// Set {1}

如果想在遍历操作中,同步改变原来的 Set 结构,目前没有直接的方法,但有两种变通方法。一种是利用原 Set 结构映射出一个新的结构,然后赋值给原来的 Set 结构;另一种是利用Array.from方法。

// 方法一
var set=new Set([1,2,3]);
set=new Set([...set].map(val => val *2));
// set的值是2, 4, 6
// 方法二
var set=new Set([1,2,3]);
set=new Set(Array.from(set, val => val *2));
// set的值是2, 4, 6

二、WeakSet

WeakSet 结构与 Set 类似,也是不重复的值的集合。但是,它与 Set 有两个区别。
首先,WeakSet 的成员只能是对象,而不能是其他类型的值。
其次,WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet 之中。
WeakSet没有clear方法、size属性和所有遍历方法

三、Map

简介
JavaScript 的对象(Object),本质上是键值对的集合(Hash 结构),但是传统上只能用字符串当作键。这给它的使用带来了很大的限制。这里直接调用了element.toString()

const data = {};
const element = document.getElementById('myDiv');
data[element] = 'metadata';
data['[object HTMLDivElement]'] // "metadata"

为了解决这个问题,ES6 提供了 Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应,是一种更完善的 Hash 结构实现。如果你需要“键值对”的数据结构,Map 比 Object 更合适。

const map = new Map([
  ['name', '张三'],
  ['title', 'Author']
]);
// 实际上等于
const items = [
  ['name', '张三'],
  ['title', 'Author']
];
const map = new Map();
items.forEach(
  ([key, value]) => map.set(key, value)
);

任何具有 Iterator 接口、且每个成员都是一个双元素的数组的数据结构
只有对同一个对象的引用,Map 结构才将其视为同一个键

const map = new Map();
map.set(['a'], 555);
map.get(['a']) // undefined

由上可知,Map 的键实际上是跟内存地址绑定的,只要内存地址不一样,就视为两个键。
如果 Map 的键是一个简单类型的值(数字、字符串、布尔值),则只要两个值严格相等,Map 将其视为一个键,比如0和-0就是一个键,布尔值true和字符串true则是两个不同的键。

实例属性和操作方法
1、size 属性:size属性返回 Map 结构的成员总数。
2、Map.prototype.set(key, value):set方法设置键名key对应的键值为value,然后返回整个 Map 结构。如果key已经有值,则键值会被更新,否则就新生成该键。
set方法返回的是当前的Map对象,因此可以采用链式写法。

let map = new Map()
  .set(1, 'a')
  .set(2, 'b')
  .set(3, 'c');

3、Map.prototype.get(key):get方法读取key对应的键值,如果找不到key,返回undefined
4、Map.prototype.has(key):has方法返回一个布尔值,表示某个键是否在当前 Map 对象之中。
5、Map.prototype.delete(key):delete方法删除某个键,返回true。如果删除失败,返回false。
6、Map.prototype.clear():clear方法清除所有成员,没有返回值。

遍历方法:
Map.prototype.keys():返回键名的遍历器。
Map.prototype.values():返回键值的遍历器。
Map.prototype.entries():返回所有成员的遍历器。
Map.prototype.forEach():遍历 Map 的所有成员。

Map也可以被扩展运算符展开

const map = new Map([[1,2],[2,1]])
console.log(...map) // [1,2],[2,1]

结合数组的map方法、filter方法,可以实现 Map 的遍历和过滤(Map 本身没有map和filter方法)。

const map0 = new Map()
  .set(1, 'a')
  .set(2, 'b')
  .set(3, 'c');
const map1 = new Map(
  [...map0].filter(([k, v]) => k < 3)
);
// 产生 Map 结构 {1 => 'a', 2 => 'b'}
const map2 = new Map(
  [...map0].map(([k, v]) => [k * 2, '_' + v])
    );
// 产生 Map 结构 {2 => '_a', 4 => '_b', 6 => '_c'}

forEach方法,与数组相似

map.forEach(function(value, key, map) {
  console.log("Key: %s, Value: %s", key, value);
});

和其他数据结构转换:
Map转数组:扩展运算符
数组转Map:只要符合格式,直接调用Map构造函数
Map转对象:仅当map的键都是字符串时才可以无损地转成对象,否则Map的键名首先被转换成字符串再作为对象键名。

function strMapToObj(strMap) {
  let obj = Object.create(null);
  for (let [k,v] of strMap) {
    obj[k] = v;
  }
  return obj;
}
const myMap = new Map()
  .set('yes', true)
  .set('no', false);
strMapToObj(myMap)
// { yes: true, no: false }

对象转Map:

let obj = {"a":1, "b":2};
let map = new Map(Object.entries(obj));

Map转JSON:
当键名都是字符串时考虑转为对象JSON

function strMapToJson(strMap) {
  return JSON.stringify(strMapToObj(strMap));
}
let myMap = new Map().set('yes', true).set('no', false);
strMapToJson(myMap)
// '{"yes":true,"no":false}'

否则考虑转换为数组JSON

function mapToArrayJson(map) {
  return JSON.stringify([...map]);
}
let myMap = new Map().set(true, 7).set({foo: 3}, ['abc']);
mapToArrayJson(myMap)
// '[[true,7],[{"foo":3},["abc"]]]'

JSON转Map:
对象JSON:

function jsonToStrMap(jsonStr) {
  return objToStrMap(JSON.parse(jsonStr));
}
jsonToStrMap('{"yes": true, "no": false}')
// Map {'yes' => true, 'no' => false}

数组JSON:

function jsonToMap(jsonStr) {
  return new Map(JSON.parse(jsonStr));
}
jsonToMap('[[true,7],[{"foo":3},["abc"]]]')
// Map {true => 7, Object {foo: 3} => ['abc']}

四、WeakMap

WeakMap与Map的区别有两点。
首先,WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键名。
其次,WeakMap的键名所指向的对象,不计入垃圾回收机制。
WeakMap的设计目的在于,有时我们想在某个对象上面存放一些数据,但是这会形成对于这个对象的引用。
和WeakSet同理,WeakMap也是弱引用,注意,WeakMap 弱引用的只是键名,而不是键值。键值依然是正常引用。WeakMap也没有遍历操作、clear操作、size属性。只有get()、set()、has()、delete()。
一个典型应用场景是,在网页的 DOM 元素上添加数据,就可以使用WeakMap结构。当该 DOM 元素被清除,其所对应的WeakMap记录就会自动被移除。

Reflect

一、概述

Reflect对象与Proxy对象一样,也是 ES6 为了操作对象而提供的新 API。Reflect对象的设计目的有这样几个。
(1) 将Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上。现阶段,某些方法同时在Object和Reflect对象上部署,未来的新方法将只部署在Reflect对象上。也就是说,从Reflect对象上可以拿到语言内部的方法。
(2) 修改某些Object方法的返回结果,让其变得更合理。比如,Object.defineProperty(obj, name, desc)在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj, name, desc)则会返回false。
(3) 让Object操作都变成函数行为。某些Object操作是命令式,比如name in obj和delete obj[name],而Reflect.has(obj, name)和Reflect.deleteProperty(obj, name)让它们变成了函数行为。

// 老写法
'assign' in Object // true
// 新写法
Reflect.has(Object, 'assign') // true

(4)Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。这就让Proxy对象可以方便地调用对应的Reflect方法,完成默认行为,作为修改行为的基础。也就是说,不管Proxy怎么修改默认行为,你总可以在Reflect上获取默认行为。

Proxy(target, {
  set: function(target, name, value, receiver) {
    var success = Reflect.set(target, name, value, receiver);
    if (success) {
      console.log('property ' + name + ' on ' + target + ' set to ' + value);
    }
    return success;
  }
});

上面代码中,Proxy方法拦截target对象的属性赋值行为。它采用Reflect.set方法将值赋值给对象的属性,确保完成原有的行为,然后再部署额外的功能。

二、静态方法

Reflect对象一共有 13 个静态方法(所有方法名和Proxy一模一样)。
Reflect.apply(target, thisArg, args)
Reflect.construct(target, args)
Reflect.get(target, name, receiver)
Reflect.set(target, name, value, receiver)
Reflect.defineProperty(target, name, desc)
Reflect.deleteProperty(target, name)
Reflect.has(target, name)
Reflect.ownKeys(target)
Reflect.isExtensible(target)
Reflect.preventExtensions(target)
Reflect.getOwnPropertyDescriptor(target, name)
Reflect.getPrototypeOf(target)
Reflect.setPrototypeOf(target, prototype)
上面这些方法的作用,大部分与Object对象的同名方法的作用都是相同的,而且它与Proxy对象的方法是一一对应的。下面是对它们的解释。
Reflect.get(target, name, receiver)
Reflect.get方法查找并返回target对象的name属性,如果没有该属性,则返回undefined。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值