作者:令川 | 发布时间:2022-10-02

JavaScript 中的集合

数组与对象

在 JavaScript 中,一个对象的键只能有两种类型:stringsymbol。下文只考虑键为字符串的情况。

创建对象

在创建对象时,若对象的键为数字,或者由 字母+数字 组成,那么键上的引号可以省去:

var obj1 = {1: 'one', 2: 'two'} // 等同于 {'1': 'one', '2': 'two'}

var obj2 = {'one': 1, 'two': 2} // 等同于 {one: 1, two: 2}

如果对象的键由 数字+字母 组成,那么键上的引号不能省去:

var obj = {'3d': true}  // √
var obj = {3d: true}   // ×

访问对象

在 JavaScript 中,访问一个对象的属性有三种方式++:++

  1. 方括号 + 字符串:obj['prop']
  2. 方括号 + 字符串变量:obj[keyName]
  3. 点 + 键名
var obj = {prop: 666, 1: 'one'}

obj.prop // => 666
obj['prop'] // => 666

// 若方括号内为数字,语法解释器会将其转换为字符串类型
obj['1'] === obj[1] // => true (*)

let keyName = 'prop'
obj[keyName] // => 666

// 访问不存在的属性时,输出 undefined
obj.keyName // => undefined
obj['keyName'] // => undefined

// 若方括号内既非数字也非字符串,语法解释器会将其解释为变量
obj[variable] // 将会报错,因为变量 variable 没有定义

上面代码的 (*) 行,obj[1] 方括号内是一个数组,语法解释器会自动将其转换成字符串类型,因此 obj['1']obj[1] 其实是一个东西,这样,我们就找到了 JavaScript 中的数组与对象的相通之处。

const arr = ['a', 'b', 'c']

arr[0] === arr['0'] // => true 

Object.keys(arr) // => ['0', '1', '2'] 注意输出的不是 [0, 1, 2]

可以看到,虽然我们习惯通过索引获取数组的元素,但在语法内部还是通过对象的键获取对应的值来实现的。根据这一特性构造一个伪数组:

const arrayLike = {0: 'a', 1: 'b', 2: 'c', length: 3}

arrayLike[0] === arrayLike['0']

数据类型

JavaScript 的八大数据类型分别是:numberbigintbooleanstringnullundefinedsymbolobject,前 7 种称为基本数据类型,object 为唯一的复杂数据类型。

JavaScript 中有 NumberStringObject…但他们是作为构造函数实际存在的。而 numberstringobject等在 JavaScript 中并不存在,是我们用来描述数据类型的。

基本数据类型的数据都是作为一个整体存在,复制操作是直接创建数据的副本,复制以后两个基本数据变量不会相互影响。

复杂数据类型的变量存放的是一个引用,复制以后两个变量指向的都是同一个内存地址,其中一个变量对数据作出改变会影响至另一个变量。

Array 并不是 JavaScript 中八大数据类型的其中一种,它之所以存在,是因为实际应用经常会有一些数据是有序的,而数组正是一种 有序 的数据集合。

我们可以认为: 数组 = 对象 + 额外的特性,这里 额外的特性 就是指数组专有的属性和方法,如表示数组长度的 length,可以进行数组拼接的 concat() 方法…

一切皆为对象,这是 JavaScript 世界中一句广为流传的话,也就是说,如果一个东西不属于 7 个基本数据类型,都可以认为它是基于对象扩展而来的一个新对象,也都能套用上面的公式:对象 + 额外的特性 = 新对象

上面说过,JavaScript 中是实际存在 Object 的,它是一个构造函数。Object 有很多方法可以帮助我们了解一个新对象的特性:

获取数据类型:

数组 Array

Array 表示数组的构造函数,Array === Array.prototype.constructor 将输出 true

据我观察,JavaScript 中首字母大写的都是构造函数…

Object === Object.prototype.constructor // => true
Date === Date.prototype.constructor // => true
Map === Map.prototype.constructor // =>  true
......

上图中,将 Array 换成 Date Map RegExp 等 JavaScript 中的其他对象,各关系依然成立。

通过 Object.getOwnPropertyNames() 即可获取该对象下所有的属性(包括不可枚举的):

