ES6 中的 Symbol,Set 和 Map

ES6 中的 Symbol,Set 和 Map

在平常的开发当中,我们会经常用到 ES6 相关语法,大致总结一下,ES6 更新的内容可以分为以下几点

  • 表达式,声明、解构赋值
  • 内置对象,字符串扩展、数值扩展、对象扩展、数组扩展、函数扩展、正则扩展、SymbolSetMapProxyReflect
  • 语句与运算,ClassModuleIterator
  • 异步编程,PromiseGeneratorAsync

有一些是我们经常会遇到和用到的,比如 letconst,扩展运算,Promise 等,之前也单独整理过一些,比如 ClassIteratorAsync,AwaitReflect

今天我们就来看看剩下的几个可能是平常用的不太多的 SymbolSetMap,主要参考的是 ECMAScript 6 入门

Symbol

ES6 引入了一种新的原始数据类型 Symbol,每个从 Symbol() 返回的 symbol 值都是唯一的,一个 symbol 值能作为对象属性的标识符,这是该数据类型仅有的目的,下面我们就来看看 Symbol 类型具有哪些特性

独一无二

直接使用 Symbol() 创建新的 symbo l变量,可选用一个字符串用于描述,当参数为对象时,将调用对象的 toString() 方法

1
2
3
4
5
var sym1 = Symbol()                 // Symbol() 
var sym2 = Symbol('foo') // Symbol(foo)
var sym3 = Symbol('foo') // Symbol(foo)
var sym4 = Symbol({ name: 'foo' }) // Symbol([object Object])
console.log(sym2 === sym3) // false

我们用两个相同的字符串创建两个 Symbol 变量,它们是不相等的,可见每个 Symbol 变量都是独一无二的,如果我们想创造两个相等的 Symbol 变量,可以使用 Symbol.for(key)

Symbol.for(key) 使用给定的 key 搜索现有的 symbol,如果找到则返回该 symbol,否则将使用给定的 key 在全局 symbol 注册表中创建一个新的 symbol

1
2
3
4
5
var sym1 = Symbol.for('foo')
var sym2 = Symbol.for('foo')

// true
console.log(sym1 === sym2)

另外还有一个 Symbol.keyFor() 的方法,它则是用于返回已登记的 Symbol 类型值的 key,但是需要注意,只能返回 Symbol.for()key

1
2
3
4
5
let s1 = Symbol.for('foo')
Symbol.keyFor(s1) // 'foo'

let s2 = Symbol('foo')
Symbol.keyFor(s2) // undefined

原始类型

需要注意的是,Symbol 函数前不能使用 new 命令,否则会报错,这是因为生成的 Symbol 是一个原始类型的值,不是对象

1
new Symbol()  // Uncaught TypeError: Symbol is not a constructor

我们可以使用 typeof 运算符判断一个 Symbol 类型

1
2
3
typeof Symbol() === 'symbol'      // true

typeof Symbol('foo') === 'symbol' // true

不可枚举

Symbol 作为属性名,遍历对象的时候,该属性不会出现在 for-infor-of 循环中,也不会被 Object.keys()Object.getOwnPropertyNames()JSON.stringify() 返回,但是可以使用 Object.getOwnPropertySymbols() 方法来获取指定对象的所有 Symbol 属性名,该方法返回一个数组

1
2
3
4
5
6
7
8
9
10
const obj = {}
let a = Symbol('a')
let b = Symbol('b')

obj[a] = 'hello'
obj[b] = 'world'

const objectSymbols = Object.getOwnPropertySymbols(obj)

objectSymbols // [Symbol(a), Symbol(b)]

另外也可以使用 Reflect.ownKeys() 方法来获取

1
Reflect.ownKeys(obj) // [Symbol(a), Symbol(b)]

所以可以利用这个特性,为对象定义一些非私有的、但又希望只用于内部的方法

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
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)]

应用场景

我们下面简单的来看几个 Symbol 在程序中的应用场景

  • 应用一,防止 XSS

ReactReactElement 对象中,有一个 typeof 属性,它是一个 Symbol 类型的变量

1
2
3
var REACT_ELEMENT_TYPE =
(typeof Symbol === 'function' && Symbol.for && Symbol.for('react.element')) ||
0xeac7

ReactElement.isValidElement 函数用来判断一个 React 组件是否是有效的,下面是它的具体实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* Verifies the object is a ReactElement.
* See https://reactjs.org/docs/react-api.html#isvalidelement
* @param {?object} object
* @return {boolean} True if `object` is a ReactElement.
* @final
*/
export function isValidElement(object) {
// 还是很严谨的
return (
typeof object === 'object' &&
object !== null &&
object.$$typeof === REACT_ELEMENT_TYPE
)
}

