03 数据类型与数据结构
ES6 新增了 Synmbol 数据类型和 Set、Map 两种数据据结构,以及衍生的 WeakSet 和 WeakMap。之前工作中基本未用过,惭愧之至,努力学习之。
Symbol 数据类型
Symbol 概述
ES6 引入了一种新的原始数据类型 Symbol,表示独一无二的值。它是 JavaScript 的 第 7 种数据类型,前 6 种分别是:Undefined、Null、布尔值(Boolean)、字符串(String)、数值(Number)和对象(Object)。
Symbol 值通过 Symbol 函数生成。注意 Symbol 函数前不能使用 new 命令,否则会报错。这是因为生成的 Symbol 是一个原始类型的值,不是对象,基本上,它是一种类似于字符串的数据类型。
Symbol 函数可以接受一个字符串作为参数,表示对 Symbol 实例的描述,主要是为了在控制台显示,或者转为字符串时比较容易区分。
如果 Symbol 的参数是一个对象,就会调用该对象的 toString 方法,将其转为字符串,然后才生成一个 Symbol 值。
需要注意:每一个 Symbol 值都是不相等的,Symbol 函数的参数只表示对当前 Symbol 值的描述,因此相同参数的 Symbol 函数的返回值是不相等的。
Symbol 值不能与其他类型的值进行运算,否则会报错。但 Symbol 值可以显式转为字符串。另外,Symbol 值也可以转为布尔值,但是不能转为数值。
Symbol 属性名遍历
Symbol 作为属性名,该属性不会出现在 for...in、for...of 循环中,也不会被 Object.keys()、Object.getOwnPropertyNames() 返回。但它也不是私有属性,Object.getOwnropertySymbols 方法可以获取指定对象的所有 Symbol 属性名。该方法返回一个数组,成员是当前对象的所有用作属性名的 Symbol 值。
此外,Reflect.ownKeys
方法可以返回所有类型的键名,包括常规键名和 Symbol 键名。
Symbol.for()、Symbol.keyFor()
Symbol.for()
与 Symbol()
这两种写法都会生成新的 Symbol。它们的区别是,前者会被登记在全局环境中供搜索,而后者不会。Symbol.for() 不会在每次调用时都返回一个新的 Symbol 类型的值,而是会先检查给定的 key 是否已经存在,如果不存在才会新建一个值。
Symbol.keyFor 方法返回一个已登记的 Symbol 类型值的 key。
注意:Symbol.for 为 Symbol 值登记的名字是全局环境的,可以在不同的 iframe 或 service worker 中取到同一个值。
内置的 Symbol 值
除了定义自己使用的 Symbol 值,ES6 还提供了 11 个内置的 Symbol 值,指向语言内部使用的方法。
Symbol.hasInstance
对象的 Symbol.hasInstance
属性指向一个内部方法,对象使用 instanceof 运算符时会调用这个方法,判断该对象是否为某个构造函数的实例。比如,foo instanceof Foo 在语言内部实际调用的是 Foo[Symbol.hasInstance](foo)
。
Symbol.isConcatSpreadable
对象的 Symbol.isConcatSpreadable
属性等于一个布尔值,表示该对象使用 Array.prototype.concat() 时是否可以展开。
该值为 true 或 undefined 时可以展开,为 false 时不可展开。
Symbol.species
对象的 Symbol.species
属性指向当前对象的构造函数。创造实例时默认会调用这个方法,即使用这个属性返回的函数当作构造函数来创造新的实例对象。
上面的代码中,子类 MyArray 继承了父类 Array。创建 MyArray 的实例对象时,本来会调用它自己的构造函数(本例中被省略了),但是由于定义了 Symbol.species 属性,所以会使用这个属性返回的函数来创建 MyArray 的实例。
默认的 Symbol.species 属性等同于下面的写法:
Symbol.match
对象的 Symbol.match
属性指向一个函数,当执行 str.match(myObject) 时,如果该属性存在,会调用它返回该方法的返回值。
示例如下:
Symbol.replace
对象的 Symbol.replace
属性指向一个方法,当对象被 String.prototype.replace 方法调用时会返回该方法的返回值。
示例如下:
上面示例中,x[Symbol.replace] 方法传入两个参数 this 和 replaceValue 分别为 'Hello' 和 'World',经过参数扩展运算符转换为数组后输出 '["Hello", "World"]'。
Symbol.search
对象的 Symbol.search
属性指向一个方法,当对象被 String.prototype.search 方法调用时会返回该方法的返回值。
示例如下:
Symbol.split
对象的 Symbol.split
属性指向一个方法,当对象被 String.prototype.split 方法调用时会返回该方法的返回值。
示例如下:
Symbol.iterator
对象的 Symbol.iterator
属性指向该对象的默认遍历器方法。
对象进行 for...of 循环时,会调用 Symbol.iterator 方法返回该对象的默认遍历器。
Symbol.toPrimitive
对象的 Symbol.toPrimitive
属性指向一个方法,对象被转为原始类型的值时会调用这个方法,返回该对象对应的原始类型值。
Symbol.toPrimitive 被调用时会接受一个字符串参数,表示当前运算的模式。一共有 3 种模式:
Number:该场合需要转成数值。
String:该场合需要转成字符串。
Default:该场合可以转成数值,也可以转成字符串。
Symbol.toStringTag
对象的 Symbol.toStringTag
属性指向一个方法,在对象上调用 Object. prototype.toString 方法时,如果这个属性存在,其返回值会出现在 toString 方法返回的字符串中,表示对象的类型。也就是说,这个属性可用于定制 [object Object] 或 [object Array] 中 object 后面的字符串。
ES6 新增了大量内置对象的 Symbol.toStringTag 属性值,这里略。
Symbol.unscopables
对象的 Symbol.unscopables
属性指向一个对象,指定了使用 with 关键字时哪些属性会被 with 环境排除。
上面代码说明,数组有 7 个属性会被 with 命令排除。
示例如下:
上面的代码通过指定 Symbol.unscopables 属性使 with 语法块不会在当前作用域寻找 foo 属性,即 foo 将指向外层作用域的变量。
Set 数据结构
Set
ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复。
Set 基本用法
Set 本身是一个构造函数,用来生成 Set 数据结构。Set 函数可以接受一个数组(或者具有 iterable 接口的其他数据结构)作为参数,用来初始化。
借助 Set 成员的唯一性,可以实现数组去重:
Set 内部判断两个值是否相同时使用的算法叫作 “Same-value equality”,它类似于精确相等运算符(===),主要的区别是 NaN 等于自身,而精确相等运算符认为 NaN 不等于自身。在 Set 内部,两个 NaN 是相等的。
Set 属性和方法
Set 结构的实例有以下属性:
Set.prototype.constructor:构造函数,默认就是 Set 函数。
Set.prototype.size:返回 Set 实例的成员总数。
Set 实例的方法分为两大类:操作方法(用于操作数据)和遍历方法(用于遍历成员)。先介绍 4 个操作方法:
add(value):添加某个值,返回 Set 结构本身。
delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
has(value):返回一个布尔值,表示参数是否为 Set 的成员。
clear():清除所有成员,没有返回值。
add 方法返回 Set 结构本身,所以可以链式调用:
Set 遍历操作
Set 结构的实例有 4 个遍历方法,可用于遍历成员:
keys():返回键名的遍历器。
values():返回键值的遍历器。
entries():返回键值对的遍历器。
forEach():使用回调函数遍历每个成员。
需要特别指出的是:Set 的遍历顺序就是插入顺序。这个特性有时非常有用,比如使用 Set 保存一个回调函数列表,调用时就能保证按照添加顺序调用。
keys 方法、values 方法、entries 方法返回的都是遍历器对象。由于 Set 结构没有键名,只有键值(或者说键名和键值是同一个值),所以 keys 方法和 values 方法的行为完全一致。
Set 结构的实例默认可遍历,其默认遍历器生成函数就是它的 values 方法。
这意味着,可以省略 values 方法,直接用 for...of 循环遍历 Set:
Set 结构实例的 forEach 方法用于对每个成员执行某种操作,没有返回值。该函数的参数依次为键值、键名、集合本身。还可传入第二个参数表示绑定的 this 对象。
扩展运算符(...)内部使用 for...of 循环,所以也可以用于 Set 结构。
数组的 map 和 filter 方法也可以用于 Set。因此使用 Set 可以很容易地实现并集(Union)、交集(Intersect)和差集(Difference)。
目前没有直接的方法在遍历操作中同步改变原来的 Set 结构,但有两种变通方法。一种是利用原 Set 结构映射出一个新的结构,然后赋值给原来的 Set 结构;另一种是利用 Array.from 方法。
WeakSet
WeakSet 含义
WeakSet 结构与 Set 类似,也是不重复的值的集合。但是,它与 Set 有两个区别:
WeakSet 的成员只能是对象,而不能是其他类型的值。
WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象是否还存在于 WeakSet 之中。
垃圾回收机制依赖引用计数,如果一个值的引用次数不为 0,垃圾回收机制就不会释放这块内存。结束使用该值之后,有时会忘记取消引用,导致内存无法释放,进而可能会引发内存泄漏。
WeakSet 里面的引用都不计入垃圾回收机制,所以就不存在这个问题。因此,WeakSet 适合临时存放一组对象,以及存放跟对象绑定的信息。只要这些对象在外部消失,它在 WeakSet 里面的引用就会自动消失。
WeakSet 的成员是不适合引用的,因为它会随时消失。另外,WeakSet 内部有多少个成员取决于垃圾回收机制有没有运行,运行前后很可能成员个数是不一样的,而垃圾回收机制何时运行是不可预测的,因此 ES6 规定WeakSet 不可遍历。
WeakSet 语法
WeakSet 是一个构造函数,可以使用 new 命令创建 WeakSet 数据结构。
同 Set 一样,WeakSet 函数可以接受一个数组(或者具有 iterable 接口的其他数据结构)作为参数,用来初始化。需要注意:成为 WeakSet 的成员的是数组的成员,而不是数组本身。这意味着,数组的成员只能是对象。
WeakSet 结构有以下 3 个方法:
WeakSet.prototype.add(value):向 WeakSet 实例添加一个新成员。
WeakSet.prototype.delete(value):清除 WeakSet 实例的指定成员。
WeakSet.prototype.has(value):返回一个布尔值,表示某个值是否在 WeakSet 实例中。
WeakSet 没有 size 属性,没有办法遍历其成员。
WeakSet 的一个用处是储存 DOM 节点,而不用担心这些节点从文档移除时会引发内存泄漏。举个栗子:
上面的代码保证了 Foo 的实例方法只能在 Foo 的实例上调用。同时,数组 foos 对实例的引用不会被计入内存回收机制,所以删除实例的时候不用考虑 foos,也不会出现内存泄漏。
Map 数据结构
Map
Map 基本用法
JavaScript 的对象(Object)本质上是键值对的集合(Hash 结构),但是只能用字符串作为键,这给它的使用带来了很大的限制。
上面代码将一个 DOM 节点作为对象 data 的键,由于对象只接受字符串作为键名,所以 element 被自动转为字符串 [Object HTMLDivElement]
。
ES6 提供了 Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应,是一种更完善的 Hash 结构实现。如果需要“键值对”的数据结构,Map 比 Object 更合适。
作为构造函数,Map 也可以接受一个数组作为参数。该数组的成员是一个个表示键值对的数组。
Map 构造函数接受数组作为参数,实际上执行的是下面的算法:
事实上,任何具有 Iterator 接口且每个成员都是一个双元素数组的数据结构都可以当作 Map 构造函数的参数。也就是说,Set 和 Map 都可以用来生成新的 Map。
两点注意事项:
如果对同一个键多次赋值,后面的值将覆盖前面的值。
只有对同一个对象的引用,Map 结构才将其视为同一个键。
上面的 set 和 get 方法表面上是针对同一个键,实际上却是两个值,内存地址是不一样的,因此 get 方法无法读取该键,返回 undefined。
Map 的键实际上是和内存地址绑定的,只要内存地址不一样,就视为两个键。如果 Map 的键是一个简单类型的值(数字、字符串、布尔值),则只要两个值严格相等,Map 就将其视为一个键,包括 0 和 -0。另外,虽然 NaN 不严格等于自身,但 Map 将其视为同一个键。
Map 属性和方法
Map 结构的 size 属性返回 Map 结构的成员总数。Map 结构的操作方法如下:
set(key, value):set 方法设置 key 所对应的键值,然后返回整个 Map 结构。如果 key 已经有值,则键值会被更新,否则就新生成该键。可以链式调用。
get(key):get 方法读取 key 对应的键值,如果找不到 key,则返回 undefined。
has(key):has 方法返回一个布尔值,表示某个键是否在 Map 数据结构中。
delete(key):delete 方法删除某个键,返回 true。如果删除失败,则返回 false。
clear():clear 方法清除所有成员,没有返回值。
Map 遍历操作
Map 原生提供了 3 个遍历器生成函数和 1 个遍历方法。
keys():返回键名的遍历器。
values():返回键值的遍历器。
entries():返回所有成员的遍历器。
forEach():遍历 Map 的所有成员。
Map 结构的默认遍历器接口(Symbol.iterator 属性)就是 entries 方法。
WeakMap
WeakMap 含义
WeakMap 结构与 Map 结构类似,也用于生成键值对的集合。WeakMap 与 Map 的区别有以下两点:
WeakMap 只接受对象作为键名(null 除外),不接受其他类型的值作为键名。
WeakMap 的键名所指向的对象不计入垃圾回收机制。
类似于 WeakSet,WeakMap 的键名所引用的对象都是弱引用,即垃圾回收机制不将该引用考虑在内。因此,只要所引用的对象的其他引用都被清除,垃圾回收机制就会释放该对象所占用的内存。
WeakMap 的专用场景就是它的键所对应的对象可能会在将来消失的场景。WeakMap 结构有助于防止内存泄漏。
上面的 DOM 节点对象的引用计数是 1,而不是 2。这时,一旦消除对该节点的引用,它占用的内存就会被垃圾回收机制释放。WeakMap 保存的这个键值对也会自动消失。
需要注意:WeakMap 弱引用的只是键名而不是键值。键值依然是正常引用的。
WeakMap 语法
WeakMap 与 Map 在 API 上的区别主要有两个:
一是没有遍历操作(即没有 key()、values() 和 entries() 方法),也没有 size 属性。因为没有办法列出所有键名,某个键名是否存在完全不可预测,和垃圾回收机制是否运行相关。
二是无法清空,即不支持 clear 方法。因此,WeakMap 只有 4 个方法可用:get()、set()、has()、delete()。
WeakMap 用途
WeakMap 应用的典型场景就是以 DOM 节点作为键名的场景:
上面的代码中,myElement 是一个 DOM 节点,每当发生 click 事件就更新一下状态。状态作为键值放在 WeakMap 里,对应的键名就是 myElement。一旦这个 DOM 节点删除,该状态就会自动消失,不存在内存泄漏风险。
注册监听事件的 listener 对象很适合用 WeakMap 来实现:
上面的代码中,监听函数放在 WeakMap 里面。一旦 DOM 对象消失,与它绑定的监听函数也会自动消失。
WeakMap 的另一个用处是部署私有属性:
上面的代码中,Countdown 类的两个内部属性 _counter 和 _action 是实例的弱引用,如果删除实例,它们也会随之消失,不会造成内存泄漏。
最后更新于