《JS高级程序设计》第6章 集合引用类型

97 阅读12分钟

前言

  1. 显式地创建Object的实例有两种方式:使用new操作符和Object构造函数、对象字面量(object literal)表示法。
  2. 在使用对象字面量表示法定义对象时,并不会实际调用Object构造函数。
  3. 在使用中括号时,要在括号内使用属性名的字符串形式。使用中括号的主要优势就是可以通过变量访问属性。

一、Array

  1. 数组中每个槽位可以存储任意类型的数据。
  2. 创建数组的方法有:使用Array构造函数、数组字面量(array literal)表示法、以及两个静态方法from()of()from()用于将类数组结构转换为数组实例,而of()用于将一组参数转换为数组实例。
let colors = new Array(3); // 创建一个包含 3 个元素的数组
let names = new Array("Greg"); // 创建一个只包含一个元素,即字符串"Greg"的数组

// 使用 Array 构造函数时,可以省略 new 操作符
let colors = Array(3); // 创建一个包含 3 个元素的数组
let names = Array("Greg"); // 创建一个只包含一个元素,即字符串"Greg"的数组
  1. 在使用数组字面量表示法创建数组不会调用Array构造函数。
  2. Array.from()的第一个参数是一个类数组对象,即任何可迭代的结构,或者有一个length属性和可索引元素的结构。还接收第二个可选的映射函数参数。接收第三个可选参数用于指定映射函数中this的值。但这个重写的this值在箭头函数中不适用。
const a1 = [1, 2, 3, 4];
const a2 = Array.from(a1, x => x ** 2);
const a3 = Array.from(a1, function (x) { return x ** this.exponent }, { exponent: 2 }); 
console.log(a2); // [1, 4, 9, 16]
console.log(a3); // [1, 4, 9, 16]
  1. 使用数组字面量初始化数组时,可以使用一串逗号来创建空位。ES6将空位当成存在的元素,只不过值为undefined
  2. ES6 之前的方法则会忽略这个空位,但具体的行为也会因方法而异:
const options = [1,,,,5];
// map() 会跳过空位置
console.log(options.map(() => 6)); // [6, undefined, undefined, undefined, 6]
// join() 视空位置为空字符串 
console.log(options.join('-')); // "1----5"
  1. 由于行为不一致和存在性能隐患,因此实践中要避免使用数组空位。如果确实需要空位,可以显式地用undefined值代替。
  2. 数组length属性的独特之处在于,它不是只读的。通过修改length属性,可以从数组末尾删除或添加元素。
  3. 使用instanceof的问题是假定只有一个全局执行上下文,如果网页里有多个框架,则可能涉及两个不同的全局执行上下文,因此就会有两个不同版本的Array构造函数。Array.isArray()方法目的就是确定一个值是否为数组,而不用管它是在哪个全局执行上下文中创建的。
  4. Array 的原型上暴露了三个用于检索数组内容的方法:keys()values()entries()keys()返回数组索引的迭代器,values()返回数组元素的迭代器,而entries()返回索引/值对的迭代器。
  5. ES6 新增了两个方法:批量复制方法copyWithin(),以及填充数组方法fill()。两个方法都静默忽略超出数组边界、零长度及方向相反的索引范围。
const zeroes = [0, 0, 0, 0, 0];

// 用 5 填充整个数组
zeroes.fill(5);
console.log(zeroes); 
// [5, 5, 5, 5, 5]
 
zeroes.fill(0); // 重置
// 用 6 填充索引大于等于 3 的元素 
zeroes.fill(6, 3);
console.log(zeroes); // [0, 0, 0, 6, 6] 

zeroes.fill(0); // 重置
// 用7填充索引大于等于1且小于3的元素 
zeroes.fill(7, 1, 3); 
console.log(zeroes); // [0, 7, 7, 0, 0]; 

zeroes.fill(0); // 重置
// 用8填充索引大于等于1且小于4的元素
// (-4 + zeroes.length = 1)
// (-1 + zeroes.length = 4) 
zeroes.fill(8, -4, -1); 
console.log(zeroes); // [0, 8, 8, 8, 0];
  1. copyWithin()会按照指定范围浅复制数组中的部分内容,然后将它们插入到指定索引开始的位置。
ints = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

// 从 ints 中复制索引 0 开始的内容,插入到索引 5 开始的位置
// 在源索引或目标索引到达数组边界时停止
ints.copyWithin(5);
console.log(ints); // [0, 1, 2, 3, 4, 0, 1, 2, 3, 4]

// 从 ints 中复制索引 5 开始的内容,插入到索引 0 开始的位置 ints.copyWithin(0, 5);
console.log(ints); // [5, 6, 7, 8, 9, 5, 6, 7, 8, 9]

