JavaScript 红宝书笔记 - 第 6 章. 集合引用类型

Object 类型

  • Object 实例的两种显示创建方法

    • ```js
      // new Object(); 构造函数法
      let person = new Object();
      person.name = “Colanns”;
      1
      2
      3
      4
      5
      6

      - ```js
      // 对象字面量表示法
      let person = {
      name: "Colanns" // 这里不加逗号是因为在老浏览器中最后一个属性加上逗号会报错,但是现代浏览器没问题
      };
      @ 表达式上下文
  • TIPS: 可以使用 typeof 检测每个属性是否存在。

  • 点语法与中括号都可以用来访问属性。

第 8 章将会更加深入介绍 Object 类型。

Array 类型

  • 数组的每个槽位可以存储任意类型的数据。
  • 数组是动态大小的。

创建数组的基本方法

  • Array 构造函数,其中 new 可以省略。

    1
    2
    3
    4
    5
    let colors = new Array();
    // 指定初始 length 为 20
    let colors = new Array(20);
    // 指定初始元素
    let colors = new Array("red", "blue", "yellow");
  • 使用数组字面量的方法创建

    1
    2
    3
    let colors = [];
    let colors = ["red", "blue", "yellow"];
    let values = [1,2,]; // 依然是两个元素
  • ES6 新增的用于创建数组的静态方法

    • Array.from() 将类数组结构转换为数组实例

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      // 字符串会被拆分为单字符数组
      console.log(Array.from("Matt")); // ["M", "a", "t", "t"]

      // 可以使用from()将集合和映射转换为一个新数组
      const m = new Map().set(1, 2)
      .set(3, 4);
      const s = new Set().add(1)
      .add(2)
      .add(3)
      .add(4);
      console.log(Array.from(m)); // [[1, 2], [3, 4]]
      console.log(Array.from(s)); // [1, 2, 3, 4]

      // Array.from()对现有数组执行浅复制
      const a1 = [1, 2, 3, 4];
      const a2 = Array.from(a1);

      console.log(a1); // [1, 2, 3, 4]
      alert(a1 === a2); // false

      // 可以使用任何可迭代对象
      const iter = {
      *[Symbol.iterator]() {
      yield 1;
      yield 2;
      yield 3;
      yield 4;
      }
      };
      console.log(Array.from(iter)); // [1, 2, 3, 4]

      // arguments 对象可以被轻松地转换为数组
      function getArgsArray() {
      return Array.from(arguments);
      }
      console.log(getArgsArray(1, 2, 3, 4)); // [1, 2, 3, 4]

      // from()也能转换带有必要属性的自定义对象
      const arrayLikeObject = {
      0: 1,
      1: 2,
      2: 3,
      3: 4,
      length: 4
      };
      console.log(Array.from(arrayLikeObject)); // [1, 2, 3, 4]

      此外,Array.from() 接受第二个可选的映射函数参数,对新数组的值进行映射修改,第三个可选参数可以指定映射函数中 this 的值(在箭头函数中不适用)。

    • Array.of() 将一组参数转换为数组实例

      Array.prototype.slice.call(arguments) 的替代品。

      1
      console.log(Array.of(1, 2, 3, 4)); // [1, 2, 3, 4]

数组空位

  • 使用一串逗号可以创建空位

    1
    2
    3
    const options = [,,,,,]; // 创建包含5 个元素的数组
    console.log(options.length); // 5
    console.log(options); // [,,,,,]
  • ES6 中,这些空位是存在的元素,但是值为 undefined

数组索引

  • 通过修改 length ,可以从数组末尾删除或者添加元素,新元素被 undefined 填充。

    1
    2
    // 一种十分好用的向数组末尾添加元素的方法
    colors[colors.length] = "black";

检测数组

  • value instanceof Array 即可。

    但是可能面临有两个框架 / 两个全局执行上下文造成构造函数不同的情况,所以 ↓

  • Array.isArray() 方法更佳。

迭代器方法

  • 3 个用于检索数组内容的方法

    • a.keys() 返回数组索引的迭代器
    • a.values() 返回数组元素的迭代器
    • a.entries() 返回索引 / 值对的迭代器
  • 可以使用 Array.from() 将上述三个方法返回的内容直接转换为数组实例。

  • 结合解构来拆分键值对

    1
    2
    3
    4
    5
    const a = ["foo", "bar", "baz", "qux"];
    for (const [idx, element] of a.entries()) {
    console.log(idx);
    console.log(element);
    }