可见 React 渲染时会把没有 $$typeof 标识,以及规则校验不通过的组件过滤掉,比如我们的服务器有一个漏洞,允许用户存储任意 JSON 对象,而客户端代码需要一个字符串,这可能会成为一个问题

1
2
3
4
5
6
7
8
9
10
11
// JSON
let expectedTextButGotJSON = {
type: 'div',
props: {
dangerouslySetInnerHTML: {
__html: '/* put your exploit here */'
},
},
}

let message = { text: expectedTextButGotJSON }

JSON 中不能存储 Symbol 类型的变量,这就是防止 XSS 的一种手段

  • 应用二,私有属性

借助 Symbol 类型的不可枚举,我们可以在类中模拟私有属性,控制变量读写

1
2
3
4
5
6
7
8
9
10
11
12
13
const privateField = Symbol()

class myClass {
constructor() {
this[privateField] = 'abc'
}
getField() {
return this[privateField]
}
setField(val) {
this[privateField] = val
}
}
  • 应用三,防止属性污染

在某些情况下,我们可能要为对象添加一个属性,此时就有可能造成属性覆盖,用 Symbol 作为对象属性可以保证永远不会出现同名属性,例如下面的场景,我们模拟实现一个 call 方法

1
2
3
4
5
6
7
8
9
10
11
12
Function.prototype.call = function (context, ...args) {
if (context === null || context === undefined) {
context = window
} else {
context = Object(context)
}
let fn = Symbol()
context[fn] = this
let result = context[fn](...args)
delete context[fn]
return result
}

Set

ES6 提供了新的数据结构 Set,它类似于数组,但是成员的值都是唯一的,没有重复的值,在平常开发当中,使用较多的就是用来进行去重,如下数组去重

1
[...new Set(array)]

也可以用于字符串去重

1
[...new Set('ababbc')].join('')  // abc

但是除了去重之外,Set 还有许多其他的方法,因为 Set 本身是一个构造函数,可以用来生成 Set 数据结构

1
2
3
4
5
6
7
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
}

Set 实例的属性和方法

我们先来看看 Set 当中的操作方法,主要有以下这些

  • add(),添加值,返回实例
  • delete(),删除值,返回布尔值
  • has(),检查值,返回布尔值
  • clear(),清除所有成员
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let s = new Set()

s.add(1).add(2).add(2)

s.size // 2

s.has(1) // true
s.has(2) // true
s.has(3) // false

s.delete(2)
s.has(2) // false

s.clear()
s.size // 0

除了操作方法之外,还有遍历方法

  • keys(),返回以属性值为遍历器的对象
  • values(),返回以属性值为遍历器的对象
  • entries(),返回以属性值和属性值为遍历器的对象
  • forEach(),使用回调函数遍历每个成员

keys 方法、values 方法、entries 方法返回的都是遍历器对象,由于 Set 结构没有键名,只有键值(或者说键名和键值是同一个值),所以 keys 方法和 values 方法的行为完全一致

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)
}
// red
// green
// blue

for (let item of set.values()) {
console.log(item)
}
// red
// green
// blue

for (let item of set.entries()) {
console.log(item)
}
// ['red', 'red']
// ['green', 'green']
// ['blue', 'blue']

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)
}
// red
// green
// blue

Set 结构的实例与数组一样,也拥有 forEach 方法,用于对每个成员执行某种操作,『没有返回值』

1
2
3
4
5
6
7
8
let set = new Set([1, 4, 9])
set.forEach((v, k) => {
console.log(k + ' : ' + v)
})

// 1 : 1
// 4 : 4
// 9 : 9

而且,数组的 mapfilter 方法也可以间接用于 Set

1
2
3
4
5
let set = new Set([1, 2, 3])
new Set([...set].map(x => x * 2)) // Set {2, 4, 6}

let set = new Set([1, 2, 3, 4, 5])
new Set([...set].filter(x => (x % 2) == 0)) // Set {2, 4}

因此使用 Set 可以很容易地实现并集、交集和差集

1
2
3
4
5
6
7
8
9
10
11
let a = new Set([1, 2, 3])
let b = new Set([4, 3, 2])

// 并集
let union = new Set([...a, ...b]) // Set {1, 2, 3, 4}

// 交集
let intersect = new Set([...a].filter(x => b.has(x))) // set {2, 3}

