集合引用类型
Object
对象可以通过Object
构造函数声明,也可以通过对象字面量来声明:
let obj = {}
let obj = new Object()
使用字面量声明对象和使用Object构造函数声明变量是有区别的: 使用字面量定义对象时,并不会实际调用Object构造函数
- 表达式上下文: 指期待返回值的上下文,对象字面量左边花括号
{
出现在赋值操作符之后,赋值操作表示期待后面有一个值,,因此左括号表示一个表达式的开始 - 语句上下文: 出现在
if
等语句后面的{
叫语句上下文,表示语句块的开始
属性名
对象的属性名可以是字符串,数字或符号,并且可以包含非数字字母的字符.使用数字作为属性名时,数值会自动转换为字符串.使用其他类型的数据作为属性时会自动调用其toString
方法转换成字符串
var boolean = true; // Boolean
var fn = function () { //Function
console.log();
};
var obj1 = { b: 2 }; //Object
var arr = [1, 2, 3]; //数组
var date = new Date(); //时间对象
var reg = new RegExp(/1/); //正则对象
var symbol = Symbol(); //符号
var map = new Map(); //Map
var set = new Set([1, 2]); //Set
var obj = {
[boolean]: 1,
[fn]: 1,
[date]: 1,
[obj1]: 1,
[arr]: 1,
[reg]: 1,
[symbol]: 1,
[map]: 1,
[set]: 1,
};
Object.keys(obj).forEach((key) => console.log(typeof key));
输出:
string
string
string
string
string
string
string
console.log('obj:', obj);
输出
obj: {
true: 1,
'function () {\n console.log();\n}': 1,
'Mon Dec 11 2023 10:21:57 GMT+0800 (中国标准时间)': 1,
'[object Object]': 1,
'1,2,3': 1,
'/1/': 1,
'[object Map]': 1,
'[object Set]': 1,
[Symbol()]: 1
}
属性获取
一般使用点操作符
来获取属性,当需要通过变量获取属性时可以通过[]
来获取
const obj = {
'hello word': 1
}
obj.hello word //直接报错
ocj['hello word'] // 1
Array
创建数组
使用构造函数和数组字面量创建数组就不赘述了,主要介绍以下ES6新增了两个创建数组的方法:Array.from(),Array.of()
Array.from方法
from方法可以将任何可迭代结构,或者有一个length
属性和可索引的结构转换成数组实例: 如Arguments,string,通过querySelector获取的DOM元素类数组
等.他有三个参数:
-
类数组对象: 将该类数组对象转换成数组实例
//字符串 console.log(Array.from('hello')); // [ 'h', 'e', 'l', 'l', 'o' ] //Map console.log(Array.from(new Map().set('a', 1).set('b', 2))); //[ [ 'a', 1 ], [ 'b', 2 ] ] //Set console.log(Array.from(new Set().add(1).add(2))); // [ 1, 2 ] //实现浅复制 const arr1 = [1, 2]; const arr2 = Array.from(arr1); console.log(arr2 === arr1); //false // 任何可迭代对象 const iter = { *[Symbol.iterator]() { yield 1; yield 2; yield 3; yield 4; }, }; console.log(Array.from(iter)); //[ 1, 2, 3, 4 ] //带有必要属性的自定义对象,可以迭代但不一定对 const aa = { a: 3, b: 2, 0: 1, length: 4, }; console.log(Array.from(aa)); // [ 1, undefined, undefined, undefined ]
-
映射函数
这个参数是可选的,可以对新数组的值进行加工,相当于
map函数
,let str = 'hello'; console.log(Array.from(str, (s) => s + 1)); // [ 'h1', 'e1', 'l1', 'l1', 'o1' ]
-
映射函数的this
这个参数也是可选的,用于指定第二个参数的
this
,但重写的this
在箭头函数中不太适用
Array.of()方法
可以把一组参数转换成数组实例,用于替换之前常用的arguments
转换方法:Arrat.prototype.slice.call(arguments)
console.log(Array.of(...arguments));
数组的空位
使用字面量创建数组的时候,可以使用逗号来创建空位,
const arr = [,,,,];
console.log(arr.length) // 4
在ES6之前和ES6之后的方法对空位的处理是不同的:
-
ES6之前的方法会忽略空位,但具体行为因方法而异
var arr = [1, , , , 2]; console.log(arr.indexOf(2)); //4 arr.map(item => console.log(item)) // 1,2直接跳过对空位的处理
-
ES6新增的方法会将空位当成存在的元素,只不过值是undefined
var arr = [1, , , , 2]; console.log(Array.from(arr)); //[ 1, undefined, undefined, undefined, 2 ]
迭代器方法
迭代器方法指的是通过迭代器对象(iterator)来遍历集合中的元素
Array的原型上暴露了三个方法: keys,values,entries
-
keys:
返回数组索引的迭代器,结果可以直接通过
for of
进行循环const a = [1, 2, 8, 3, 4, 5, 6]; console.log(a.keys()); //Object [Array Iterator] {} const iterator = a.keys() for (let key of iterator) { console.log(key) //0,1,2,3,4,5 }
-
values:
与keys相似,只不过返回的是数组元素的迭代器对象
-
entries:
返回的是
[key,value]
形式的迭代器对象
复制和填充方法
-
fill()
参数: 1.填充内容 2.可选,填充起始位置 3.可选,填充结束位置
向数组中指定位置填充相同的值,没有指定开始索引则从0开始,没有指定结束索引将填充至数组末尾
const a = [1, 2, 8, 3, 4, 5, 6]; a.fill(0, 1, 3); console.log(a); //[ // 1, 0, 0, 3, // 4, 5, 6 //]
-
copyWithin()
浅复制开始索引到结束索引的内容,并插入到指定索引的位置
参数: 1.指定索引 2.开始索引,可选默认为0. 3.结束索引,可选默认为数组结尾
const a = [1,2,3,4]; const b = a.copyWithin(3,0,2);// 复制数组0~2位置的数据,并插入到3的位置 console.log(b) // [ 1, 2, 3, 1 ]
数组的方法大致可以分为以下几种:
-
转换方法
- toString()
- toLocaleString()
- valueOf()
-
栈方法
- push()
- pop()
-
队列方法
- shift(),也提供了unshift()方法,与shift使用方式相反
- push()
-
排序方法
- reverse()
- sort()
-
操作方法
- concat()
- slice()
- splice()
-
搜索和位置方法
- iindexOf()
- lastIndexOf()
- includes(): ECMAScript7提供的
-
断言函数
- find()
- findIndex()
-
迭代方法
- every()
- some()
- forEach()
- map()
- filter()
-
归并方法
- reduce()
- reduceRight()
各位看官如果这些方法忘记使用,快去MDN温习一下吧!
定型数组
本章节将介绍: 定型数组的由来,ArrayBuffer是什么,什么是视图,它能做什么,有哪些?
定型数组也叫类型化数组是ECMAScript新增的数据结构目的是为了提升向原生库(WebGL,Canvas等)传输数据的效率js类型化数组是一种类似数组的对象,并提供了一种在内存缓冲中访问原始二进制的机制, 类型化数组的每一个元素都是以某种格式表示的原始二进制,特别适合文件I/O,网络通信等交互.js支持从8位整数到64位浮点数的多种二进制格式
定型数组的由来
在早期随着浏览器的流行,人们希望通过浏览器来运行更加复杂的3D程序,后来出现了WebGL,其提供了一套API用于构建3D应用程序,但JS的数组和原生数组不匹配导致js与WebGL之间出现了性能问题.问题根因就是js数组在内存中的格式是双精度浮点数,而WebGL的API不需要这种格式的数据.导致每次WebGl和js传递数组的时候,都需要将js数组中的数据进行类型转换,于是就出现了类型化数组
类型化数组的组成
javascript
类型化数组的实现拆分为:缓冲和视图.
- 缓冲: 是一种表示数据块的对象,没有格式,也不能直接访问.可以理解为存储二进制数据的仓库
- 视图: 访问缓冲只能通过视图,其是可以以不同的的数据类型解释存储在数组缓冲区的二进制数据.简单理解就是一个提供了操作缓冲方法的对象
缓冲
缓冲包含两种: ArrayBuffer
和ShareArrayBuffer
,缓冲支持一下操作:
-
创建: 通过构造函数创建一个缓冲,会分配新的内存块,并初始化为0.参数表示创建缓冲区的大小
const buffer = new ArrayBuffer(8) //创建一个8字节的缓冲区
-
复制: 通过
slice
可以高效的复制缓冲中的部分数据 -
转移: 使用
transfer()
和transferToFixedLength
,可以将内存块的所有权转移给一个新的缓冲对象,可以方便的在不同执行上下文之间转移数据,而不必复制.转移后的源数据将不可用,ShareArrayBuffer
不能被转移,因为其已经共享于所有的执行上下文 -
调整大小: 使用
resize()
方法,可以调整内存块的大小,只要不超过预设maxByteLength
.ShareArrayBuffer
只能增长不能缩小
ArrayBuffer
通过ArrayBuffer
构造函数创建,参数为缓存区的大小,缓冲区的元素会初始化为0,
参数:
- length: 必选,要创建的数组缓冲区的大小
- option: 可选,包含
maxByteLength
的对象:maxByteLength
表示数组缓冲区可以调整的最大大小:,以字节为单位
// 创建一个八字节大小的缓冲区,并设置其可调整大小为12
const buffer = new ArrayBuffer(8, { maxByteLength: 12 });
console.log(buffer) //结果如下1:
buffer.resize(12)
console.log(buffer) // 结果如下2:
属性:
- byteLength: 数组缓冲区的长度
- maxByteLength:数组缓冲区可调整的最大长度
- resizable: 数组缓冲区是否可以调整大小
方法:
- Array.isView():判断传入的值是不是
ArrayBuffer
的视图之一 Array.prototype.resize()
:调整缓冲区大小Array.prototype.slice()
:返回新的ArrayBuffer
实例,包含已有ArrayBuffer
的字节副本Array.prototype.transfer()
:实验阶段的方法,可以将缓冲区从当前上下文转移到另一上下文
ShareArrayBuffer
由于安全和隐私问题,现代浏览器大多禁用了对 SharedArrayBuffer
的支持,故不在这里介绍
视图
视图主要有两种: 类型化数组视图
和DataView
,类型化数组视图
可以方便的转换二进制格式.DataView
更加底层,可以精确控制数据的访问形式.两种视图都会使ArrayBuffer.isView()
返回true,他们都有以下属性:
-
buffer: 视图所引用的底层缓冲
const buffer = new ArrayBuffer(8, { maxByteLength: 12 }); const view = new DataView(buffer); console.log(view.buffer === buffer);//true
-
byteOffset: 视图相对于缓冲起始位置的便宜量(字节为单位)
-
byteLength: 视图长度(字节为单位)
此外类型化数组视图的构造函数还接受length
作为元素数量,而不是字节长度
类型化数组视图
类型化数组视图TypedArray
不是指一种视图,而是11种特定类型试图集合的统称,这11种类型数组分别包括:
类型(对应其构造函数) | 值范围 | 字节数 | 描述 | 对应的web IDL 类型 | 等效的C类型 |
---|---|---|---|---|---|
Int8Array | -128~127 | 1 | 8位有符号整数(补码) | byte | int8_t |
Uint8Array | 0~255 | 1 | 8位无符号整数 | cotet | uint8_t |
Uint8ClampedArray | 0~255 | 1 | 8位无符号整数(值会被裁剪) | octet | uint8_t |
Int16Array | -32768~32767 | 2 | 32位有符号整数(补码) | short | int16_t |
Uint16Array | 0-65535 | 2 | 16位无符号整数 | undigned | uint16_t |
Int32Array | -2147483648~2147483647 | 4 | 32 位有符号整数(补码 | long | int32_t |
Uint32Array | 0~4294967295 | 4 | 32 位无符号整数 | unsigned long | uint32_t |
Float32Array | -3.4E38 ~3.4E38 以及 1.2E-38 (最小正数) | 4 | 32 位 IEEE 浮点数(7 位有效数字,例如 1.123456 ) | unrestricted float | float |
Float64Array | -1.8E308 ~1.8E308 以及 5E-324 (最小正数) | 8 | 64 位 IEEE 浮点数(16 位有效数字,例如 1.123...15 ) | unrestricted double | double |
BigInt64Array | -2^63~2^63 - 1 | 8 | 64 位有符号整数(补码 | bigint | int64_t (signed long long) |
BigUint64Array | 0~2^64 - 1 | 8 | 64 位无符号整数 | bigint | uint64_t (unsigned long long) |
所有类型化数组类型都有相同的方法和属性,这里使用Int8Array
进行举例:
-
创建:
-
三个参数
new Int8Array(buffer [, byteOffset [, length]]); 参数: buffer: 缓冲 byteOffset: 字节偏移量,视图从buffer哪个位置开始关联对应的缓冲 lenght: 包含元素的个数 const buffer = new ArrayBuffer(16) //创建一个大小16字节的缓冲区 const int8 = new Int8Array(buffer, 0, 4) //使用类型数组将缓冲区前四个字节设置为Int8类型 const int16 = new Int16Array(buffer, 4, 6)//使用类型数组将缓冲区后12个字节设置为Int16类型 此时的buffer缓冲区的布局如下: 前四个字节是Int8类型的数据 后12个字节是int16类型的数据
-
一个参数: 数字,表示类型化数组的长度.此时构造函数会自动创建大小合适的缓冲区* *
new Int8Array(length); 参数: length: 表示创建的类型化数组的长度,而且大小可以根据长度乘以不同类型数组元素的大小获取 const int8 = new Int8Array(4); console.log(int8.byteLength); //4 int8类型数组每个元素占用一个字节,因此大小是4
-
一个参数: 类型化数组
new Int8Array(int16) 参数: 名为int16的类型化数组 const int8 = new Int8Array(4) const int16 = new Int16Array(int8) console.log(int16.byteLength,int16.length) //8 4 基于参数数组创建了新的缓冲区,元素的个数保持不变
-
一个参数: 数组,类数组对象
const arr = [1, 2, 3, 4, 5] const int8 = new Int8Array(arr) console.log(int8.length) //5 原数组元素和长度都不变
-
-
方法
类型化数组的方法与js数组方法大部分是相通的,但要注意由于类型化数组依赖的缓冲区大小不可以改变,因此可以改变数组长度的方法类型化数组是不能使用的如:
push,shift,pop,unshift,splice
,一些数组特有场景的方法也是不可以使用的:flat,concat,flatMap
,其他方法类型化数组都可以使用除了数组的一些方法,类型化数组还有以下方法:
-
set(TypeArray,[,byteOffset]):从提供的数组或者类型数组中把复制当前类型化数组的当制定位置
set(TypeArray,[,byteOffset]) 参数: TypeArray: 类型化数组 byteOffset: 赋值的起始索引 const arr = [1, 2, 3, 4, 5] const int8 = new Int8Array(arr) const int16 = new Int16Array(7) int16.set(int8,1) console.log(int16) //从int16索引为1的地方开始元素被设置为int8的元素
-
subarray([,start[,end]]):从当前类型化数组中起始索引到终止索引的值复制,并返回一个新的类型化数组
subarray([,start[,end]]) 参数: start: 复制开始索引位置(包含) end: 复制结束索引位置(不包含) const arr = [1, 2, 3, 4, 5] const int8 = new Int8Array(arr) const newInt8 = int8.subarray(1, 4) console.log("newInt8:", newInt8); //newInt8中包含: 2,3,4
-
DataView
和TypeArray不同,要想创建DataView
视图,必须先创建缓冲区,它提供了可以操作缓冲区任意数据类型getter/setter
API,同是TypeArray
都是运行在本地字节序模式的,不能改变,而DataView
默认是大端字节序
,也可以通过提供的getter/setter
的最后一个参数控制字节序为小端字节序
.
字节序: 大端字节序表示最高有效位保存在第一个字节,最低有效位保存在最后一个字节.小端字节序表示最高有效位保存在最后一个字节,最低有效位保存在第一个字节
创建视图:
const buffer = new ArrayBuffer(16)
const view1 = new DataView(buffer) //一个参数: 缓冲区
console.log(view1.byteLength) // 16
cosnt view2 = new DataView(buffer,2) //两个参数: 第二个参数表示字节偏移量
console.log(view2.byteLength) // 14
const view3 = new DataView(buffer,2,8)// 三个参数: 第三个参数表示包含的字节长度
console.log(view3.byteLength) // 8
写入和读取:
DataView想要读写缓冲区的数据时,需要先为其指定数据类型,它支持10种数据类型,在TypeArray
的类型表格中除了(Uint8Clamped),DataView提供了十对原型方法,均以set/get
为前缀,set
用来写入,get
用来读取,每种方法都是set/get + 类型
,以int8
类型为例子:
set方法
const buffer = new ArrayBuffer(16)
const view = new DataView(buffer)
view.setInt8(1,2,false) //set方法有三个参数,第一个参数表示字节偏移量,第二个参数表示设置的值.第三个参数表示设置的字节序,默认时true表示大端序,false为小端序.这里表示将view的索引为1的字节设置为int8类型的2
get方法:
view.getInt8(1,false) //get方法有两个参数表示字节偏移量,第一个参数指明获取哪个元素,第二个参数时boolean值,false为小端序读取,true为大端序读取
.这里将会输出上面设置的2
使用定型数组
通过canvas
和TypeArray
实现上传的图片进行反转:
- 读取上传的图片
- 创建image实例,上个步骤读取的数据转换成URL,并赋值给image实例
- 读取image实例的像素数据
- 反转
- 绘制在canvas上
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'>
<meta http-equiv='X-UA-Compatible' content='IE=edge'>
<title>Page Title</title>
<meta name='viewport' content='width=device-width, initial-scale=1'>
</head>
<body>
<input type="file" />
<canvas id="myCanvas" width="300" height="200"></canvas>
<script>
const oInput = document.querySelector('input')
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
oInput.addEventListener('change', (e) => {
const file = e.target.files[0]
const reader = new FileReader()
reader.onload = (e) => {
const buffer = e.target.result
const blob = new Blob([buffer])
const url = URL.createObjectURL(blob)
const oImg = new Image()
oImg.src = url
oImg.onload = () => {
ctx.drawImage(oImg, 0, 0, canvas.width, canvas.height);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
// 获取图像像素二进制数据 返回一个TypeArray类型的视图
const data = imageData.data;
// 反转颜色
for (let i = 0; i < data.length; i += 4) {
imageData.data[i] = 255 - data[i]; // Red channel inversion
imageData.data[i + 1] = 255 - data[i + 1]; // Green channel inversion
imageData.data[i + 2] = 255 - data[i + 2]; // Blue channel inversion
// data[i + 3] doesn't need modification, it represents the alpha channel.
}
// 将反转后的图像数据绘制到 Canvas 上
ctx.putImageData(imageData, 0, 0);
}
}
reader.readAsArrayBuffer(file);
})
</script>
</body>
</html>
Map
也叫映射ES6新增的特性,js中真正的键/值存储机制
基本API
-
创建
const map = new Map() 参数: 一个可迭代对象,需要包含键值对数组.可迭代对象的每个键值对都会按照迭代顺序插入到新实例中 例如: const map = new Map([["key1",val1],["key2",val2],["key3",val3])
映射的键可以是任意值
-
添加
map.set('键','值') 参数: 第一参数为插入的键,可以是任意值 第二个参数为键对应的值,可以是任意值 返回值: 返回添加后的map,因此可以进行链式操作
-
获取
cosnt key = map.get('键'); 参数: 键名 返回值: map中有键返回对应的值,无值返回undefined
-
删除
-
delete
map.delete("键") 参数: 要删除的键名 返回值: 删除成功,返回true,删除失败返回false
-
clear
map.clear(); 无参数,清空map 返回值: 返回清空后的map
-
-
查找
map.has('键'); 参数: 要查找的键名 返回值: 找到返回true 未找到返回false
-
size
map.size; 该属性返回map中的键值对个数
SameValueZero
在Map中使用SameValueZero检查键的匹配性,类似严格相等,下面我们来回顾一下js中提供的判断相等的三种方式
-
==
宽松相等- 比较时会进行隐式类型转换,但对
NAN,+0,-0
做了特殊处理:NaN != NaN, 0 == -0
均返回true
- 比较时会进行隐式类型转换,但对
-
===
严格相等- 比较时不进行严格相等,其余与宽松相同
-
object.is()
- 不进行类型转换,也不对
NaN
、-0
和+0
进行特殊处理
- 不进行类型转换,也不对
以上三个操作分别与js的四个相等算法对应:
- IsLooselyEqual:
==
- IsStrictlyEqual:
===
- SameValue:
Object.is()
- SameValueZero:其与SameValue区别在于SameValueZero的
+0,-0相等
,这种算法被许多内置运算使用,包括Map
顺序和迭代
在迭代方面与Object的区别在于
- Map中会维护键值对的插入顺序,迭代时可以根据插入顺序迭代
- Map内实现了迭代器,内部实现了
[Symbol.itreator]
,可以直接迭代
同时Map还提供了两个方法:
-
entries():获取迭代器
const map = new Map(); console.log(map.entries() === map[Symbol.iterator])
-
keys(): 获取键的迭代器
-
values(): 获取值的迭代器
Object or Map
内存占用
如果给定固定大小的内存,Map大约可以比Object多存储50%的键值对,因此大量存数据
插入性能
两者的插入操作性能大致相当,不过map在所有浏览器中要稍微快一点,对于两个类型来说,插入速度不会随着键/值对的增加而线性增加,如果代码涉及大量插入操作,那么Map是首选
查找速度
从大型Object和Map中查找键/值的性能差异极小,但如果只包含少量数据,Object会更快一点,在把Object当作数组使用的时候(使用连续整数作为键),浏览器引擎会做相关优化,在内存中使用更高效的布局,因此涉及大量查找操作某些情况下Object是首选
删除性能
大量的删除毫无疑问使用Map,因为在Object中删除属性只能用delete
,但其性能问题比较大,主要因为V8引擎存储对象时有快属性
和慢属性
之分,字符串数字属性属于快属性,存放在线性结构中,常规属性以字典的方式存放属于慢属性,当用delete删除属性的时候,可能就会破坏线性结构吗,使查询性能变慢.因此大量删除操作时,非Map莫属
Set
集合ES新增的Set是一种新集合类型,允许存储任何值,且集合中的元素是唯一的,内部也使用SameValueZero来匹配值(简单说除了+0,-0其他值和Object.js()返回结果是一致的)
基本API
-
创建
const set = new Set() 创建一个空集合,可以接收一个可迭代对象,用于初始化该集合
-
添加
set.add(任意值) 返回添加元素后的集合 set.add(+0) set.add(-0) console.log(set) //Set(1) { 0 }
-
删除
clear
set.clear() 清空集合,无返回值
delete
set.delete(任意值) 删除执行的值,删除成功返回true,否则返回false
-
查找
has
set.has(任意值) 若值存在于集合中,则返回true,否则返回false
-
迭代
entries: 返回其迭代器对象
keys: 返回下标的迭代器对象
values: 返回元素的迭代器对象
forEach: 通过回调函数进行迭代
此外还有新增的weekMap
和weekSet
日常开发应用的比较少,这里暂不介绍.