Object.getOwnPropertyNames(Array) // => ["length", "name", "prototype", "isArray", "from", "of"]

Object.getOwnPropertyNames(Date) // => ["length", "name", "prototype", "now", "parse", "UTC"]

......

Array 的属性

Array.length: number

构造函数的属性,是静态的,固定值为 1

Array.name: string

构造函数的静态属性,固定值为 "Array"

Array.prototype: any[]

数组的原型对象,可认为它是程序中所有数组实例的“母体”

[].__proto__ === Array.prototype // => true

// 可以认为 Array.prototype ≈ []
Object.prototype.toString.call(Array.prototype) // => '[object  Array]'
Array.prototype.length // => 0

Array.prototype.max = function () {
    let currentArr = this // “谁调用该方法,this 就是谁”
    let max = -Infinity // 无穷小
    for (let i = 0; i < currentArr.length; ++i) {
      max = currentArr[i] > max ? currentArr[i] : max
    }
    return max
}

[6, 9, 7].max() // => 9

Array 的方法

Array.isArray(param: any): boolean

判断传入的参数是否为数组

// 下面均返回 true
Array.isArray([1])
Array.isArray([]) // 构造空数组
Array.isArray(Array()) // 构造空数组
Array.isArray(new Array()) // 构造空数组
Array.isArray(Array.prototype) // 数组的原型就是数组!

Array.from(arrayLike any, callback?: Function, thisArg?: any): any[]

根据传入的 arrayLike 创建一个数组,并将其返回。arrayLike 应该是下面两种对象的某一种:

// 伪数组对象
Array.from({0: 'a', 1: 'b', length: 2}) // => Array ["a", "b"]
// 可迭代对象
Array.from(['a', 'b'].keys()) // => Array [0, 1]

若指定了 callbackthisArg,那么 Array.from(arrayLike, callback, thisArg) 等同于 Array.from(arrayLike).map(callback, this),即让生成的数组调用一次 map() 方法,实现对数组中元素的“再加工”。

Array.from([1, 2, 3], el => el * 2) // [2, 4, 6]

Array.of(...items): any[]

返回一个数组,数组元素由传入的参数构成

Array.of(66, null, {}, "ok") // => [ 66, null, {}, "ok" ]
Array.of(8).length // => 1
Array(8).length // => 8

数组实例

一个 JavaScript 程序中,可以有很多数组实例,这些数组实例的原型只有一个:Array.prototype

[].__proto__ === Array.prototype // => true

[1, 2, 3].__proto__ === Array.prototype // => true

在浏览器控制台中输入 Array.prototype,即可查看当前环境支持的数组全部方法和属性,数组实例都可使用这些方法和属性

可以看到输入 Array.prototype 首先返回的是 [](绝大部分场景下,Array.prototype 是可以用 [] 代替的);其中有两个属性的属性名为 symbol 类型:

Array.prototype[Symbol.iterator] === Array.prototype.values // => true

Array.prototype[Symbol.unscopables]
/* => 
{
  copyWithin: true,
  entries: true,
  fill: true,
  find: true,
  findIndex: true,
  flat: true,
  flatMap: true,
  includes: true,
  keys: true,
  values: true
} */

属性值为原始数据类型的只有一个 length,剩下的就是数组成员方法了。

创建数组实例

4 种方式,创建数组 [1, 2, 3]

[1, 2, 3]

new Array(1, 2, 3)

Array(1, 2, 3)

Array.prototype.constructor(1, 2, 3)