复制和填充方法

  • 填充数组方法 fill()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    const zeroes = [0, 0, 0, 0, 0];

    // 用 5 填充整个数组
    zeroes.fill(5);
    // 用 6 填充索引大于等于 3 的元素
    zeroes.fill(6, 3);
    // 用 7 填充索引大于等于 1 且小于 3 的元素
    zeroes.fill(7, 1, 3);
    // 用 8 填充索引大于等于 1 且小于 4 的元素
    // 此处为数组长度加上负值
    zeroes.fill(8, -4, -1);
    // 注意:fill()会静默忽略超出数组边界、零长度及方向相反的索引范围,比如:
    zeroes.fill(1, -10, -6);
  • 批量复制方法 copyWithin()

    copyWithin() 会按照指定范围浅复制数组中的部分内容并插入到指定索引开始的位置,开始索引和结束索引同 fill()

    同时,由于 JS 引擎在插值前会完整复制范围内的值,所以不存在重写的风险。

    1
    2
    3
    4
    5
    6
    let ints;
    let reset = () => ints = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
    reset();

    ints.copyWithin(2, 0, 6);
    console.log(ints); // [0, 1, 0, 1, 2, 3, 4, 5, 8, 9]

转换方法

  • 所有对象都有 toLocaleString()toString()valueOf() 方法。对于数组如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    let colors = ["red", "blue", "green"]; // 创建一个包含3 个字符串的数组
    // 返回数组中每个值等效字符串(调用了toString())拼接成的逗号分割的字符串
    alert(colors.toString()); // red,blue,green
    alert(colors.valueOf()); // red,blue,green
    // 后台也调用了toString()方法
    alert(colors); // red,blue,green
    // 后台会调用每个值的toLocaleString()方法
    alert(colors.toLocaleString());
    // 使用join()方法即可换一个新的字符串分隔符
    alert(colors.join("||")); // red||green||blue

    对于 null 或者 undefined 会在上述方法返回的结果中以空字符串表示。

栈方法

  • push()
  • pop()

队列方法

  • shift() 从列表开头获取数据并删除它

  • push() 同栈一样

  • unshift() 在数组开头添加任意多个值,并返回新的数组长度,可以与 pop() 结合使用。

排序方法

  • reverse() 反向排序,直接将数组反向,顺序不变

  • sort() 按升序(从大到小)重新排列元素,但是会先转换为字符串再比较,所以会出现下面这种诡异的情况。

    1
    2
    3
    let values = [0, 1, 5, 10, 15];
    values.sort();
    alert(values); // 0,1,10,15,5

    为了解决这种问题,sort() 方法可以接受一个比较函数来确定哪个值在前面。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    function compare(value1, value2) {
    if (value1 < value2) {
    return -1; // value1在前面
    } else if (value1 > value2) {
    return 1; // value2在前面
    } else {
    return 0;
    }
    }

    let values = [0, 1, 5, 10, 15];
    // 传入比较函数
    values.sort(compare);
    // 这样就正常起来了
    alert(values); // 0,1,5,10,15
    // 简写是这样的
    values.sort((a, b) => a < b ? 1 : a > b ? -1 : 0);
    // 如果数组元素是数值还能更简单
    function compare(value1, value2) {
    return value2 - value1;
    }
  • 注意 reverse()sort() 都返回调用它们的数组的引用。

操作方法

  • a.concat() 可以在现有数组上构建新数组。

    会创建当前数组副本,将参数添加到末尾(参数数组会被打平),返回一个新构建的数组,原数组保持不变

    • 在参数数组上如果指定特殊符号 Symbol.isConcat- Spreadablefalse 即可阻止打平数组,反之可以强制打平数组。
  • a.slice() 如果结束位置小于开始位置则返回空数组。

  • a.splice() 超绝强大数组方法,可以删除、插入、替换数组中元素。

    • 删除:a.splice(开始位置, 删除数量)
    • 插入:a.splice(开始位置, 0(删除数量), 插入的元素) 在第三个参数之后可以传任意多需要插入的元素作为参数
    • 替换:a.splice(开始位置, 删除数量,插入的元素)

