0%

学习笔记 2020 10 12

学习笔记 2020-10-12

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

集合引用类型

Array

  1. 归并方法
    • reduce

    • reduceRight

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

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

定型数组

人们期望开发一套 JavaScript API 来利用 3D 图形 APIGPU 加速,以便渲染运行复杂的 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

      每种类型都有 getset 方法,使用 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)

    写一个返回数字 ab 中较小的那个数字的函数 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),返回 xn 次方。换句话说,将 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 年的笔记。