// 从 ints 中复制索引 0 开始到索引 3 结束的内容
// 插入到索引 4 开始的位置
ints.copyWithin(4, 0, 3);
alert(ints); // [0, 1, 2, 3, 0, 1, 2, 7, 8, 9]

// 支持负索引值,与fill()相对于数组末尾计算正向索引的过程是一样的 
// -4=6 -7=3 -3=7
ints.copyWithin(-4, -7, -3);
alert(ints); // [0, 1, 2, 3, 4, 5, 3, 4, 5, 6]
  1. 如果数组中某一项是nullundefined,则在join()toLocaleString()toString()valueOf()返回的结果中会以空字符串表示。

  2. push()方法接收任意数量的参数,并将它们添加到数组末尾,返回数组的最新长度。pop()方法则 用于删除数组的最后一项,同时减少数组的length值,返回被删除的项。

  3. shift()会删除数组的第一项并返回它,然后数组长度减 1。

  4. reverse()方法就是将数组元素反向排列。

  5. sort()会按照升序重新排列数组元素,即最小的值在前面,最大的值在后面。sort()会在每一项上调用String()转型函数,然后比较字符串来决定顺序。即使数组的元素都是数值, 也会先把数组转换为字符串再比较、排序。

  6. reverse()sort()都返回调用它们的数组的引用。

  7. slice()方法可以接收一个或两个参数:返回元素的开始索引和结束索引。参数有负值,那么就以数值长度加上这个负值的结果确定位置。如果结 束位置小于开始位置,则返回空数组。

  8. splice()的主要目的是在数组中间插入元素,返回从数组中被删除的元素。具体有三种方式:

    1. 删除。要传 2 个参数:要删除的第一个元素的位置和要删除的元素数量。
    2. 插入。要传 3 个参数:开始位置、0(要删除的元素数量)和要插入的任意多元素。
    3. 替换。要传 3 个参数:开始位置、要删除元素的数量和要插入的任意多个元素。
  9. 搜索数组的方法有两类:按严格相等搜索和按断言函数搜索。

  10. 3 个严格相等的搜索方法:indexOf()lastIndexOf()includes()

  11. find()findIndex()方法使用了断言函数。这两个方法都从数组的最小索引开始,find()返回 第一个匹配的元素,findIndex()返回第一个匹配元素的索引。找到匹配项后,这两个方法都不再继续搜索。

  12. 数组有 5 个迭代方法,方法接收两个参数:以每一项为参数运行的函数,以及可选的作为函数运行上下文的作用域对象(影响函数中this的值)。传给每个方法的函数接收 3 个参数:数组元素、元素索引和数组本身。

    1. every():对数组每一项都运行传入的函数,如果对每一项函数都返回true,则这个方法返回true
    2. some():对数组每一项都运行传入的函数,如果有一项函数返回true,则这个方法返回true
    3. filter():对数组每一项都运行传入的函数,函数返回true的项会组成数组之后返回。
    4. forEach():对数组每一项都运行传入的函数,没有返回值。
    5. map():对数组每一项都运行传入的函数,返回由每次函数调用的结果构成的数组。
let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
let everyResult = numbers.every((item, index, array) => item > 2);
alert(everyResult);  // false
let someResult = numbers.some((item, index, array) => item > 2);
alert(someResult);   // true
  1. 数组提供了两个归并方法:reduce()reduceRight()reduce()方法从数组第一项开始遍历到最后一项。 而reduceRight()从最后一项开始遍历至第一项。
let values = [1, 2, 3, 4, 5];
// 上一个归并值、当前项、当前项的索引和数组本身
let sum = values.reduce((prev, cur, index, array) => prev + cur);
alert(sum);  // 15

二、定型数组

  1. 定型数组(typed array)的目的是提升向原生库传输数据的效率,是一种特殊的包含数值类型的数组。

  2. 定型数组包含一套不同的引用类型,用于管理数值在内存中的类型。

  3. Float32Array实际上是一种“视图”,可以允许 JavaScript 运行时访问一块名为ArrayBuffer的预分配内存。

  4. ArrayBuffer是所有定型数组及视图引用的基本单位。

  5. ArrayBuffer()是一个普通的 JavaScript 构造函数,可用于在内存中分配特定数量的字节空间。

  6. ArrayBuffer一经创建就不能再调整大小。可以使用slice()复制其全部或部分到一个新实例中。

  7. ArrayBuffer某种程度上类似于 C++ 的malloc(),但也有几个明显的区别:

    1. malloc()在分配失败时会返回一个null指针。ArrayBuffer在分配失败时会抛出错误。
    2. malloc()可以利用虚拟内存,因此最大可分配尺寸只受可寻址系统内存限制。ArrayBuffer分配的内存不能超过 Number.MAX_SAFE_INTEGER(2^53-1)字节。
    3. malloc()调用成功不会初始化实际的地址。声明ArrayBuffer则会将所有二进制位初始化为 0。
    4. 通过malloc()分配的堆内存除非调用free()或程序退出,否则系统不能再使用。而通过声明ArrayBuffer分配的堆内存可以被当成垃圾回收,不用手动释放。
  8. 视图有不同的类型,但引用的都是ArrayBuffer中存储的二进制数据。

  9. 定型数组的区别于DataView的地方是特定于一种ElementType且遵循系统原生的字节序。

  10. 定型数组的构造函数和实例都有一个BYTES_PER_ELEMENT属性,返回该类型数组中每个元素的大小。

  11. 定型数组同样使用数组缓冲来存储数据,而数组缓冲无法调整大小。因此,concat()pop()push()shift()splice()unshift()等合并、复制和修改数组的方法不适用于定型数组。

