学习笔记 2020-10-12
JavaScript 高级程序设计(第4版) 阅读记录
集合引用类型
Array
归并方法
reduce
reduceRight
两个方法都会迭代数组的所有项,构建一个最终返回值。区别是
reduce
从头遍历到尾,reduceRight
从尾遍历到头。两个方法都接收两个参数,一个归并函数和归并起点的初始值。归并函数接收四个参数,上一个归并值,当前项,当前项的索引和数组本身。函数的返回值会作为下一次遍历的归并值即第一个参数。如果没有传入第二个参数,那么第一次迭代默认从数组第二项开始。
定型数组
人们期望开发一套 JavaScript API
来利用 3D
图形 API
和 GPU
加速,以便渲染运行复杂的 3D
应用程序。基于 OpenGL ES 2.0
规范开发了一个 API
名为 WebGL
。早期版本中,它与原生数组之间格式不匹配,出现了性能问题。在此基础上, Mozilla
实现了 CanvasFloatArray
,最终变成 Float32Array
。
ArrayBuffer
ArrayBuffer
是所有定型数组及视图引用的基本单位,是一个普通的JavaScript
构造函数,可用于在内存中分配特定数量的字节空间。const buf = new ArrayBuffer(16); console.log(buf.byteLength); // 16
ArrayBuffeer
创建后无法改变大小,但可以利用slice
方法复制部分成为一个新的实例。查看
ArrayBuffer
的原型对象:console.log(ArrayBuffer.prototype); { byteLength: (...) constructor: ƒ ArrayBuffer() slice: ƒ slice() Symbol(Symbol.toStringTag): "ArrayBuffer" get byteLength: ƒ byteLength() __proto__: Object }
可以看出,
ArrayBuffer
其实和数组没有特别大的关系,只是原型对象上挂载了slice
方法以及byteLength
属性,它的原型指向了Object.prototype
。ArrayBuffer
分配失败时会抛出错误。- 分配的内存不能超过
Number.MAX_SAFE_INTEGER
即 (2^53 - 1) 字节。 - 声明
ArrayBuffer
会将所有二进制位初始化为0
。 - 分配的堆内存可以被垃圾回收,不需要手动释放。
DataView
允许读写
ArrayBuffer
的视图。专为文件I/O
和网络I/O
设计,支持对缓冲数据的高度控制,但性能较差,对缓冲内容没有任何预设,也不能迭代。查看
DataView
的构造函数定义:DataView(buffer: ArrayBuffer, byteOffset?: number, byteLength?: number): DataView
可以看出,第一个参数是必填的
ArrayBuffer
,第二个参数为偏移量,第三个参数为字节长度。若不指定,偏移量默认从0
开始,字节长度默认为剩余缓冲。const buf = new ArrayBuffer(16); const fullDataView = new DataView(buf); console.log(fullDataView.byteOffset); // 0 console.log(fullDataView.byteLength); // 16 console.log(fullDataView.buffer === buf); // true const firstHalfDataView = new DataView(buf, 0, 8); console.log(firstHalfDataView.byteOffset); // 0 console.log(firstHalfDataView.byteLength); // 8 console.log(firstHalfDataView.buffer === buf); // true const secondHalfDataView = new DataView(buf, 8); console.log(secondHalfDataView.byteOffset); // 8 console.log(secondHalfDataView.byteLength); // 8 console.log(secondHalfDataView.buffer === buf); // true
ElementType
用于实现
JavaScript
中的Number
类型到缓冲内二进制格式的转换。有以下几种类型:Int8
Uint8
Int16
Uint16
Int32
Uint32
Float32
Float64
每种类型都有
get
和set
方法,使用byteOffset
定位位置进行读写。类型间可以相互转换。const buf = new ArrayBuffer(2); // 分配了两个字节 const view = new DataView(buf); // 定义一个视图 console.log(view.getInt8(0)); // 0 console.log(view.getInt8(1)); // 0 console.log(view.getInt16(0)); // 0 view.setUint8(0, 255); console.log(view.getInt16(0)); // -256 console.log(view.getInt8(1)); // 0 view.setUint8(1, 0xff); console.log(view.getInt16(0)); // -1 console.log(view.getInt8(1)); // -1
字节序
字节序指计算系统维护的一种字节顺序的约定。
DataView
支持两种约定,大端字节序和小端字节序。大端字节序指最高有效位保存在第一个字节,最低有效位保存在最后一个字节,也称网络字节序。
小端字节序则相反。
DataView
不遵守JavaScript
运行时所在系统的原生字节序。它默认为大端字节序,也可以接收参数修改为小端字节序。// 填充缓冲,让第一位和最后一位都是 1 view.setUint8(0, 0x80); // 设置最左边的位等于 1 view.setUint8(1, 0x01); // 设置最右边的位等于 1 // 缓冲内容(为方便阅读,人为加了空格) // 0x8 0x0 0x0 0x1 // 1000 0000 0000 0001 // 按大端字节序读取 Uint16 // 0x80 是高字节,0x01 是低字节 // 0x8001 = 2^15 + 2^0 = 32768 + 1 = 32769 console.log(view.getUint16(0)); // 32769 // 按小端字节序读取 Uint16 // 0x01 是高字节,0x80 是低字节 // 0x0180 = 2^8 + 2^7 = 256 + 128 = 384 console.log(view.getUint16(0, true)); // 384 // 按大端字节序写入 Uint16 view.setUint16(0, 0x0004); // 缓冲内容(为方便阅读,人为加了空格) // 0x0 0x0 0x0 0x4 // 0000 0000 0000 0100 console.log(view.getUint8(0)); // 0 console.log(view.getUint8(1)); // 4 // 按小端字节序写入 Uint16 view.setUint16(0, 0x0002, true); // 缓冲内容(为方便阅读,人为加了空格) // 0x0 0x2 0x0 0x0 // 0000 0010 0000 0000 console.log(view.getUint8(0)); // 2 console.log(view.getUint8(1)); // 0
边界情形
DataView
的操作需要在缓冲区范围内,否则会抛出RangeError
。写缓冲的时候会将写入值转为适当类型,若是无法转换则会抛出错误。
定型数组
定型数组是另一种形式的
ArrayBuffer
视图。它特定于一种ElementType
且遵循系统原生的字节序。目的在于提高与WebGL
等原生库交换二进制数据的效率。创建定型数组的方式包括读取已有的缓冲、使用自有缓冲、填充可迭代结构,以及填充基于任意类型的定型数组。另外,通过
<ElementType>.from()
和<ElementType>.of()
也可以创建定型数组// 创建一个 12 字节的缓冲 const buf = new ArrayBuffer(12); // 创建一个引用该缓冲的 Int32Array const ints = new Int32Array(buf); // 这个定型数组知道自己的每个元素需要 4 字节 // 因此长度为 3 console.log(ints.length); // 3 // 创建一个长度为 6 的 Int32Array const ints2 = new Int32Array(6); // 每个数值使用 4 字节,因此 ArrayBuffer 是 24 字节 console.log(ints2.length); // 6 // 类似 DataView,定型数组也有一个指向关联缓冲的引用 console.log(ints2.buffer.byteLength); // 24 // 创建一个包含[2, 4, 6, 8]的 Int32Array const ints3 = new Int32Array([2, 4, 6, 8]); console.log(ints3.length); // 4 console.log(ints3.buffer.byteLength); // 16 console.log(ints3[2]); // 6 // 通过复制 ints3 的值创建一个 Int16Array const ints4 = new Int16Array(ints3); // 这个新类型数组会分配自己的缓冲 // 对应索引的每个值会相应地转换为新格式 console.log(ints4.length); // 4 console.log(ints4.buffer.byteLength); // 8 console.log(ints4[2]); // 6 // 基于普通数组来创建一个 Int16Array const ints5 = Int16Array.from([3, 5, 7, 9]); console.log(ints5.length); // 4 console.log(ints5.buffer.byteLength); // 8 console.log(ints5[2]); // 7 // 基于传入的参数创建一个 Float32Array const floats = Float32Array.of(3.14, 2.718, 1.618); console.log(floats.length); // 3 console.log(floats.buffer.byteLength); // 12 console.log(floats[2]); // 1.6180000305175781
查看定型数组的构造函数原型对象:
console.log(Int16Array.prototype); // BYTES_PER_ELEMENT: 2 返回该类型数组中每个元素的大小 // buffer: (...) // byteLength: (...) // byteOffset: (...) // constructor: ƒ Int16Array() // length: (...) // Symbol(Symbol.toStringTag): (...) // __proto__: Object console.log(Int32Array.prototype); // BYTES_PER_ELEMENT: 4 // buffer: (...) // byteLength: (...) // byteOffset: (...) // constructor: ƒ Int32Array() // length: (...) // Symbol(Symbol.toStringTag): (...) // __proto__: Object
如果在初始化时没有提供值,那么关联的缓冲会以
0
填充。定型数组行为
定型数组的原型对象上绑定了非常多的数组方法,继承自一个叫
TypedArray
的类型。定型数组缺少修改大小的几个数组方法:
concat
pop
push
shift
splice
unshift
但提供了两个新方法,可以用于复制数据:
set
subarray
const container = new Int16Array(8); container.set(Int8Array.of(1, 2, 3, 4)); console.log(container); // Int16Array(8) [1, 2, 3, 4, 0, 0, 0, 0] container.set([5, 6, 7, 8], 4); console.log(container); // Int16Array(8) [1, 2, 3, 4, 5, 6, 7, 8]
看一下
set
的函数定义:set(array: ArrayLike<number>, offset?: number): void
偏移量溢出时会抛出错误。
subarray
会从原定型数组中复制出一个新定型数组。const source = Int16Array.of(2, 4, 6, 7); const fullCopy = source.subarray(); console.log(fullCopy); // Int16Array(4) [2, 4, 6, 7] const halfCopy = source.subarray(2); console.log(halfCopy); // Int16Array(2) [6, 7] const partialCopy = source.subarray(1, 3); console.log(partialCopy); // Int16Array(2) [4, 6]
看一下
subarray
的函数定义subarray(begin?: number, end?: number): Int16Array
手写定型数组拼接函数:
function typedArrayConcat(typedArrayConstructor, ...typedArrays) { const numElements = typedArrays.reduce((x, y) => (x.length || x) + y.length); const resultArray = new typedArrayConstructor(numElements); let currentOffset = 0; typedArrays.map(x => { resultArray.set(x, currentOffset); currentOffset += x.length; }); return resultArray; } const concatArray = typedArrayConcat( Int32Array, Int8Array.of(1, 2, 3), Int16Array.of(4, 5, 6), Float32Array.of(7, 8, 9) ); console.log(concatArray); // Int32Array(9) [1, 2, 3, 4, 5, 6, 7, 8, 9]
下溢和上溢
// 长度为 2 的有符号整数数组 // 每个索引保存一个二补数形式的有符号整数 // 范围是-128(-1 * 2^7)~127(2^7 - 1) const ints = new Int8Array(2); // 长度为 2 的无符号整数数组 // 每个索引保存一个无符号整数 // 范围是 0~255(2^7 - 1) const unsignedInts = new Uint8Array(2); // 上溢的位不会影响相邻索引 // 索引只取最低有效位上的 8 位 unsignedInts[1] = 256; // 0x100 console.log(unsignedInts); // [0, 0] unsignedInts[1] = 511; // 0x1FF console.log(unsignedInts); // [0, 255] // 下溢的位会被转换为其无符号的等价值 // 0xFF 是以二补数形式表示的-1(截取到 8 位), // 但 255 是一个无符号整数 unsignedInts[1] = -1; // 0xFF (truncated to 8 bits) console.log(unsignedInts); // [0, 255] // 上溢自动变成二补数形式 // 0x80 是无符号整数的 128,是二补数形式的-128 ints[1] = 128; // 0x80 console.log(ints); // [0, -128] // 下溢自动变成二补数形式 // 0xFF 是无符号整数的 255,是二补数形式的-1 ints[1] = 255; // 0xFF console.log(ints); // [0, -1]
还存在一种数组类型
Unit8ClampedArray
, 不允许任何方向溢出。超出的值会被向下舍入为255
, 小于的值会被向上舍入为0
。
Map
ES6
中新增的集合类型,真正意义上的键值存储机制。
基本
API
const m = new Map();
初始化时可以传入一个可迭代对象,需要包含键值对数组。
const m1 = new Map([ ['key1', 'val1'], ['key2', 'val2'], ['key3', 'val3'] ]); console.log(m1.size); // 3 console.log(m1); // Map(3) {"key1" => "val1", "key2" => "val2", "key3" => "val3"} // 使用自定义迭代器初始化映射 const m2 = new Map({ [Symbol.iterator]: function*() { yield ["key1", "val1"]; yield ["key2", "val2"]; yield ["key3", "val3"]; } }); alert(m2.size); // 3 // 映射期待的键/值对,无论是否提供 const m3 = new Map([[]]); alert(m3.has(undefined)); // true alert(m3.get(undefined)); // undefined
set()
初始化后添加键值对的方法。
get()
获取值。
has()
查询是否存在值。
size
获取映射中的键值对数量。
delete
删除某个键值对。
clear
清空该映射的所有键值对。
现代 JavaScript 教程
任务
是否需要 else ?
如果参数
age
大于18
,那么下面的函数将返回true
。否则它将会要求进行确认,并返回确认结果:
function checkAge(age) { if (age > 18) { return true; } else { // ... return confirm('Did parents allow you?'); } }
如果
else
被删除,函数的工作方式会不同吗?function checkAge(age) { if (age > 18) { return true; } // ... return confirm('Did parents allow you?'); }
工作方式相同,因为
return
执行后函数就已经结束。此处的else
并没有意义。使用 ? 或 || 重写函数
如果参数
age
大于18
,那么下面的函数返回true
。否则它将会要求进行确认,并返回确认结果:
function checkAge(age) { if (age > 18) { return true; } else { return confirm('Do you have your parents permission to access this page?'); } }
重写这个函数并保证效果相同,不使用
if
,且只需一行代码。编写
checkAge
的两个变体:function checkAge(age) { return age > 18 ? true : confirm('Do you have your parents permission to access this page?'); }
function checkAge(age) { return age > 18 || confirm('Do you have your parents permission to access this page?'); }
函数 min(a, b)
写一个返回数字
a
和b
中较小的那个数字的函数min(a,b)
。例如:
min(2, 5) == 2 min(3, -1) == -1 min(1, 1) == 1
function min(a, b) { return a > b ? b : a; }
函数 pow(x, n)
写一个函数
pow(x,n)
,返回x
的n
次方。换句话说,将x
与自身相乘n
次,返回最终结果。pow(3, 2) = 3 * 3 = 9 pow(3, 3) = 3 * 3 * 3 = 27 pow(1, 100) = 1 * 1 * ...*1 = 1
function pow(x, n) { if (n === 1) { return x; } return x * pow(x, n - 1); }
TODO-LIST
- 想尝试一下
hugo
作为博客。 - 整理 18 年的笔记。