重学JavaScript系列——(六)集合引用类型
博主以扎实JavaScript基础为目的,以《JavaScript高级程序设计(第四版)》为核心参考资料,以一个“复习者”的角度有针对性地来创作这期专栏。文章加入了博主的很多思考和开发经验,关注初学JavaScript时容易忽略的地方,着重总结了ECMAScript新标准知识点的特性和应用场景。最终,本专栏将覆盖完整的JavaScript知识体系,以辅佐各路豪杰在开发路上的稳步前进。
专栏传送门:https://blog.csdn.net/huoyihengyuan/category_10586561.html
文章目录
本章继续讨论内置的引用类型,包括Object、Array、Map、WeakMap、Set、WeakSet相关内容。
6.1 Object
虽然Object实例没多少功能,但适合存储和在应用中交换数据。
显式地创建Object实例的方法,一种是通过new和构造函数,另一种是对象字面量表示法(即使用“{}”)。
在使用对象字面量表示法定义对象时,并不会实际调用Object构造函数。
属性一般是通过点语法才存取的,但也可以通过中括号来存取,主要优势是可以通过变量来访问属性,以及可以传入带空格的字符串。
第8章将会更全面而深入地介绍Object类型。
6.2 Array
6.2.1 创建数组
类似Object,Array也是同样可以使用new和数组字面量表示法创建,并且使用数组字面量表示法时不会调用Array构造函数。
不过,ES6新增了两个创建数组的静态方法:Array.form()用于将类数组结构转化为数组实例;Array.of()用于将一组参数转化为数组实例。
类数组结构,即任何可迭代的结构,或有一个length属性和可索引元素的结构。
6.2.2 数组空位
使用字面量初始化数组时,可以用一串逗号来创建空位(hole),ES6规范重新定义了该如何处理这些空位。
let arr = [,,,,]
ES6新增方法和迭代器和早期ECMAScript版本中方法行为不同。ES6新增方法普遍将空位当成undefined元素,而早期的方法可能会将空位当成单纯的空empty。
由于行为不一致和存在性能隐患,应尽量避免数组空位,实在需要可以显示地使用undefined替代。
6.2.3 数组索引
数组的length表示数组长度,但它不是只读的,我们可以强制设置length等于2来设置原数组自动删除到2个元素长度。
6.2.4 检测数组
Array.isArray(value)用于检测一个值是否是数组。
6.2.5 迭代器方法
循环遍历数组的方法我们已经很熟悉了,其实ES6中数组还提供了三个返回迭代器的方法:keys()返回数组索引迭代器、values()返回数组元素迭代器、entries()返回键值对的迭代器。
6.2.6 复制和填充方法
ES6新增了两个方法:fill()可向一个数组中插入全部或部分相同的值;copyWithin()会按照指定范围浅复制数组中的部分内容。
6.2.7 转换方法
valueOf()返回数组本身;toString()返回逗号分隔的数组字符串;toLocaleString()会依次调用每个数组元素的toLocaleString()方法。
6.2.8 栈方法
基于栈先进后出的特性,可以通过数组的push()和pop()来模拟栈的行为。
6.2.9 队列方法
基于队列先进先出的特性,可以通过数组的push()和shift()来模拟队列的行为。
6.2.10 排序方法
reverse()可以倒序数组。
sort()方法会在每一项上调用String转型函数,然后按照比较字符串的顺序重新排列数组。如果要改变sort的排序行为,可以在参数中传递函数。
对于Number数组来说,可以直接用
arr.sort((a,b)=>a-b)
来操作。
6.2.11 操作方法
- concat()方便合并两个数组;
- slice()切割子数组。
- splice()方法可以便捷删除、插入和替换数组内容。
6.2.12 搜索和位置方法
(1)按严格相等搜索
indexOf()、lastIndexOf()、includes()
(2)断言函数
ECMAScript也允许通过断言函数搜索数组。主要利用find()和findIndex(),用法类似map()。
6.2.13 迭代方法
数组的5个迭代方法,都不改变调用它们的数组:
- forEach():遍历数组,无返回值。
- map():遍历数组,返回新数组。
- filter():将函数返回true的项组成数组返回。
- every():所有项都返回true,则这个方法返回true。
- some():任意一项返回true,则这个方法返回true。
6.2.14 归并方法
reduce((上个归并值prev,当前值cur,index,arr)=>prev+cur)
reduceRight()则倒序归并。
6.3 定型数组
6.3.1 历史和背景
随着浏览器的流行,为了充分利用3D图形API和GPU加速,以便在canvas上渲染复杂的图形,后来发布了WebGL。但在WebGL早期版本中,因为JavaScript数组和原生数组之间不匹配,JavaScript的双精度浮点格式总是需要转换为WebGL需要的适当格式,而这些需要花费很多时间。
当时,Mozilla为了解决这个问题,实现了CanvasFloatArray。这是一个提供JavaScript接口、C语言风格的浮点值数组。JavaScript运行时操作CanvasFloatArray可以直接传给底层图形API。CanvasFloatArray最终变成了Float32Array,也就是今天定型数组中可用的第一个类型。
6.3.2 ArrayBuffer
定型数组中的一个类型是Float32Array,它实际上是一种“视图”,可以允许JavaScript运行时访问一块名为ArrayBuffer的预分配内存。
ArrayBuffer()是个普通的JavaScript构造函数,可以通过例如new ArrayBuffer(16)
可以在内存中分配指定数量的字节空间。然后再利用“部分Array通用API”进行操作。
ArrayBuffer()一旦创建就不能再分配大小。
不能仅通过对ArrayBuffer的引用就直接读取或写入内容,必须要利用视图。视图有不同的类型,但引用的都是ArrayBuffer存储的二进制数据。
作者理解:ArrayBuffer为JavaScript提供了精确分配内存的读写方式,所以和常规数据类型直接通过变量读取的方式不同。所以我们要借用一种称之为“视图”的东西,来进行“窥探”和“获取”具体内容,可以理解成一种类似显微镜的工具,只有借助它才能继续操作。
6.3.3 DataView
DataView就是读写ArrayBuffer的视图的一种,转为文件I/O和网络I/O设计,其API支持对缓冲数据的高度控制。
(1)ElementType
DataView对存储在缓冲内的数据类型没有预设,即读出来的内容仅仅是元数据,所以在读写时需要强制指定一个类型ElementType,然后DataView在读写时就会完成相应的转换。
ElementType | 字节 | 说明 | 等价的C类型 | 值的范围 |
---|---|---|---|---|
Int8 | 1 | 8位有符号整数 | signed char | -128~127 |
Uint8 | 1 | 8位无符号整数 | unsigned char | 0~255 |
Int16 | 2 | 16位有符号整数 | short | -32768~32767 |
Uint16 | 2 | 16位无符号整数 | unsigned short | 0~65335 |
Int32 | 4 | 32位有符号整数 | int | -2147483648~2147483647 |
Uint32 | 4 | 32位无符号整数 | unsigned int | 0~4294967295 |
Float32 | 4 | 32位IEEE-754浮点数 | float | -3.4e+38~+3.4e+38 |
Float64 | 8 | 64位IEEE-754浮点数 | double | -1.7e+308~+1.7e+308 |
DataView为上表的每种类型都暴露了set/get方法。
(2)字节序
“字节序”指的是计算系统维护的一种字节顺序的约定。DataView支持两种约定:大端字节序和小端字节序。大端字节序(也称“网络字节序”)最高有效位保存在第一个字节,最低有效位保存在最后一个字节。小端字节序正好相反。
JavaScript运行时所在系统的原生字节序决定了如何读写字节,但对DataView并不遵守这个约定,它的所有API以大端字节序作为默认值,但都可以接受一个可选布尔参数来启用小端字节序。
(3)边界情况
DataView完成读、写操作的前提是必须有充足的缓冲区,否则就会抛出RangeError。
DataView在写入缓存里会尽最大努力把一个值转换为适当的类型,后备为0,如果无法转换,抛出错误。
6.3.4 定型数组
如果对Web绘图性能技术感兴趣,这个方面不失为一个不错的学习重点。
定型数组(typed array),就是将任何数字转换为一个包含数字比特的数组,随后就可以通过我们熟悉的JS数组方法来进一步处理。
DataView是一种访问ArrayBuffer的视图,定型数组是另一种形式的ArrayBuffer视图。定型数组特定于一种ElementType且遵循系统原生的字节序。相应地,它提供了使用面更广的API和更高性能。
定型数组的设计目的就是提高与WebGL等原生库数据交换的效率,由于定型数组的二进制表示对操作系统而言也是一种容易使用的格式,JavaScript引擎可以重度优化算数运算、按位运算等等,因此它们速度极快。
(1)定型数组的行为
定型数组提供了很多和常规数组相同的API,并且实现了Symbol.iterator符号属性,依次也具有迭代器的行为。
(2)合并、复制和修改定型数组
定型数组同样使用数据缓冲来存储数据,而数组缓冲无法调整大小,因此诸如push()等等的方法不适用于定型数组。
不过,定型数组提供了两个新方法:
- set():从数组或定型数组中把值复制到当前定型数组制定的索引位置。
- subarray():与set()相反,它基于原始定型数组中复制一个值返回一个新定型数组。
虽然定型数组没有原生的拼接能力,但我们也可以利用它们的API手动构建一些我们需要的方法。
(3)下溢和上溢
定型数组中值的下溢和上溢不会影响到其他索引,有必要的时候仍需要我们对它们的溢出情况提前做好考量,以选用8种元素类型中最合适的那一种。
(4)常见定型数组
6.4 Map
在ES6之前,“键值对”的存储方式一直是用Object来完成的。但作为ES6的新特性,Map(映射)为这门语言带来了真正的键值对存储机制。
6.4.1 基本API
- set()添加键值对
- get()和has()进行查询
- size属性反映键值对的数量
- delete()和clear()删除值
与Object只能利用数值、字符串、符号作为键不同,Map可以使用任意JavaScript类型(包括Object)作为键。Map内部使用SameValueZero比较操作,基本上相当于使用严格相等的标准来检查键的匹配性。
假设一个Object实例A作为键存储到Map中,当该A中的内容改变时,并不影响通过map.get(A)来获取到对应的值。
SameValueZero是ECMAScript规范新增的相等性比较算法。
6.4.2 顺序和迭代
与Object一个主要差异是,Map实例会维护键值对的插入顺序。
Map实例可以引用entires()来取得一个迭代器,这个迭代器能以插入顺序生成[key,value]形式的数组。
如果不使用迭代器,可以通过forEach((key,value)=>{})进行传入回调。
也可以使用keys()和values()来分别返回以插入顺序生成的迭代器。
6.4.3 选择Object还是Map
对于多数Web开发者而言,Object还是Map只是偏好问题。不过,对于在乎内存和性能的开发者之之间,对象和映射之间存在显著差别。
总的来说,在内存占用、操作性能上,Map整体更胜一筹。
6.5 WeakMap
ECMAScript6新增了“弱映射”(WeakMap)是Map的“兄弟类型”,其API是Map的子集。WeakMap中的“weak(弱)”描述的是JavaScript垃圾回收程序对待“弱映射”中键的方式。
6.5.1 基本API
弱映射中的键只能是Object或继承自Object的类型,尝试使用非对象设置键会抛出TypeError。
同样是使用set()、has()、get()、delete()进行操作。
6.5.2 弱键
实际上,“weak”表示弱映射的键是“弱弱地拿着”,含义是,这些键不属于正式的引用,不会阻止垃圾回收。要注意,对于值来说,没有weak的含义。
举个例子:
const wm = new WeakMap()
wm.set({},'val')
set()方法初始化了一个新对象作为字符串的键。因为没有指向这个对象的引用,所以程序执行完毕后,对象的键会被当作垃圾回收,然后这个键值对就从弱映射中消失了,使之成为一个空映射,所以,值本身也会被当作垃圾回收。
6.5.3 不可迭代键
因为WeakMap在任何情况下都有可能被销毁,所以就没必要提供迭代键值对的能力,当然也没有clear()方法。
6.5.4 应用场景
WeakMap与现有JavaScript对象有很大不同,这个问题没有唯一的答案,但已经出现了很多相关策略。
(1)私有变量
弱映射造就了在JavaScript中实现私有变量的一种新方式。前提很明确:私有变量会存储在弱映射中,以对象实例为键,以私有成员的字典为值。
(2)DOM节点元数据
WeakMap不会阻止垃圾回收,所以非常适合保存关联元数据。
举个例子:给一个按钮绑定点击事件,并进行计数,需要一个变量count来保存当前的的次数。如果按钮的DOM销毁后,没有把count指向null,便会造成内存泄漏。如果我们使用WeakMap来保存count,就能够在按钮DOM销毁后,自动销毁count所占用的内存。
<button type="button" id="btn">按钮</button>
<script>
let wm = new WeakMap();
let btn = document.querySelector('#btn');
wm.set(btn, {count: 0});
btn.addEventListener('click', () => {
let v = wm.get(btn);
v.count++;
console.log(wm.get(btn).count);
</script>
6.6 Set
除了Map,ES6新增了Set,这是一种集合类型,他和Map有很多共有API和行为。
6.6.1 基本API
- add()添加值,支持链式调用。
- has()查询。
- size属性取得元素数量。
- delete()和clear()删除元素。
与Map类似,Set也使用SameValueZero进行相等性比较。
6.6.2 顺序与迭代
Set也会维护插入时的顺序,支持顺序迭代。
可以通过values()和其别名方法keys()取得迭代器,两个方法完全相等。
同样也可以使用entires()和forEach()进行迭代和回调。
6.6.3 定义正式集合操作
很多开发者都喜欢用Set操作,并且手动封装一些函数。比如手动实现set集合的布尔运算等等。
Set的很多方法是有关联性的,不要修改已有的集合实例。另外作为开发者,要适当的去考虑set的按顺序插入、去重性、尽可能高效地使用内存。
6.7 WeakSet
类似WeakMap,“弱集合”(WeakSet)是一种新的集合类型。
6.7.1 基本API
初始化之后,可以使用add()、has()、delete()进行操作。
6.7.2 弱值
类似于WeakMap的弱键,WeakSet的值具有“weak”的含义,即不会阻止垃圾回收。
6.7.3 不可迭代值
WeakSet同样不可迭代,并且无clear()。
6.7.4 应用场景
相对于WeakMap,WeakSet用处没那么大。
这里是个小应用场景:
创建一个WeakSet用来给DOM节点“打禁用标签”,查询元素是否在WeakSet实例中即可知道它是不是被禁用了,如果节点被在DOM树中删除,垃圾回收就可以直接释放其内存。
个人觉得,这个场景倒真的不如自定义属性
data-*
来得实在。
6.8 迭代与扩展操作
ES6新增的迭代器和扩展操作符对集合引用类型特别有用,这些新特性让集合类型间相互操作、复制和修改变得异常方便。
正因如此,开发者即可对他们很方便地使用for-of
、扩展运算符(…)、Array.of()、Array.from()等方法。
小结
JavaScript中的对象是引用值,可通过几种内置引用类型创建特定类型的对象。
引用类型与传统面向对象编程语言中的类相似,但实现不同。
- Object:基础类型,所有引用类型都继承了它的基本行为。
- Array:一组有序的值,并提供了操作和转换值的能力。
- 定型数组:针对在和WebGL等原生库数据交换时而使用的一种类似Array的引用类型,用于管理数值在内存中的类型。
- Date:提供了日期和时间信息。
- RegExp:提供了基本的正则表达式的接口,以及一些高级正则表达式的能力。
JavaScript比较独特的一点是,函数实际上是Function的实例,这意味着函数也是对象。由于函数也是对象,也就拥有增强自身的能力。
包装类型的存在,使JavaScript原始值可以拥有类似对象的行为。
JavaScript还有两个在执行代码时就存在的特殊对象:Global和Math。
- Global:相当于全局对象,在浏览器中实现为window对象。全局函数和变量都是Global的属性。
- Math:提供了辅助完成数学计算的属性和方法。
ES6新增了一批引用类型:Map、WeakMap、Set、WeakSet。为组织应用程序和简化内存管理提供了新的能力。