搜索和位置方法

  • 严格相等的搜索方法(===)

    • indexOf()
    • lastIndexOf()
    • includes() (ES7 新增,返回布尔值)
  • 断言函数

    • find()findIndex() 前者返回第一个匹配元素,后者返回第一个匹配元素的索引,找到匹配项后,这两个方法都不再继续搜索。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      const people = [
      {
      name: "Matt",
      age: 27
      },
      {
      name: "Nicholas",
      age: 29
      }
      ];
      alert(people.find((element, index, array) => element.age < 28));
      // {name: "Matt", age: 27}
      alert(people.findIndex((element, index, array) => element.age < 28));
      // 0

      这两个方法也接受第二个可选的参数,用于指定断言函数内部 this 的值。

迭代方法

  • 有 5 个迭代方法

    • 接受两个参数:一个是传入的函数,一个是可选的作为函数运行上下文的作用域对象(影响函数中的 this)。
    • 他们对数组每一项都会运行传入的函数。
    • 传入的函数接受三个参数:数组元素、元素索引和数组本身。
  • every():从数组中搜索符合某个条件的元素。如果对每一项传入的函数都返回 true,则这个方法返回 true。

    1
    2
    3
    let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
    let everyResult = numbers.every((item, index, array) => item > 2);
    alert(everyResult); // false
  • filter():传入的函数返回 true 的项会组成数组之后返回。

    1
    2
    let filterResult = numbers.filter((item, index, array) => item > 2);
    alert(filterResult); // 3,4,5,4,3
  • forEach():没有返回值,相当于遍历函数。

  • map():返回由每次函数调用的结果构成的数组。

    1
    2
    let mapResult = numbers.map((item, index, array) => item * 2);
    alert(mapResult); // 2,4,6,8,10,8,6,4,2
  • some():从数组中搜索符合某个条件的元素。如果有一项传入的函数返回 true,则这个方法返回 true。

    1
    2
    let someResult = numbers.some((item, index, array) => item > 2);
    alert(someResult); // true

归并方法

  • reduce()reduceRight()

    • 两个方法都会迭代数组的所有项,前者从开始到结束,后者从结束到开始。
    • 传给二者的函数接受 4 个参数:上一个归并值、当前项、当前索引和数组本身,这个函数的返回值会作为下一次调用它时的第一个参数。
    • 如果没有给这两个方法传入第二个参数作为归并起点值,则第一次迭代会从第二项开始,此时第一个参数会是第一项。
    1
    2
    3
    // 使用reduce()累加数组的值
    let values = [1, 2, 3, 4, 5];
    let sum = values.reduce((prev, cur, index, array) => prev + cur);

定型数组

即 Typed Array, 在 JS 里指的就是一种特殊的包含数值类型的数组,是为了解决 WebGL 性能问题而诞生的。