// 差集
let difference = new Set([...a].filter(x => !b.has(x))) // Set {1}

WeakSet

WeakSet 结构与 Set 类似,也是不重复的值的集合,但是 WeakSet 的成员『只能是对象,而不能是其他类型的值』,WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet 之中,并且 WeakSet 结构是『不可遍历』的

1
2
3
4
5
const a = [[1, 2], [3, 4]]
const ws = new WeakSet(a) // WeakSet {[1, 2], [3, 4]}

const b = [1, 2]
const ws = new WeakSet(b) // Uncaught TypeError

WeakSet 结构有以下三个方法

  • add(),添加值,返回实例
  • delete(),删除值,返回布尔值
  • has(),检查值,返回布尔值
1
2
3
4
5
6
7
8
9
10
11
12
const ws = new WeakSet()
const obj = {}
const foo = {}

ws.add(window)
ws.add(obj)

ws.has(window) // true
ws.has(foo) // false

ws.delete(window)
ws.has(window) // false

WeakSet 的一个用处,是储存 DOM 节点,而不用担心这些节点从文档移除时,会引发内存泄漏

Map

Map 数据结构类似于对象的数据结构,成员键可以是任何类型的值,也就是说,Object 结构提供了字符串与值的对应,而 Map 结构提供了值与值的对应,是一种更完善的 Hash 结构实现,如果你需要键值对的数据结构,MapObject 更合适

1
2
3
4
5
6
7
8
9
const m = new Map()
const o = { p: 'hello world' }

m.set(o, 'test')
m.get(o) // 'test'

m.has(o) // true
m.delete(o) // true
m.has(o) // false

Map 也可以接受一个数组作为参数,该数组的成员是一个个表示键值对的数组

1
2
3
4
5
6
7
8
9
10
11
const map = new Map([
['name', 'zhangsan'],
['name', 'lisi'],
['age', '18']
])

map.size // 2
map.has('name') // true
map.get('name') // 'lisi'
map.has('age') // true
map.get('age') // '18'

可以发现,如果对同一个键多次赋值,后面的值将覆盖前面的值,有一个需要注意的地方,只有对同一个对象的引用,Map 结构才将其视为同一个键

1
2
3
4
5
6
7
8
9
10
11
const m1 = new Map()

m1.set(['a'], 555)
m1.get(['a']) // undefined


const m2 = new Map()
const a = {}

m2.set(a, 555)
m2.get(a) // 555

Map 的键实际上是跟内存地址绑定的,只要内存地址不一样,就视为两个键

与其他数据结构的互相转换

关于 Map 数据结构的方法,和 Set 方法使用是类似的,这里就不详细展开了,我们来看看与其他数据结构的互相转换

  • Map 转为数组,Map 转为数组最方便的方法,就是使用扩展运算符(...
1
2
3
4
5
const m = new Map()
.set(true, 7)
.set({foo: 3}, ['abc'])

[...m] // [ [ true, 7 ], [ { foo: 3 }, [ 'abc' ] ] ]
  • 数组转为 Map,将数组传入 Map 构造函数,就可以转为 Map
1
2
3
4
5
6
7
8
new Map([
[true, 7],
[{ foo: 3 }, ['abc']]
])
// Map {
// true => 7,
// Object { foo: 3 } => ['abc']
// }
  • Map 转为对象,如果所有 Map 的键都是字符串,它可以无损地转为对象,如果有非字符串的键名,那么这个键名会被转成字符串,再作为对象的键名
1
2
3
4
5
6
7
8
9
10
11
12
13
function strMapToObj(strMap) {
let obj = Object.create(null)
for (let [k, v] of strMap) {
obj[k] = v
}
return obj
}

const m = new Map()
.set('yes', true)
.set('no', false)

strMapToObj(m) // { yes: true, no: false }
  • 对象转为 Map
1
2
3
4
5
6
7
8
9
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 }) // Map {'yes' => true, 'no' => false}
  • Map 转为 JSONMap 转为 JSON 要区分两种情况,一种情况是 Map 的键名都是字符串,在这种情况下可以先将其转换为对象(使用之前的 strMapToObj() 方法),然后在转换为对象 JSON
1
2
3
4
5
6
function strMapToJson(strMap) {
return JSON.stringify(strMapToObj(strMap))
}

let myMap = new Map().set('yes', true).set('no', false)
strMapToJson(myMap) // '{ "yes": true, "no": false }'

另一种情况是 Map 的键名有非字符串,这时可以选择转为数组 JSON,这时可以选择转为数组 JSON