4 种方式,创建长度为 3 的空数组(数组中 3 个元素均为 undefined

new Array(3)

Array(3)

Array.prototype.constructor(3)

var arr = []
arr.length = 3

数组实例的属性

再次说明:下方的 Array.prototype 都能用 [] 替换

Array.prototype.length: number

通过 length 属性,可以获取、设定一个数组的长度

设定一个比当前 length 更大的数值,可以实现对数组的扩充(扩充的元素为 undefined

const arr = ['a', 'b', 'c']
arr.length = 4
arr[3] // => undefined
arr // => ["a", "b", "c", empty];即相当于 {0: 'a', 1: 'b', 2: 'c', length: 4}

通过设定一个比当前 length 更小的数值,可以实现对数组的裁剪

const arr = ['a', 'b', 'c']
arr.length = 1
arr[1] // => undefined
arr // => ["a"]

数组实例的方法

下列方法中,如果方法的参数为索引,一般都可以是负索引,如 -1 是倒数第一个元素的索引,-2 是倒数第二个元素的索引…

修改器方法

若数组调用了修改器方法,那么该数组将会 发生变化。常用的修改器方法有如下几个: copyWithin() fill() pop() push() shift() unshift()  reverse() sort() splice()

Array.prototype.copyWithin(target: number, start?: number, end?:number): any[]

复制数组 start (默认为 0)到 end (默认为数组长度) 位置上的元素,再从 target 位置开始粘贴;返回改变后的数组。

let arr = ['a', 'b', 'c', 'd', 'e']
// 复制 arr[3], arr[4], 从 arr[2] 开始进行粘贴
arr.copyWithin(2, 3, 5) // => ["a", "b", "d", "e", "e"]

Array.prototype.fill(value: any, start?: number, end?: number): any[]

value 填充数组,填充的位置是从 start (默认为0) 到 end (默认为数组长度);返回改变后的数组。

let arr = ['a', 'b', 'c', 'd', 'e']
arr.fill([], 3, 5) // => ["a", "b", "c", [], []]
arr.fill('哈') // => ["哈", "哈", "哈", "哈", "哈"]

Array.prototype.pop(): any

删除数组最后一个元素;数组长度将减 1;返回被删除的元素。

let arr = ['a', 'b', 'c']
arr.pop() // => "c"
arr // => ["a", "b"]

Array.prototype.push(...items): number

将参数中的值追加到数组后面;返回数组长度。

let arr = ['a','b']
arr.push('c','d') // => 4
arr // => ["a", "b", "c", "d"]

Array.prototype.shift(): any

删除数组实例的第一个元素;数组长度将减 1;返回被删除的元素

let arr = ['a', 'b']
arr.shift() // => 'a'
arr // Outpus: ['b']

Array.prototype.unshift(...items): number

将传入的参数添加到数组的头部;返回改变后的数组长度

let arr = ['a', 'b']
arr.unshift('A', 'B') // => 4
arr // => ["A", "B", "a", "b"]

Array.prototype.reverse(): any[]

翻转数组实例中元素排列的顺序;返回翻转后的数组

let arr = ['a', 'b', 'c']
arr.reverse() // => ["c", "b", "a"]
arr // => ["c", "b", "a"]

Array.prototype.sort(compare?: Function): any[]

对数组中的元素进行排序;返回排序后的数组

const arr = ['b', 'c', 'a']
arr.sort() // => ["a", "b", "c"]
arr // => 同上

sort() 会试图将数组元素转换为字符串类型,再以字符串的标准进行排序

[2, 31, 111].sort() // [111, 2, 31]

['2', '31', '111'].sort() // ["111", "2", "31"]

可以自定义比较函数,当比较函数返回负数时,排序会发生变化

// arr.reverse() <==> arr.sort(() => -1)
['哈哈', '嘿嘿', '嘻嘻'].sort(() => -1) // => ["嘻嘻", "嘿嘿", "哈哈"]

[1, 2, 3, 4].sort((a, b) => {
    // 第一轮比较时,b = 1, a = 2,后面也是按照这种先后顺序
    return b - a
}) // => [4, 3, 2, 1]

Array.prototype.splice(start: number, deleteCount?: number, ...items): any[]

删除从 start 开始的 deleteCount 个元素,再将 items 插入此处;返回被删除的所有元素

const arr1 = ['a', 'b', 'c', 'd', 'e']
arr1.splice(2) // => ["c", "d", "e"]
arr1 // => ["a", "b"]

const arr2 = ['a', 'b', 'c', 'd', 'e']
arr2.splice(2, 2) // => ["c", "d"]
arr2 // => ["a", "b", "e"]

const arr3 = ['a', 'b', 'c', 'd', 'e']
arr3.splice(-2, 1, 'dd') // => ["d"]
arr3 // => ["a", "b", "c", "dd", "e"]

访问方法

数组调用访问方法不会改变自身,它会返回访问方法期望的值。常用的访问方法有:concat() includes() join()  slice() toString() indexOf() lastIndexOf()

Array.prototype.concat(...items): any[]

将参数拼接到数组末尾;返回拼接后的数组

const arr = [1, 2]
arr.concat(3, 4) // => [1, 2, 3]
arr // => [1, 2]

如果传入的参数是个数组,那么会将该数组展开再拼接,但至多展开一层

[].concat('a', ['b', 'c'], [1, [2,2]]) // => ["a", "b", "c", 1, [2, 2]]

实现数组的扁平化:

function flat(arr) {
  let rst = []
  for (let i = 0; i < arr.length; ++i) {
    if (arr[i].__proto__.constructor.name == 'Array') {
      rst = rst.concat(flat(arr[i]))
    } else {
      rst = rst.concat(arr[i])
    }
  }
  return rst
}

flat(['a', ['b', 'c'], [1, [2,2]]]) // => [ "a", "b", "c", 1, 2, 2 ]

Array.prototype.includes(searchElement: any, fromIndex?: number): boolean

fromIndex (默认为 0)开始,判断一个数组是否含有 searchElement

const arr = ['a', 'b', 'c']
arr.includes('b') // => true
arr.includes('b', -1) // => false

Array.prototype.indexOf(searchElement: any, fromIndex?: number): number

fromIndex (默认为 0)开始,在数组中寻找 searchElemnt, 若找到了则返回该元素所在的索引,没找到则返回 -1

const subArr = [3] 
const arr = [1, [2], subArr]
arr.indexOf(1) // => 0
arr.indexOf([2]) // => -1
arr.indexOf(subArr) // => 2

Array.prototype.lastIndexOf(searchElement: any, fromIndex?: number): number

indexOf() 是从前往后寻找,lastIndexOf() 则是从后往前寻找

Array.prototype.slice(begin?: number, end?: number): any[]

提取数组区间从 begin (默认为 0) 到 end (默认为数组长度) 之间的元素;返回提取的元素组成的数组

const arr = ['a', 'b', 'c', 'd', 'e']
arr.slice(2, 4) // => ["c", "d"]
arr.slice(-2) // => ["d", "e"]
arr // => ["a", "b", "c", "d", "e"]

Array.prototype.join(separator?: string): string

将数组中的元素用分隔符 separator (默认为 ,) 拼接;返回拼接后的字符串

const arr = ['a', 'b', 'c']
arr.join(',') // => 'a,b,c'
arr.join() // => 同上
arr // => ['a', 'b', 'c']

Array.prototype.toString(): string

',' 将数组元素拼接成字符串,因此 arr.toString()arr.joinarr.join(',') 这三者效果是相同的。

如果一个数组被当作字符串进行字符串拼接时,将隐式调用 toString() 方法

const arr = [1, 2]
'number: ' + arr // => "number: 1,2"

迭代方法

迭代方法会接收一个回调函数作为参数,数组中的元素会按照从前往后的顺序,依次作为参数传入回调函数中。常用的迭代方法有:forEach() every() some() filter() find() findIndex() map() reduce() reduceRight()

回调函数一般都会有如下三个参数:

  1. element 当前传入回调函数中的数组元素
  2. index 当前传入回调函数中的元素对应的索引
  3. array 调用迭代方法的数组实例

除了回调函数,迭代方法一般还会接收另一个参数 thisArg: any,它可作为回调函数中 this 的指向。

迭代方法不会改变数组,为了代码的可读性,请不要在回调函数中对数组进行修改操作。

Array.prototype.forEach(callback: Function, thisArg?: any): undefined

该方法是经典 for 循环的简便写法

let arr = [1]

arr.forEach(function () {
    console.log(this === arr)
}) // => false

arr.forEach(function () {
    console.log(this === arr)
}, arr) // => true

Array.prototype.every(callback: Function, thisArg?: any): boolean

判断数组中的每一个元素,是否都能通过回调函数的测试;若每次回调函数返回的都是 truthy,那么该方法将返回 true,否则返回 false

[3, 5, 1].every(el => el > 0) // => true

只要有一次回调函数返回的是 falsy,那么 every() 方法将立即结束,返回 false

[1, 2, 3].every(el => {
    console.log(el) // 分别打印 1 和 2
    return el != 2
}) // => false

Array.prototype.some(callback: Function, thisArg?: any): boolean

判断数组是否存在能通过回调函数的测试的元素;只要有一次回调函数返回的是 truthy,那么方法立即结束,返回 true;若所有回调函数返回的都是 falsy,那么方法返回 false

['a', 'b', 'c'].some(el => el === 'b') // => true
[false, 0, '', null, undefined, NaN].some(el => el) // => false

Array.prototype.filter(callback: Function, thisArg?: any): any[]

根据测试条件过滤元素;如果回调函数返回 truthy,那么暂存当前传入回调函数的数组元素,方法结束后,这些暂存的元素将组成一个新的数组,作为方法的返回值

[1, 2, 3, 4, 5].filter(el => el % 2 === 0) // => [2, 4]

Array.prototype.map(callback: Function, thisArg?: any): any[]

方法返回一个数组,数组元素由回调函数每次返回的结果组成

[1, 2, 3].map(el => el * 2) // => [2, 4, 6]

Array.prototype.find(callback: Function, thisArg?: any): any | undefined

寻找符合测试条件的元素;在回调函数 第一次 返回 truthy 时,方法结束,返回此时轮到的数组元素;若回调函数始终不返回 truthy,方法将返回 undefined

[2, 8, 11, 5].find(el => el > 10) // => 11

Array.prototype.findIndex(callback: Function, thisArg?: any): number

寻找符合测试条件的元素的索引;在回调函数 第一次 返回 truthy 时,返回此时轮到的数组元素的索引;若回调函数始终不返回 truthy,方法将返回 -1

[2, 8, 11, 5].findIndex(el => el > 10) // => 2

Array.prototype.reduce(callback: Function, firstValue?: any): any

通过回调函数中的运算规则,计算 array[i]array[i + 1],计算的结果作为下一轮回调函数的参数,与 array[i + 2] 再进行计算……返回最后一次回调函数计算的结果。

在之前的迭代方法中,回调函数接收 3 个参数:callback(element?: any, index?: number, array?: any[]){}reduce() 中的回调函数将会接收四个参数:callback(accumulator?: any, element?: any, index?: number, array?: any[]) {}。可以看出 reduce() 多出一个 accumulator 参数,其译为 累积器,它的值是上一轮回调函数返回的值。

['x', 'y', 'z'].reduce((accumulator, element) => {
    console.log(accumulator, element) // 将依次输出:1. x y  2. xy z
    return accumulator + element
}) // => "xyz"

从上面的例子中还能看出:

因此,单元素的数组调用 reduce() 方法,将始终返回第一个元素值

['first'].reduce(() => 'end') // => 'first'
['first'].reduce(a => a + ' end') // => 同上

reduce() 返回最后一次回调函数的结果

[[1, 1], [2, 2]].reduce((accumulator, element, index, arr) => {
    accumulator = accumulator.concat(element)
    return accumulator
}) // => [1, 1, 2, 2]

[[1, 1], [2, 2]].reduce((accumulator, element, index, arr) => {
    accumulator = accumulator.concat(element)
    return index === arr.length - 1 ? 'end' : accumulator
}) // => 'end'

若指定了 reduce() 的第二个参数 firstValue: any,那么 accumulator 的初始值就是 firstValue,执行回调函数的次数也将多 1 次

[1, 2].reduce((accumulator, element) => {return accumulator + element}) // => 3
[1, 2].reduce((accumulator, element) => {return accumulator + element}, 97) // => 100;不是 99!

实现数组去重:

function deDuplication(arr) {
  return arr.reduce((accumulator, el) => {
    return accumulator.includes(el) ? accumulator : accumulator.concat(el)
  }, [])
}

deDuplication([1,1,2,3,2,3,3]) // => [1, 2, 3]

Array.prototype.reduceRight(callback: Function, firstValue?: any): any

reduce() 是从前往后运算,reduceRight() 是从后往前运算;其他方面一致

获得迭代器对象的方法

let arr = ['a', 'b', 'c']

let keyIterator = arr.keys()
keyIterator.next() // => {value: 0, done: false}
keyIterator.next() // => {value: 1, done: false}
keyIterator.next() // => {value: 2, done: false}
keyIterator.next() // => {value: undefined, done: true}

for (let value of arr.values()) {
    console.log(value)
}
// =>
// 'a'
// 'b'
// 'c'

console.log(...arr.entries()) // => [0, 'a'] [1, 'b'] [2, 'c']

数组实例的方法小结

通用性

数组的很多方法被设计成是通用的,也就是说非数组对象也可以调用数组的方法。下面是几个示例:

fill() 的通用用法:

let length = 3
Array.prototype.fill.call({length}, 'x') // => {0: "x", 1: "x", 2: "x", length: 3}
[].fill.apply({'length': 3}, ['x', 0, 3]) // 输出同上

map() 的通用用法:

// 输出字符串中每个字符的代码
[].map.call('ABC', el => el.charCodeAt()) // => [65, 66, 67]

includes() 的通用用法:

// 判断函数输入的参数中是否有某个值
function hasParam(arguments_, param) {
  // 每个函数的 arguments 虽然是数组的外形,但它的类型不是 Array,因此不能直接调用数组的方法 includes
  return [].includes.call(arguments_, param)
}

function func() {
  if (hasParam(arguments, 'GET')) {
    console.log('输入中包含 GET')
  } else {
    console.log('输入中不包含 GET')
  }
}

push() 的通用用法:

let obj = {}
Array.prototype.push.apply(obj, ['a', 'b']) // => 2(push() 返回数组长度)
obj // => {0: 'a', 1: 'b', length: 2}; 若对象中没有 length 属性将自动加上

数组和函数

声明函数时,可以通过 ...args 将待传入的参数包裹在一个数组中。其他地方使用 ... 可以展开一个数组或对象。

// 延时 ms 毫秒执行函数
Function.prototype.delay = function(ms) {
    let that = this
    return function(...args) {
      setTimeout(() => {
        that.apply(this, args)
        // 或:that.call(that, ...args)
      }, ms)
    }
}

function f(a, b) {
  console.log( a + b );
}

f.delay(1000)(1, 2) // 1000ms 后输出 3

Array、Set、Map、WeakMap、WeakSet

Array 用来表示实际应用中的一组 有序 的数据

Set 用来表示实际应用中一组 不会重复 的数据,即集合中个元素唯一

Map 可以视为 Object 的扩展,因为 Object 只允许键为 stringsymbol 类型,而 Map 的键可以是 任意类型

Map 实例的方法和属性

通过 new Map(Object.entries(o))Object.fromEntires(map) 可以实现 MapObject 的相互转换

Set 实例的方法和属性

WeakMapWeakSet

现有如下代码:

let obj = {1: 'one'}
let map = new Map()
// 将 obj 作为 map 的一个键
map.set(obj, '')

// 覆盖引用
obj = null

map.keys() // => [{value: {1: 'one'}}]

null 赋值给引用 obj,我们预期的是对象 {1: 'one'} 能够随之被“垃圾回收”清理掉,然而因为它存在于 map 中,因此会被阻止清理——我们仍然可以通过 map.keys() 获取到 {1: 'one'}。也就是说 {1: 'one'} 将始终存在内存中,这不是我们预期的,在大型项目当中,这会导致大量不再用到的对象始终存在内存中。

WeakMapWeakSet 可以解决这一问题。

WeakMap 的实例只有以下方法:

WeakSet 的实例只有以下方法:

WeakMap 只允许对象作为键,WeakSet 只允许对象作为集合元素;当该对象在其他地方不能被访问时,WeakMapWeakSet 中的对象将会随之被清理掉。

// 该代码直接在控制台中执行看不到效果
// 需要放在文件中执行,才能看到对象自动被清理
let obj = {}
let weakMap = new WeakMap()
weakMap.set(obj, '')
console.log(weakMap) // => [[{}: '']]

obj = null
console.log(weakMap) // => []

目录 / Contents

空。

令川 · 记