0%

学习笔记 2020 10 12

学习笔记 2020-10-12

JavaScript 高级程序设计(第4版) 阅读记录

集合引用类型

Array

  1. 归并方法
    • reduce

    • reduceRight

      两个方法都会迭代数组的所有项,构建一个最终返回值。区别是 reduce 从头遍历到尾, reduceRight 从尾遍历到头。

      两个方法都接收两个参数,一个归并函数和归并起点的初始值。归并函数接收四个参数,上一个归并值,当前项,当前项的索引和数组本身。函数的返回值会作为下一次遍历的归并值即第一个参数。如果没有传入第二个参数,那么第一次迭代默认从数组第二项开始。

定型数组

人们期望开发一套 JavaScript API 来利用 3D 图形 API 和 GPU 加速,以便渲染运行复杂的 3D 应用程序。基于 OpenGL ES 2.0 规范开发了一个 API 名为 WebGL 。早期版本中,它与原生数组之间格式不匹配,出现了性能问题。在此基础上, Mozilla 实现了 CanvasFloatArray ,最终变成 Float32Array 。

  1. 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 。
    • 分配的堆内存可以被垃圾回收,不需要手动释放。
  2. 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 。

      写缓冲的时候会将写入值转为适当类型,若是无法转换则会抛出错误。

  3. 定型数组

    定型数组是另一种形式的 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 中新增的集合类型,真正意义上的键值存储机制。

  1. 基本 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 教程

任务

  1. 是否需要 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 并没有意义。

  2. 使用 ? 或 || 重写函数

    如果参数 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 的两个变体:

    1. 使用问号运算符 ?
    2. 使用或运算符 ||
    • 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?');
      }
  3. 函数 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;
    }
  4. 函数 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 年的笔记。