1
2
3
4
5
6
function mapToArrayJson(map) {
return JSON.stringify([...map])
}

let myMap = new Map().set(true, 7).set({ foo: 3 }, ['abc']).set('yes', true)
mapToArrayJson(myMap) // '[[true, 7], [{ "foo": 3 }, ["abc"]], ["yes", true]]'
  • JSON 转为 MapJSON 转为 Map,正常情况下,所有键名都是字符串,也是使用之前的 objToStrMap() 方法
1
2
3
4
5
function jsonToStrMap(jsonStr) {
return objToStrMap(JSON.parse(jsonStr))
}

jsonToStrMap('{"yes": true, "no": false}') // Map {'yes' => true, 'no' => false}

但是有一种特殊情况,整个 JSON 就是一个数组,且每个数组成员本身,又是一个有两个成员的数组

1
2
3
4
5
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 结构类似,也是用于生成键值对的集合

1
2
3
4
5
6
7
8
9
10
11
// WeakMap 可以使用 set 方法添加成员
const wm1 = new WeakMap()
const key = { foo: 1 }
wm1.set(key, 2)
wm1.get(key) // 2

// WeakMap 也可以接受一个数组,作为构造函数的参数
const k1 = [1, 2, 3]
const k2 = [4, 5, 6]
const wm2 = new WeakMap([[k1, 'foo'], [k2, 'bar']])
wm2.get(k2) // 'bar'

WeakMapMap 的区别有两点

  • WeakMap 只接受对象作为键名(null 除外),不接受其他类型的值作为键名
  • WeakMap 的键名所指向的对象,不计入垃圾回收机制

WeakSet 一致,WeakMap 的键名所引用的对象都是弱引用,即垃圾回收机制不将该引用考虑在内,WeakMapMapAPI 上的区别主要是两个

  • 一是没有遍历操作(即没有 keys()values()entries() 方法),也没有 size 属性,因为没有办法列出所有键名
  • 二是无法清空,即不支持 clear() 方法

因此,WeakMap 只有四个方法可用 get()set()has()delete()

  • get(),返回键值对
  • set(),添加键值对,返回实例
  • delete(),删除键值对,返回布尔值
  • has(),检查键值对,返回布尔值

下面我们来看一些 WeakMap 的使用场景

在 DOM 对象上保存相关数据

传统使用 jQuery 的时候,我们会通过 $.data() 方法在 DOM 对象上储存相关信息,当你将 DOM 元素删除,DOM 对象置为空的时候,相关联的数据并不会被删除,你必须手动执行 $.removeData() 方法才能删除掉相关联的数据,WeakMap 就可以简化这一操作

1
2
3
4
5
6
7
8
let wm = new WeakMap(), el = document.querySelector('.el')
wm.set(el, 'data')

let value = wm.get(el)
console.log(value) // data

el.parentNode.removeChild(el)
el = null

数据缓存

从之前的例子我们可以看出,当我们需要关联对象和数据,比如在不修改原有对象的情况下储存某些属性或者根据对象储存一些计算的值等,而又不想管理这些数据的时候就可以考虑使用 WeakMap,数据缓存就是一个非常好的例子

1
2
3
4
5
6
7
8
9
10
11
12
const cache = new WeakMap()
function countOwnKeys(obj) {
if (cache.has(obj)) {
console.log('Cached')
return cache.get(obj)
} else {
console.log('Computed')
const count = Object.keys(obj).length
cache.set(obj, count)
return count
}
}

部署私有属性

WeakMap 也可以被用于实现私有变量,不过在 ES6 中实现私有变量的方式有很多种,这只是其中一种

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const privateData = new WeakMap()

class Person {
constructor(name, age) {
privateData.set(this, {
name,
age
})
}

getName() {
return privateData.get(this).name
}

getAge() {
return privateData.get(this).age
}
}

const p = new Person('zhangsan', 20)

p.name // undefined
p.getName() // zhangsan

结论 && 区别

  • SetMapWeakSetWeakMap、都是一种集合的数据结构
  • SetWeakSet 是一种值-值的集合,且元素唯一不重复
  • MapWeakMap 是一种键-值对的集合,Map 的键可以是任意类型,WeakMap 的键只能是对象类型
  • Set 添加值使用 add()Map 添加值和返回键值对使用 set()/get()
  • SetMap 可遍历,WeakSetWeakMap 不可遍历
  • WeakSetWeakMap 键名所指向的对象,不计入垃圾回收机制

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×