在平常的开发当中,我们会经常用到 ES6
相关语法,大致总结一下,ES6
更新的内容可以分为以下几点
- 表达式,声明、解构赋值
- 内置对象,字符串扩展、数值扩展、对象扩展、数组扩展、函数扩展、正则扩展、
Symbol
、Set
、Map
、Proxy
、Reflect
- 语句与运算,
Class
、Module
、Iterator
- 异步编程,
Promise
、Generator
、Async
有一些是我们经常会遇到和用到的,比如 let
,const
,扩展运算,Promise
等,之前也单独整理过一些,比如 Class,Iterator,Async,Await 和 Reflect 等
今天我们就来看看剩下的几个可能是平常用的不太多的 Symbol
,Set
和 Map
,主要参考的是 ECMAScript 6 入门
Symbol
ES6
引入了一种新的原始数据类型 Symbol
,每个从 Symbol()
返回的 symbol
值都是唯一的,一个 symbol
值能作为对象属性的标识符,这是该数据类型仅有的目的,下面我们就来看看 Symbol
类型具有哪些特性
独一无二
直接使用 Symbol()
创建新的 symbo
l变量,可选用一个字符串用于描述,当参数为对象时,将调用对象的 toString()
方法
1 | var sym1 = Symbol() // Symbol() |
我们用两个相同的字符串创建两个 Symbol
变量,它们是不相等的,可见每个 Symbol
变量都是独一无二的,如果我们想创造两个相等的 Symbol
变量,可以使用 Symbol.for(key)
Symbol.for(key)
使用给定的 key
搜索现有的 symbol
,如果找到则返回该 symbol
,否则将使用给定的 key
在全局 symbol
注册表中创建一个新的 symbol
1 | var sym1 = Symbol.for('foo') |
另外还有一个 Symbol.keyFor()
的方法,它则是用于返回已登记的 Symbol
类型值的 key
,但是需要注意,只能返回 Symbol.for()
的 key
1 | let s1 = Symbol.for('foo') |
原始类型
需要注意的是,Symbol
函数前不能使用 new
命令,否则会报错,这是因为生成的 Symbol
是一个原始类型的值,不是对象
1 | new Symbol() // Uncaught TypeError: Symbol is not a constructor |
我们可以使用 typeof
运算符判断一个 Symbol
类型
1 | typeof Symbol() === 'symbol' // true |
不可枚举
Symbol
作为属性名,遍历对象的时候,该属性不会出现在 for-in
、for-of
循环中,也不会被 Object.keys()
、Object.getOwnPropertyNames()
、JSON.stringify()
返回,但是可以使用 Object.getOwnPropertySymbols()
方法来获取指定对象的所有 Symbol
属性名,该方法返回一个数组
1 | const obj = {} |
另外也可以使用 Reflect.ownKeys()
方法来获取
1 | Reflect.ownKeys(obj) // [Symbol(a), Symbol(b)] |
所以可以利用这个特性,为对象定义一些非私有的、但又希望只用于内部的方法
1 | let size = Symbol('size') |
应用场景
我们下面简单的来看几个 Symbol
在程序中的应用场景
- 应用一,防止
XSS
在 React
的 ReactElement
对象中,有一个 typeof
属性,它是一个 Symbol
类型的变量
1 | var REACT_ELEMENT_TYPE = |
ReactElement.isValidElement
函数用来判断一个 React
组件是否是有效的,下面是它的具体实现
1 | /** |
可见 React
渲染时会把没有 $$typeof
标识,以及规则校验不通过的组件过滤掉,比如我们的服务器有一个漏洞,允许用户存储任意 JSON
对象,而客户端代码需要一个字符串,这可能会成为一个问题
1 | // JSON |
而 JSON
中不能存储 Symbol
类型的变量,这就是防止 XSS
的一种手段
- 应用二,私有属性
借助 Symbol
类型的不可枚举,我们可以在类中模拟私有属性,控制变量读写
1 | const privateField = Symbol() |
- 应用三,防止属性污染
在某些情况下,我们可能要为对象添加一个属性,此时就有可能造成属性覆盖,用 Symbol
作为对象属性可以保证永远不会出现同名属性,例如下面的场景,我们模拟实现一个 call
方法
1 | Function.prototype.call = function (context, ...args) { |
Set
ES6
提供了新的数据结构 Set
,它类似于数组,但是成员的值都是唯一的,没有重复的值,在平常开发当中,使用较多的就是用来进行去重,如下数组去重
1 | [...new Set(array)] |
也可以用于字符串去重
1 | [...new Set('ababbc')].join('') // abc |
但是除了去重之外,Set
还有许多其他的方法,因为 Set
本身是一个构造函数,可以用来生成 Set
数据结构
1 | const s = new Set() |
Set 实例的属性和方法
我们先来看看 Set
当中的操作方法,主要有以下这些
add()
,添加值,返回实例delete()
,删除值,返回布尔值has()
,检查值,返回布尔值clear()
,清除所有成员
1 | let s = new Set() |
除了操作方法之外,还有遍历方法
keys()
,返回以属性值为遍历器的对象values()
,返回以属性值为遍历器的对象entries()
,返回以属性值和属性值为遍历器的对象forEach()
,使用回调函数遍历每个成员
keys
方法、values
方法、entries
方法返回的都是遍历器对象,由于 Set
结构没有键名,只有键值(或者说键名和键值是同一个值),所以 keys
方法和 values
方法的行为完全一致
1 | let set = new Set(['red', 'green', 'blue']) |
Set
结构的实例默认可遍历,它的默认遍历器生成函数就是它的 values
方法,所以可以省略 values
方法,直接用 for-of
循环遍历 Set
1 | let set = new Set(['red', 'green', 'blue']) |
Set
结构的实例与数组一样,也拥有 forEach
方法,用于对每个成员执行某种操作,『没有返回值』
1 | let set = new Set([1, 4, 9]) |
而且,数组的 map
和 filter
方法也可以间接用于 Set
1 | let set = new Set([1, 2, 3]) |
因此使用 Set
可以很容易地实现并集、交集和差集
1 | let a = new Set([1, 2, 3]) |
WeakSet
WeakSet
结构与 Set
类似,也是不重复的值的集合,但是 WeakSet
的成员『只能是对象,而不能是其他类型的值』,WeakSet
中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet
对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet
之中,并且 WeakSet
结构是『不可遍历』的
1 | const a = [[1, 2], [3, 4]] |
WeakSet
结构有以下三个方法
add()
,添加值,返回实例delete()
,删除值,返回布尔值has()
,检查值,返回布尔值
1 | const ws = new WeakSet() |
WeakSet
的一个用处,是储存DOM
节点,而不用担心这些节点从文档移除时,会引发内存泄漏
Map
Map
数据结构类似于对象的数据结构,成员键可以是任何类型的值,也就是说,Object
结构提供了字符串与值的对应,而 Map
结构提供了值与值的对应,是一种更完善的 Hash
结构实现,如果你需要键值对的数据结构,Map
比 Object
更合适
1 | const m = new Map() |
Map
也可以接受一个数组作为参数,该数组的成员是一个个表示键值对的数组
1 | const map = new Map([ |
可以发现,如果对同一个键多次赋值,后面的值将覆盖前面的值,有一个需要注意的地方,只有对同一个对象的引用,Map
结构才将其视为同一个键
1 | const m1 = new Map() |
Map
的键实际上是跟内存地址绑定的,只要内存地址不一样,就视为两个键
与其他数据结构的互相转换
关于 Map
数据结构的方法,和 Set
方法使用是类似的,这里就不详细展开了,我们来看看与其他数据结构的互相转换
Map
转为数组,Map
转为数组最方便的方法,就是使用扩展运算符(...
)
1 | const m = new Map() |
- 数组转为
Map
,将数组传入Map
构造函数,就可以转为Map
1 | new Map([ |
Map
转为对象,如果所有Map
的键都是字符串,它可以无损地转为对象,如果有非字符串的键名,那么这个键名会被转成字符串,再作为对象的键名
1 | function strMapToObj(strMap) { |
- 对象转为
Map
1 | function objToStrMap(obj) { |
Map
转为JSON
,Map
转为JSON
要区分两种情况,一种情况是Map
的键名都是字符串,在这种情况下可以先将其转换为对象(使用之前的strMapToObj()
方法),然后在转换为对象JSON
1 | function strMapToJson(strMap) { |
另一种情况是 Map
的键名有非字符串,这时可以选择转为数组 JSON
,这时可以选择转为数组 JSON
1 | function mapToArrayJson(map) { |
JSON
转为Map
,JSON
转为Map
,正常情况下,所有键名都是字符串,也是使用之前的objToStrMap()
方法
1 | function jsonToStrMap(jsonStr) { |
但是有一种特殊情况,整个 JSON
就是一个数组,且每个数组成员本身,又是一个有两个成员的数组
1 | function jsonToMap(jsonStr) { |
WeakMap
WeakMap
结构与 Map
结构类似,也是用于生成键值对的集合
1 | // WeakMap 可以使用 set 方法添加成员 |
WeakMap
与 Map
的区别有两点
WeakMap
只接受对象作为键名(null
除外),不接受其他类型的值作为键名WeakMap
的键名所指向的对象,不计入垃圾回收机制
同 WeakSet
一致,WeakMap
的键名所引用的对象都是弱引用,即垃圾回收机制不将该引用考虑在内,WeakMap
与 Map
在 API
上的区别主要是两个
- 一是没有遍历操作(即没有
keys()
、values()
和entries()
方法),也没有size
属性,因为没有办法列出所有键名 - 二是无法清空,即不支持
clear()
方法
因此,WeakMap
只有四个方法可用 get()
、set()
、has()
、delete()
get()
,返回键值对set()
,添加键值对,返回实例delete()
,删除键值对,返回布尔值has()
,检查键值对,返回布尔值
下面我们来看一些 WeakMap
的使用场景
在 DOM 对象上保存相关数据
传统使用 jQuery
的时候,我们会通过 $.data()
方法在 DOM
对象上储存相关信息,当你将 DOM
元素删除,DOM
对象置为空的时候,相关联的数据并不会被删除,你必须手动执行 $.removeData()
方法才能删除掉相关联的数据,WeakMap
就可以简化这一操作
1 | let wm = new WeakMap(), el = document.querySelector('.el') |
数据缓存
从之前的例子我们可以看出,当我们需要关联对象和数据,比如在不修改原有对象的情况下储存某些属性或者根据对象储存一些计算的值等,而又不想管理这些数据的时候就可以考虑使用 WeakMap
,数据缓存就是一个非常好的例子
1 | const cache = new WeakMap() |
部署私有属性
WeakMap
也可以被用于实现私有变量,不过在 ES6
中实现私有变量的方式有很多种,这只是其中一种
1 | const privateData = new WeakMap() |
结论 && 区别
Set
、Map
、WeakSet
、WeakMap
、都是一种集合的数据结构Set
和WeakSet
是一种值-值的集合,且元素唯一不重复Map
和WeakMap
是一种键-值对的集合,Map
的键可以是任意类型,WeakMap
的键只能是对象类型Set
添加值使用add()
,Map
添加值和返回键值对使用set()
/get()
Set
和Map
可遍历,WeakSet
和WeakMap
不可遍历WeakSet
和WeakMap
键名所指向的对象,不计入垃圾回收机制