三、Map

  1. Map是一种新的集合类型,为这门语言带来了真正的键/值存储机制。

  2. 初始化实例可以给Map构造函数传入一个可迭代对象,需要包含键/值对数组。

  3. 初始化之后,可以使用set()方法再添加键/值对。另外,可以使用get()has()进行查询,可以通过size属性获取映射中的键/值对的数量,还可以使用delete()clear()删除值。

  4. Object只能使用数值、字符串或符号作为键不同,Map可以使用任何 JavaScript 数据类型作为键。

  5. Object类型的一个主要差异是,Map实例会维护键值对的插入顺序,因此可以根据插入顺序执行迭代操作。

  6. entries()是默认迭代器,Symbol.iterator属性引用entries()

  7. 键和值在迭代器遍历时是可以修改的,但映射内部的引用则无法修改。

  8. ObjectMap的优劣对比:

    1. 内存占用:给定固定大小的内存,Map大约可以比Object多存储 50%的键/值对。
    2. 插入性能:Map的性能更佳。
    3. 查找速度:Object更好一些。
    4. 删除性能:Map的性能更佳。
  9. WeakMap的 API 是Map的子集,不可迭代键。

  10. “Weak”指的是 JavaScript 垃圾回收程序对待“弱映射”中键的方式。键不属于正式的引用, 不会阻止垃圾回收。

const wm = new WeakMap();
wm.set({}, "val"); // 因为没有指向这个对象的其他引用,当这行代码执行完成后,这个对象键就会被当作垃圾回收
  1. 弱映射中的键只能是Object或者继承自Object的类型,尝试使用非对象设置键会抛出 TypeError。值的类型没有限制。
  2. WeakMap实例之所以限制只能用对象作为键,是为了保证只有通过键对象的引用才能取得值。如果允许原始值,那就没办法区分初始化时使用的字符串字面量和初始化之后使用的一个相等的字符串了。
  3. 使用弱映射的场景:私有变量、DOM 节点元数据。
const m = new Map();
const loginButton = document.querySelector('#login');
// 给这个节点关联一些元数据 
m.set(loginButton, {disabled: true});
// 对应的 DOM 节点仍然会逗留在内存

const wm = new WeakMap();
const loginButton = document.querySelector('#login');
// 给这个节点关联一些元数据 
wm.set(loginButton, {disabled: true});
// 那么当节点从 DOM 树中被删除后,垃圾回收程序就可以立即释放其内存

四、Set

  1. 初始化实例可以给Set构造函数传入一个可迭代对象,其中需要包含插入到新集合实例中的元素。使用add()增加值,使用has()查询,通过size取得元素数量,以及使用delete()clear()删除元素。
  2. Set可以包含任何 JavaScript 数据类型作为值。
  3. Set会维护值插入时的顺序,因此支持按顺序迭代。
  4. WeakSet中的“weak”(弱),描述的是 JavaScript 垃圾回收程序对待“弱集合”中值的方式。弱集合中的值只能是Object或者继承自Object的类型,尝试使用非对象设置值会抛出TypeError。这些值不属于正式的引用,不会阻止垃圾回收。
const ws = new WeakSet();
ws.add({}); // 因为没有指向这个对象的其他引用,所以当这行代码执行完成后,这个对象值就会被当作垃圾回收。
  1. WeakSet之所以限制只能用对象作为值,是为了保证只有通过值对象的引用才能取得值。如果允许原始值,那就没办法区分初始化时使用的字符串字面量和初始化之后使用的一个相等的字符串了。
  2. 弱集合在给对象打标签时还是有价值的。
const disabledElements = new WeakSet();
const loginButton = document.querySelector('#login');

// 通过加入对应集合,给这个节点打上“禁用”标签 
disabledElements.add(loginButton);
// 只要 WeakSet 中任何元素从 DOM 树中被删除,垃圾回收程序就可以忽略其存在,而立即释放其内存