ArrayBuffer

  • 是一块预分配内存,是所有定型数组及视图引用的基本单位。

  • SharedArrayBufferArrayBuffer 的变体,可以无需复制即可在执行上下文中传递它。(具体请参考 27 章)

  • // 在内存中分配特定数量的字节空间,这里是16字节
    const buf = new ArrayBuffer(16);
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13

    - `ArrayBuffer` 创建后就不能够再调整大小,不过可以使用 `slice()` 复制到新的实例中。

    - 类似于 C++ 的 `malloc()`,但是又不太一样。![image-20220322215304808](https://colanns-picgo.oss-cn-beijing.aliyuncs.com/picgo/image-20220322215304808.png)

    - 读写需要通过视图。

    ### `DataView`

    - 专为文件 I/O 和网络 I/O 设计,其 API 支持对缓冲数据的高度控制,但相比于其他类型的视图性能也差一些。

    - ```js
    // 具体使用请参考 P156。
  • ```js
    // 字节序问题请参考 P157。

    1
    2
    3
    4
    5
    6
    7

    ### 定型数组

    - 定型数组提供了适用面更广的 API 和更高的性能。设计定型数组的目的就是提高与 WebGL 等原生库交换二进制数据的效率。

    - ```js
    // 具体使用请参考 P160。

Map

基本 API

  • 基本方法创建空映射

    1
    const m = new Map();
  • 创建的同时初始化实例,需要传入一个可迭代对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    // 使用嵌套数组初始化映射
    const m1 = new Map([
    ["key1", "val1"],
    ["key2", "val2"],
    ["key3", "val3"]
    ]);
    alert(m1.size); // 3

    // 使用自定义迭代器初始化映射
    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.size); // 1
    alert(m3.has(undefined)); // true
    alert(m3.get(undefined)); // undefined
  • set() 添加键值对,也可以覆盖键值对

  • get() / has() 查询键值对和查询存在性

  • size 获取数量

  • delete() 删除某个值

  • clear() 清空

  • SameValueZero 比较方法,请参考 P166。

顺序与迭代

  • 迭代器:m.entries() 或者 m[Symbol.iterator] 来获得。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    const m = new Map([
    ["key1", "val1"],
    ["key2", "val2"],
    ["key3", "val3"]
    ]);
    alert(m.entries === m[Symbol.iterator]); // true
    for (let pair of m.entries()) {
    alert(pair);
    }
    // [key1,val1]
    // [key2,val2]
    // [key3,val3]
    for (let pair of m[Symbol.iterator]()) {
    alert(pair);
    }
    // [key1,val1]
    // [key2,val2]
    // [key3,val3]
  • 可以直接对映射实例使用扩展操作,把映射转换为数组。

    1
    2
    console.log([...m]);
    // [[key1,val1],[key2,val2],[key3,val3]]
  • 使用回调的方式。

    1
    2
    3
    4
    m.forEach((val, key) => alert(`${key} -> ${val}`));
    // key1 -> val1
    // key2 -> val2
    // key3 -> val3
  • keys()values() 依然可用。

  • 迭代器遍历的时候可以修改键和值,但是映射内部的引用无法修改,可以修改键或值的对象内部的属性。

选择 Object 还是 Map

  • Map 内存占用和插入性能好些,查找速度和 Object 差不多,删除性能要好很多。

WeakMap

  • 弱映射中的键只能是 Object 或者 继承自 Object 的类型,值则无限制。
  • 初始化与 Map 相同,原始值可以包装为对象再用作键。
  • weak 表示键不属于正式的引用,不会阻止垃圾回收。而对于值,只要键存在就会被引用,而键一旦被回收,如果这个值只被键的引用的话,也会被回收。
  • 由于键值随时可能被销毁,所以不提供迭代,也没有 clear()
  • 可以用来实现一个十分阴间的闭包私有变量模式。

Set

基本 API

  • 就很加强版的 Map

  • 使用数组初始化集合

    1
    2
    // 使用数组初始化集合
    const s1 = new Set(["val1", "val2", "val3"]);
  • add() / has() / size / delete() / clear()

  • 也使用 SameValueZero 来检测值的匹配性。

顺序与迭代

  • 默认迭代器 values() 别名 keys() 或者 Symbol.iterator 属性。

  • 可以直接扩展转换为数组。

    1
    2
    const s = new Set(["val1", "val2", "val3"]);
    console.log([...s]); // ["val1", "val2", "val3"]
  • entries() 可以返回一个迭代器,按照插入顺序产生包含两个元素的数组。

  • 不使用迭代器,使用回调方式迭代键值对

    1
    2
    const s = new Set(["val1", "val2", "val3"]);
    s.forEach((val, dupVal) => alert(`${val} -> ${dupVal}`));
  • 修改集合中值的属性不会影响其作为集合值的身份

定义正式集合操作

  • 可以在子类实现静态方法,实现更多操作,比如返回并交叉集,但是注意不要对原来的 Set 进行修改。

WeakSet

  • 基本同 WeakMap
  • 不可迭代

迭代与扩展操作

  • 四种原生集合类型定义了默认迭代器

    • Array
    • 所有定型数组
    • Map
    • Set
  • 则上述所有类型都支持顺序迭代,可以传入 for-of 循环。

  • 上述所有类型都兼容扩展操作符 ... ,意味着十分简单就可以对对象执行浅复制。

    1
    2
    3
    4
    5
    6
    let arr1 = [1, 2, 3];
    let arr2 = [...arr1];
    console.log(arr1); // [1, 2, 3]
    console.log(arr2); // [1, 2, 3]
    console.log(arr1 === arr2); // false

  • 当然对于期待可迭代对象的构造函数也可以直接传入可迭代对象。

    1
    2
    let map1 = new Map([[1, 2], [3, 4]]);
    let map2 = new Map(map1);
  • 浅复制:只会复制对象引用。

    1
    2
    3
    4
    let arr1 = [{}];
    let arr2 = [...arr1];
    arr1[0].foo = 'bar';
    console.log(arr2[0]); // { foo: 'bar' }