阅读 3102

你必须理解的 JavaScript 知识 —— 引用类型

传送门

系列文章适合想系统性学习、进阶 JavaScript 或扫盲的前端开发者阅读

基本引用类型

  • 理解对象
  • 基本 JavaScript 数据类型
  • 原始值和原始值包装类型

引用值(对象)是某个特定引用类型的实例。新对象通过 new 操作符后跟一个构造函数来创建。构造函数就是用来创建新对象的函数,比如:

const date = new Date()
复制代码

这行代码创建了引用类型 Date 的实例 date,Date 就是构造函数。ECMAScript 提供了很多像 Date 这样的原生引用类型。

Date

Date 类型为操作日期提供了方便。本身没有什么难点,这里只介绍几个简单常用的方法,更加复杂的方法在平时使用时可以参考 MDN

建议大家在平时的工程开发中使用 dayjs,提供了简单、丰富、实用的 API,足以满足大家平时的需求;momentjs 也可以,只是相对 dayjs 会比较大一些,而且官方已经宣布不维护了。不建议大家在项目开发中通过原生 Date 类型去造轮子。

// 基于当前时间创建一个日期对象
const d1 = new Date()
// Sat Dec 26 2020 07:49:22 GMT+0800 (中国标准时间)
console.log(d1)
// 返回当前时间的毫秒数
console.log(Date.now())
// 返回指定日期的毫秒数
console.log(Date.parse('5/23/2019'))
const d2 = new Date()
// 后台会调用 d2.valueOf() 方法得到毫秒数,再做比较
console.log(d1 < d2)https://www.runoob.com/regexp/regexp-tutorial.html
复制代码

RegExp

ECMAScript 通过 RegExp 类型支持正则表达式。

正则表达式说难不难(就是一堆要记的东西),但说简单也不简单,毕竟它处于一个经常学,经常忘的状态,所以基本上平时的状态就是现用现查,

这里给大家推荐两个网站 菜鸟教程MDN,不论是系统性的学习还是平时资料查询都可以满足。另外再推荐一个可视化的工具 Regulex。这三个网站足以满足大家对正则表达式的日常需求了。

原始值包装类型

为了方便操作原始值,ECMAScript 提供了 3 种特殊的引用类型(原始值包装类型):Boolean、String、Number。,它们的存在让原始值拥有了对象的行为,每当用到某个原始值的方法或者属性时,后台就会创建一个相应原始包装类型的对象,从而暴露出操作原始值的各种方法。

引用类型和原始值包装类型的区别在于对象的生命周期。通过 new 实例化一个引用类型后,得到的实例会在离开作用域时被销毁,而自动创建的原始值包装对象则只存在于访问它的那行代码执行期间。这意味着不能在运行时给原始值添加属性和方法,因为这条语句执行完这个实例就销毁了。

const str = 'test'
const sub = str.substring(2)
console.log(sub)  // st
str.color = 'red'
console.log(str.color) // undefined
复制代码

以上代码的执行过程如下:

  • 第一行,创建一个字符串常量,它是一个原始值
  • 第二行,是以 读模式 访问的,也就是要从内存中读取变量的值。在以读模式访问字符串时,后台会执行以下过程
    • 创建一个 String 类型的实例,const ins = new String(str)
    • 调用实例上的特定方法,const sub = ins.substring(2)
    • 销毁实例(ins)
  • 第三行,输出 sub 变量
  • 第四行
    • 创建一个 String 类型的实例,const ins = new String()
    • 给 ins 对象设置属性,ins.color = 'red'
    • 销毁 ins 实例
  • 第五行
    • 创建一个 String 类型的实例,const ins = new String)
    • 输出 ins.color,因为没有 color 属性,所以值为 undefined
    • 销毁 ins 实例

Object 构造函数作为一个工厂方法,能够根据传入值的类型返回相应原始值包装类型的实例,比如:

const obj = new Object(2)
console.log(obj instanceof Number) // true
const objB = new Object(false)
console.log(objB instanceof Boolean) // true
console.log(objB instanceof Number) // false
复制代码

如非必要情况,不要手动创建原始值包装类型,会让其它开发者疑惑,分不清楚它们到底值是原始值还是引用类型。

接下来介绍如何手动创建原始值包装类型(永远不要这么做)和它们的一些实例方法

Boolean

布尔值的引用类型是 Boolean,Boolean 的实例会重写 valueOf() 方法,返回布尔值的原始值,toString() 方法被调用时返回布尔值的字符串形式。

以下示例包含了使用方法和需要大家注意、理解的一些奇特现象:

const bObj = new Boolean(false)
// valueOf 和 toString 方法
console.log(bObj.valueOf()) // false
console.log(oObj.toString()) // false
// 类型
console.log(typeof bObj)  // object
console.log(typeof false) // boolean
// 需要注意的现象
console.log(true && bOjb) // true,因为 bObj 是对象,Boolean(对象)永远返回 true
console.log(true && false) // false
// 是否是 Boolean 类型的实例
console.log(bObj instanceof Boolean) // true
console.log(false instanceof Boolean) // false
复制代码

Number

Number 是数值类型的引用值,使用 new Number(10) 创建一个 Number 对象。Number 类型也重写了 valueOf()、toLocaleString() 和 toString() 方法。valueOf() 方法返回 Number 对象表示的原始值,剩下两个返回数值的字符串表示形式,并且接受一个参数,表示返回指定进制的数值字符串。

const num = 2
// 返回指定进制的数值字符串
console.log(num.toString(2)) // 10
// 返回数值的字符串表示形式
console.log(num.toLocaleString(2)) // 2
// 返回指定小数位位数的数值字符串,不涉及四舍五入
console.log(num.toFixed(2)) // 2.00
// 返回数值的科学记数法的字符串表示形式,接收一个参数表示小数的位数,一般这么小的数不用科学计数法形式
console.log(num.toExponential(1)) // 2.0e+0
// 会根据情况返回合理的输出结果,有可能是固定长度,也有可能是科学计数法形式
console.log(num.toPrecision(1)) // 2
// 注意
const numObj = new Number(2)
console.log(typeof num) // number
console.log(typeof numObj) // object
console.log(numObj instanceof Number) // true
console.log(num instanceof Number) // false
复制代码

String

String 是字符串的引用类型,使用如下方法创建一个 String 对象:

const strObj = new String('hello world')
复制代码

JavaScript 字符

  • 码元:JavaScript 中的每个字符都是由 16 位的码元组成,字符串的 length 属性表示字符串包含多少个 16 位码元

    const msg = 'hello'
    // 返回指定索引位置的字符
    console.log(msg.charAt(1)) // e
    // 返回指定索引位置的字符的码元值,即 ASCII 值
    console.log(msg.charCodeAt(1)) // 101
    // 根据给定的所有码元值参数返回拼接后的字符串
    console.log(String.fromCharCode(0x61, 0x62, 0x63, 0x64, 0x65)) // abcde
    console.log(String.fromCharCode(97, 98, 99, 100, 101)) // abcde
    复制代码
  • 基本多语言平面:在 U+0000~U+FFFF 范围内的字符,即16 位码元唯一表示 65536 个字符

  • 代理对:为了表示更多的字符,Unicode采用了一个策略,即每个字符使用另外 16 位去选择一个增补平面。这种每个字符使用两个

16 位码元的策略成为 代理对

length、charAt()、charCodeAt() 和 fromCharCode() 都是基于 16 位码元完成操作,但是涉及使用代理对编码的字符就会出问题,比如笑脸表情符号😊(U+1F60A),它是由两个 16 位码元组成,但是 length 还是返回 1,所以如果涉及到非基本多语言平面的字符时需要注意,不过这在平时的编码中不多见

  • 码点:Unicode 中一个字符的完整标识,比如 "c" 的码点是 0x0063, "😊"的码点是 0x1F60A。码点可能是 16位,也可能是 32 位。为了正确解析既包含单码元字符又包含代理对字符的字符串,可以使用以下两个方法:

    • codePointAt():返回指定索引位置的码点。如果传入的索引不是代理对的开头,会返回错误的码点,一般会在检测单个字符的时候出现,可以通过迭代字符串的方式来规避这种情况,迭代字符串可以只能的识别代理对的码点
    • fromCodePoint():接收任意数量的码点,返回对应字符拼接后的字符串
    const str = 'a😊'
    // 码点
    console.log(str.codePointAt(0))	// 97
    console.log(str.codePointAt(1)) // 128522
    console.log(str.codePointAt(2)) // 56842
    // fromCharCode 能返回正确的结果是因为它是基于提供的二进制表示直接组合成字符串,浏览器可以正确解析代理对,并正确的将其识别位一个 Unicode 笑脸字符
    console.log(String.fromCharCode(97, 55357, 56842)) // a😊
    console.log(String.fromCodePoint(97, 128522)) // a😊
    复制代码

normalize() 方法

某些 Unicode 字符可以有多种编码方式。有的字符既可以通过一个 BMP 字符表示,也可以通过一个代理对表示。比如:Å

// U+00C5:上面带圆圈的大写拉丁字母A
const a1 = String.fromCharCode(0x00C5)
// U+212B:长度单位“埃”
const a2 = String.fromCharCode(0x212B)
// U+004:大写拉丁字母A,U+030A:上面加个圆圈
const a3 = String.fromCharCode(0x0041, 0x030A)

console.log(a1, a2, a3);          // Å Å Å

// 同一个 Unicode 字符,比较的结果却互不相等
console.log(a1 === a2) // false
console.log(a1 === a3) // false
console.log(a2 === a3) // false
复制代码

为了解决上面的问题,Unicode 提供了 4 种规范化形式,可以将类似上面的字符规范化为一致的格式,这 4 种规范化形式是:NFD(Normalization Form D)、NFC(Normalization Form C)、NFKD(Normalization Form KD)和NFKC(Normalization Form KC)。使用 normalize() 方法对字符串应用这些规范化形式,使用时需要传入表示那种形式的字符串:"NFD"、"NFC"、"NFKD" 或 "NFKC"。

// 使用同一种规范化形式可以让比较操作符返回正确的结果
console.log(a1.normalize('NFD') === a2.normalize('NFD')) // true
console.log(a2.normalize('NFKC') === a3.normalize('NFKC')) // true
console.log(a1.normalize('NFC') === a3.normalize('NFC')) // true
复制代码

字符串方法

以下内容是一些常用方法的示例,可以自己写一写,测验以下是否都清楚,更加具体的信息查看 MDN,这里就不占用篇幅一一列举这些方法的使用说明了

// 以下所有方法都不会更改原字符串
const str = 'hello'

// 字符串拼接
console.log(str.concat(' world', '!!')) // hello world!!
console.log(str + ' world' + '!!') // hello world!!
// 提取子串
console.log(str.slice(0, 2)) // he
console.log(str.substring(0, 2)) // he
console.log(str.substr(1, 2)) // el
// 搜做指定字符的索引
console.log(str.indexOf('el')) // 1
console.log(str.lastIndexOf('el')) // 1
// 判断字符串是否包含指定子串
console.log(str.startsWith('he')) // true
console.log(str.endsWith('lo')) // true
console.log(str.includes('ll')) // true
// 删除字符串两端的空格,返回新的字符串
console.log(' hello '.trim()) // 'hello'
console.log(' hello '.trimLeft()) // 'hello '
console.log(' hello '.trimRight()) // ' hello'
// 复制字符串 x 次
console.log(str.repeat(3)) // hellohellohello
// 在字符串的一侧填充指定字符到指定长度,第二个参数是待填充的字符,默认为空格
console.log(str.padStart(10)) // '     hello'
console.log(str.padEnd(10)) // 'hello     '
// 迭代和解构,通过 String 的默认迭代器方法 @@iterator 实现
const strIterator = str[Symbol.iterator]()
console.log(strIterator.next()) // { value: 'h', done: 'false' }
console.log(strIterator.next()) // { value: 'e', done: 'false' }
// for of 循环可以通过这个迭代器按需访问每个字符
for (const c of  str) {
  console.log('for of: ', c)
}
console.log([...str]) // ['h', 'e', 'l', 'l', 'o']
// 大小写转换
console.log(str.toUpperCase()) // HELLO
console.log('HELLO'.toLowerCase()) // hello
// 字符串模式匹配
console.log(str.match(/ll/g)) // ["ll", index: 2, input: "hello", groups: undefined]
console.log(str.match('ll')) // ["ll", index: 2, input: "hello", groups: undefined]
// 搜索指定匹配项的位置
console.log(str.search(/ll/g)) // 2
// 字符串替换
console.log(str.replace(/ll/g, '**')) // he**o
// replace 的第二个参数可以是一个函数,可以做更加细致复杂的替换
const ret = str.replace(/ll/g, function (match, pos, originalText) {
  return '**'
})
console.log(ret) // he**o
// 分割字符串,如果指定的分割字符串现在字符串的开头或末尾,结果数组中的第一个或最后一个元素为空字符串
console.log(str.split('')) // ['h', 'e', 'l', 'l', 'o']
复制代码

单例内置对象

ECMA-262 对内置对象的定义是“由 ECMAScript 实现、与宿主环境无关,并且在 ECMAScript 程序开始运行时就已经存在的对象”。意味着,开发者不需要显示的实例化内置对象,因为它们已经实例化好了。前面提到的 Object、Array、String 都是内置对象。

Global

Global 对象是 ECMA-262 规定的一个兜底对象,代码无法显示的访问它。不属于任何对象的属性和方法就是 Global 的属性和方法。比如之前提到的 isNaN()、isFinite()、parseInt() 和 parseFloat() 等,实际都是 Global 对象的方法。

URL 编码方法

encodeURI() 和 encodeURIComponent() 方法用于编码统一资源标识符(URI),用特殊的 UTF-8 编码替换掉所有的无效字符(比如:空格),然后传递给浏览器便于浏览器理解。

示例 URLwww.wrox.com/illegal value.js#start

  • encodeURI(),不会编码属于 URL 组件的特殊字符,比如冒号、斜杠、问好、井号
  • encodeURIComponent(),会编码它发现的所有非标准字符
  • decodeURI(),解码 encodeURI() 编码后的字符串
  • decodeURIComponent(),解码 encodeURIComponent() 方法编码的字符串
const url = 'www.wrox.com/illegal value.js#start?dev = true'
// 编码
// www.wrox.com/illegal%20value.js#start
console.log(encodeURI(url))
// www.wrox.com%2Fillegal%20value.js%23start
console.log(encodeURIComponent(url))
// 解码,解码方法不能混用,decodeURI() 无法解码 encodeURIComponent()
// www.wrox.com/illegal value.js#start?dev = true
console.log(decodeURI(encodeURI(url)))
// www.wrox.com/illegal value.js#start?dev = true
console.log(decodeURIComponent(encodeURIComponent(url)))
复制代码

eval() 方法

eval() 方法是整个 ECMAScript 语言中最强大的方法,因为它就是一个完整的 ECMAScript 解释器,接收一个要执行的 ECMAScript(JavaScript)字符串作为参数。

// 以下代码不要在 google 的首页运行,会报错,可以去百度的首页运行
// hello eval
eval("console.log('hello eval')")
console.log('hello eval')
复制代码

代码在运行时,解释器发现 eval() 调用时,会将参数解释为实际的 ECMAScript 语句,然后将其插入到该位置。非严格模式下 eval() 执行的代码属于当前上下文,被 eval() 执行的代码与该上下问拥有相同的作用域链。

eval() 函数很强大,但是一般情况下你用不到,而且用在不合适的场景,危险且性能也差。比如如果解释的内容为用户输入的内容,容易造成 XSS 攻击。性能差是因为编译器无法做静态优化

window 对象

虽然 ECMA-262 没有规定直接访问 Global 对象的方式,但是浏览器将 window 对象实现为 Global 对象的代理。因此所有全局作用域中声明的变量和函数都成了 window 对象的属性,const 和 let 声明的除外。

提供一种获取 Global 对象的方式:

// 这个立即执行函数中的 this 代表任何上下文中的 global 对象
const global = function () {
  return this
}()
复制代码

Math

ECMAScript 通过 Math 对象提供了很多的数学公式、信息和计算方法。Math 对象上提供的计算要比直接通过 JavaScript 实现快的多,因为 Math 对象上的计算使用了 JavaScript 引擎中更高效的实现和处理器指令。但使用 Math 计算的精度会因浏览器、操作系统、指定和硬件而已

Math 对象上的各个方法使用都很简单,就是简单的方法调用,这里就不占用篇幅一一列举了,详细的方法列表和使用说明参考 MDN

集合引用类型

  • 对象
  • 数组和定型数组
  • Map、WeakMap、Set 和 WeakSet 类型

Object

Object 是 ECMAScript 中最常用的类型之一,创建 Object 的实例有两种方式。

  • new 加 Object 构造函数的方式
    const obj = new Object({
      name: 'lyn'
    })
    复制代码
  • 字面量的方式
    const obj = {
      name: 'lyn'
    }
    复制代码

使用字面量创建对象时,后台并没有调用 Object 构造函数

当一个函数有大量的可选参数时,这些可选参数可以用一个 对象 来代替。

一般有两种方式读取对象的属性值:

const obj = {
  name: 'lyn'
  'test prop': 'haha'
}
// 点语法
console.log(obj.name)  // lyn
// 中括号语法
console.log(obj['test prop']) // haha
复制代码

中括号的方式特别适合有些属性中包含非字母数字字符的情况,这种情况下不能使用点语法。不过,点语法通常是首选的属性存取方式,除非访问属性时必须使用变量。

Array

ECMAScript 中的数组是一组有序数据,和其它语言不同的是,数组的每个元素可以是不同的类型,而且数组的大小也是动态可变的,会随着数据的添加自动增长。

创建数组

  • Array 构造函数

    const arr1 = new Array()
    console.log(arr1) // [],length 为 0
    const arr2 = new Array(3)
    console.log(arr2) // [],length 为 3 的空数组
    const arr3 = new Array(1, 2, 3)
    console.log(arr3) // [1, 2, 3]
    const arr4 = Array(1, 2, 3) 
    console.log(arr4) // [1, 2, 3]
    复制代码
  • 字面量

    const arr1 = []
    console.log(arr1) // [], length 为 0
    const arr2 = [1, 2, 3]
    console.log(arr2) // [1, 2, 3]
    // 空位数组
    const arr3 = [, , ,]
    console.log(arr3) // [], length 为 3 的空数组
    复制代码
  • Array.of(),将一组参数转换为数组实例,用于替换 ES6 之前的 Array.prototype.slice.call(args),一种异常笨拙的方法

    const arr = Array.of(1, 2, 3)
    console.log(arr) // [1, 2, 3]
    复制代码
  • Array.from(),将类数组结构(可迭代对象或有 length 属性和可索引元素的结构)转换为数组实例

    console.log(Array.from('test')) // ['t', 'e', 's', 't']
    const m = new Map().set(1, 2).set(3, 4)
    // 将 Map 转换为数组,Set 也可以
    console.log(Array.from(m)) // [[1, 2], [3, 4]]
    // 数组浅复制
    const arr1 = [1, 2, 3]
    const arr2 = Array.from(arr1)
    console.log(arr1 === arr2) // false
    // 通过第二个参数增强新数组的元素
    // Array.from().map() 的升级版
    const arr3 = Array.from(arr1, function(x) { return x * 2 })
    console.log(arr3) // [2, 4, 6]
    复制代码

备注:尽量不要使用空位数组[,,,]的方式声明数组,因为 ES6 新增的方法和 ES6 之前的方法对空位数组的处理不一致,ES6 新增的方法(比如 Map)认为每个空位的值都是 undefined,但 ES6 之前的方法(比如 join)认为其为空(不存在)

检测数组

三种检测一个变量是否为数组的方式,instanceof、Array.isArray()、Object.prototype.toString.call()

instanceof

判读变量是否是 Array 类型的实例来判断变量是否为数组。在页面只有一个全局作用域的情况下没问题。如果网页中有多个框架,则可能有两个不同版本的 Array 构造函数。这时候使用 instanceof 就会有问题

Array.isArray()

Array.isArray() 方法,就是为了解决 instanceof 的问题,这个方法可直接确定一个值是否为数组,不用管它是在哪个全局执行上下文中创建的。

Object.prototype.toString.call()

万能类型判断方法,无缺点。

const arr = [1, 2, 3]
console.log(arr instanceof Array) // true
console.log(Array.isArray(arr)) // true
console.log(Object.prototype.toString.call(arr)) // [object Array]
复制代码

迭代器方法

在 ES6 中,Array 的原型上暴露了 3 个用于确定数组内容的方法:keys()、values()、entries()

  • keys(),返回数组索引的迭代器
  • values(),返回数组元素的迭代器
  • entries(),返回数组索引/值对的迭代器
const arr = ['foo', 'bar']
console.log(arr.keys(), arr.values(), arr.entries()) // 输出 3 个 Array Iterator {}
console.log(Array.from(arr.keys())) // [1, 2]
console.log(Array.from(arr.values())) // ['foo', 'bar']
console.log(Array.from(arr.entries())) // [[0, 'foo'], [1, 'bar']]
for (const [idx, val] of arr.entries()) {
  // 0 => foo
  // 1 => bar
  console.log(idx, ' => ', val)
}
复制代码

不要将 Array 的 keys()、values() 和 Object.keys()、Object.values() 搞混,后面两个返回的是一个对象索引、值组成的数组,前两个返回的是数组索引、值的迭代器

复制和填充方法

  • fill(要填充的值val,开始索引start, 结束索引end),给数组指定的索引范围(>= start && < end)填充指定的值

    const arr = [0, 0, 0]
    arr.fill(5, 0, 3)
    console.log(arr) // [5, 5, 5]
    复制代码
  • copyWithin(插入位置的索引insert, 开始索引start, 结束索引end),按照指定的范围浅复制数组中的部分内容(>=start && < end),然后将复制的内容插入到指定位置(insert)。该方法不会改变数组长度,默认忽略超出数组长度的部分

    const arr = [1, 2, 3, 4, 5]
    // start 默认为 0,end 默认为数组最后一个索引
    arr.copyWithin(3)
    console.log(arr) // [1, 2, 3, 1, 2]
    复制代码

这两个方法也支持负值索引,负值索引其实就是“数组长度加上它得到的一个正索引”

转换方法

所以对象都有 toLocaleString()、toString() 和 valueOf() 方法,valueOf() 方法返回数组本身,toLocaleString() 和 toString() 返回由数组中每个元素的字符串形式和逗号拼接而成的字符串,会依次调用数组中每个元素的 toString() 方法。

const person1 = {
  toString() {
    return 'p1'
  }
}
const person2 = {
  toString() {
    return 'p2'
  }
}
const arr = [person1, person2]
console.log(arr.toString()) // p1, p2
// 指定分隔符
console.log(arr.join('-')) // p1-p2
// null、undefined
const arr1 = [1, null, undefined, 3]
console.log(arr1.join('-')) // 1---3
复制代码

如果数组某一项为 null 或 undefined,则在 join()、toLocaleString()、toString() 和 valueOf() 返回的结果中会以空字符串表示

栈、队列方法

ECMAScript 为数组提供了几个方法,用来模拟另外一些数据结构。比如 栈和队列。栈是一种先进后出的结构。队列是先进先出的结构。

  • 栈方法,元素的添加和删除都在栈顶发生,即数组的末尾
    • push(),从数组末尾插入指定元素,并返回数组的最新长度
    • pop(),删除数组的最后一个元素,并返回元素,然后数组的 length - 1
  • 队列方法,元素从末尾(队尾)添加,开始除(队首)删除
    • push()
    • shift(),从数组开始位置删除一个元素并返回它,然后数组长度减 1

unshift() 方法可以在数组开头添加任意多个元素,然后返回新的数组长度。

排序方法

数组有两个排序方法:reverse() 和 sort()。reverse() 用来反转数组,sort() 用来给数组按照指定的规则排序。两个方法都是直接在愿数组上操作。

sort() 方法默认按照每个元素的字符串顺序升序排序,这个默认规则在大多数情况下是不合适的,比如

const arr = [5, 10]
arr.sort()
console.log(arr) // [10, 5]
复制代码

在执行的时候会依次在数组的每一项上调用 String 转型函数,'10' 是小于 '5'。为此 sort() 方法可以接收一个比较函数,用于判断哪个值应该排在前面。比较函数接收两个参数,如果希望第一个参数排在第二个参数前面,则返回负值,相等,就返回0,否则返回正值。

const arr = [5, 10]
// 接收两个参数
function compare(a, b) {
  return a - b
}
arr.sort(compare) // [5, 10]
复制代码

操作方法

  • concat(),用于在当前数组的副本末尾拼接新的元素

    const arr = [1, 2]
    const ret = arr.concat([3, 4], 5, [[6]])
    // 有打平数组的能力,不过只能打平一层
    console.log(ret) // [1, 2, 3, 4, 5, [6]]
    // 原数组保持不变
    console.log(arr) // [1, 2]
    复制代码

    打平数组参数的行为可以重写,方法是在参数数组上指定一个特殊的符号:Symbole.isConcatSpreadable。这个符号可以控制 concat 是否打平参数数组。

    const arr = [1, 2]
    const arr1 = [3, 4]
    // 禁止打平
    arr1[Symbol.isConcatSpreadable] = false
    // 强制打平类数组对象
    const arr2Obj = {
      [Symbol.isConcatSpreadable]: true,
      length: 2,
      0: 5,
      1: 6
    }
    const ret = arr.concat(arr1, arr2Obj)
    console.log(ret) // [1, 2, [3, 4], 5, 6]
    console.log(arr) // [1, 2]
    复制代码
  • slice(),用于床架一个包含原数组部分元素的新数组。接收两个参数,开始索引(start)和结束索引(end),选取两个索引之间(>= start && < end)的元素作为新数组的元素。

    const arr = [1, 2, 3, 4, 5]
    const ret = arr.slice(1, 3)
    console.log(ret) // [2, 3]
    复制代码
  • splice(),可以说是最强大的数组方法,可以完成数组元素的删除、插入、替换操作。splice(要插入或删除的元素的索引,被删除的元素的个数,一系列要插入的元素)

    const arr = [1, 4, 5]
    // 插入,插入到指定索引前面
    arr.splice(1, 0, 2, 3)
    console.log(arr) // [1, 2, 3, 4, 5]
    // 删除,从指定索引位置开始删除 x 个元素
    arr.splice(4, 1)
    console.log(arr) // [1, 2, 3, 4]
    // 替换
    arr.splice(0, 4, ...['*', '*', '*', '*'])
    console.log(arr) // ['*', '*', '*', '*']
    复制代码

搜索和位置方法

ECMAScript 提供了两类搜索数组元素的方法:按严格相等搜索和按断言函数搜索

严格相等

ECMAScript 提供了 3 个严格相等的搜索方法:indexOf()、lastIndexOf()、includes()。每个方法都接收两个参数,第一个是要查找的元素,第二个是要查找的起始位置,不提供默认为 0。在执行过程中会使用全等(===)操作符进行比较,也就是两项必须严格相等。

const arr = [1, 2, 3, 4, 5]
// 返回被查找元素的索引位置,未找到则返回 -1
console.log(arr.indexOf(2)) // 1
// 从后向前搜索,第二个参数还是从左向右计算
console.log(arr.lastIndexOf(43)) // 3
console.log(arr.lastIndexOf(4, 1)) // -1
console.log(arr.includes(3)) // true
复制代码

断言函数

ECMAScript 允许按照定义的断言函数搜索数组元素,每个元素都会依次调用断言函数,根据断言函数的返回值(boolean)决定元素是否匹配。方法接收两个参数,第一个是断言函数,断言函数接收三个参数,当前元素,当前元素的索引,当前便利的数组;第二个参数是一个可选的 this,用于指定断言函数中的 this。

ECMAScript 中支持断言函数的方法有:find()、findIndex()。

const arr = [1, 2, 3]
// 返回第一个匹配到的元素
const retItem = arr.find((item, idx, arrBak) => {
  return item === 2
})
console.log(retItem) // 2
// 返回第一个匹配到的元素的索引
const retIdx = arr.findIndex((item, idx, arrBak) => {
  return item === 2
})
console.log(retIdx) // 1
复制代码

迭代方法

ECMAScript 为数组提供了五个迭代方法,每个迭代方法的函数签名和断言函数是一样的。数组的每一项都会执行传入的函数,函数的执行结果的影响因方法而异。

  • every(),如果每一项都返回 true,则返回 true

    const arr = [1, 2, 3]
    const retTrue = arr.every(function(item) {
      return item > 0
    })
    console.log(retTrue) // true
    const retFalse = arr.every(function(item) {
      return item > 1
    })
    console.log(retFalse) // false
    复制代码
  • filter(),函数返回 true 的项会组成新的数组然后返回

    const arr = [1, 2, 3]
    const ret = arr.filter(function(item) {
      return item > 1
    })
    console.log(ret) // [2, 3]
    复制代码
  • forEach(),无返回值,就是单纯的给数组的每一项执行传入的函数

    const arr = [1, 2, 3]
    const ret = arr.forEach(function(item) {
      // 依次输出数组的每个元素
      console.log(item)
    })
    复制代码
  • map(),返回由每次函数调用结果构成的数组

    const arr = [1, 2, 3]
    const ret = arr.map(function(item) {
      return item * 2
    })
    console.log(ret) // [2, 4, 6]
    复制代码
  • some(),如果有以一项函数返回 true,则返回 ture

    const arr = [1, 2, 3]
    const retTrue = arr.some(function(item) {
      return item >= 3
    })
    console.log(retTrue) // true
    复制代码

every() 和 some() 就像与和或运算符一样,every() 都为 true 结果才为 true,some() 是只要有一个为 true,结果就为 true。

归并方法

ECMAScript 为数组提供了两个归并方法:reduce() 和 reduceRight()。这两个函数的功能和函数签名一样都一样,区别是遍历数组元素的方向不同,reduce() 从前到后,reduceRight,从后向前。

方法的接收两个参数,第一个是每一项都会运行的归并函数,归并函数接收 4 个参数:上一个归并值、当前值、当前项的索引和数组本身,第二个参数是可选的以之为归并起点的初始值。

const arr = [1, 2, 3]
const ret = arr.reduce(function(prev, cur, idx, arrBak) {
  return prev + cur
}, 0)
console.log(ret) // 6
复制代码

定型数组

定型数组(typed array)是 ES6 新增的数据结构,目的是为了提高和原生库的传输效率。JavaScript 本身并没有 TypedArray 类型,它所指的是一种特殊的只包含数值类型的数组。

历史

随着浏览器的流行以及设备性能的提升,大家就想在浏览器上运行更加复杂的 3D 应用程序。于是,就决定在浏览器开发一套原生的 JavaScript API,从而充分利用 3D 图形 API 和 GPU 加速能力在 canvas 元素上渲染复杂的图形。

于是就有了后来的 WebGL,但是由于 JavaScript 数组和原生数组之间不匹配,导致出现了性能问题。图形驱动程序通常不需要 JavaScript 的双精度浮点数,因此,每次将 JavaScript 的数组传递给 WebGL 时,WebGL 都需要在目标环境分配新的数组,然后迭代 JavaScript 数组,将数组元素转换为 WebGL 数组需要的数值格式,这需要耗费大量的时间。

为了解决这个问题,就有了定型数组,定型数组是类似于 C、C++ 语言风格的数值型数组。JavaScript 可以使用这个类型分配、读取和写入数组,这个数组可以直接传递给底层图形驱动程序的 API,也可以直接从底层获取到,不需要中间的转换过程。

ArrayBuffer

ArrayBuffer 是一个普通的 JavaScript 构造函数,用于在内存中分配特定数量的字节。和 C、C++ 中的 malloc 类似。

声明的 ArrayBuffer 必须通过视图(比如 DataView)来读取和写入,视图有不同的类型,但引用的都是存储在 ArrayBuffer 中的二进制数据。

const buf = new ArrayBuffer(16) // 在内存中分配了 16 字节的空间,所有二进制位被初始化位 0
console.log(buf.byteLength) // 16
复制代码

DataView

第一种读取 ArrayBuffer 的视图是 DataView。这个视图转为文件 I/O 和网络 I/O 设计,其 API 对 ArrayBuffer 的数据具有很高的控制能力,但相比于其它的视图性能差一些。DataView 对 ArrayBuffer 没有任何预设,也不能迭代。

以下示例代码一定要认证阅读和学习才能更好的理解这部分的知识

const buf = new ArrayBuffer(16)

// DataView 默认使用整个 ArrayBuffer
const fullDV = new DataView(buf)
// 表示 DataView 在 ArrayBuffer 中的开始位置
console.log(fullDV.byteOffset) // 0
// ArrayBuffer 的字节数
console.log(fullDV.byteLength) // 16
// DataView 实例维护着对 ArrayBuffer 实例的引用
console.log(fullDV.buffer === buf) // true

// 构造函数接口一个可选的字节偏移量和字节长度
// byteOffset = 0 表示 DataView 从 ArrayBuffer 中的起点开始
// byteLength = 8 限制 DataView 为前 8 个字节
const firstDV = new DataView(buf, 0, 8)
console.log(firstDV.byteOffset) // 0
console.log(firstDV.byteLength) // 8
console.log(firstDV.buffer === buf) // true

// 如果不指定 byteLength,默认使用 byteOffset 到结束位置的 ArrayBuffer
const secondDV = new DataView(buf, 8)
console.log(secondDV.byteOffset) // 8
console.log(secondDV.byteLength) // 8
console.log(secondDV.buffer === buf) // true
复制代码

DataView 对存储在 ArrayBuffer 中的数据类型没有预设。它暴露的 API 强制开发者在读、写时指定一个 ElementType(元素类型),然后 DataView 就会为读、写完成相应的转换,这也是 DataView 相较于其它类型性能差的原因。

ElementType

ES6 支持 8 不同的 ElementType

以下内容的阅读必须充分理解每一个 ElementType 表示的数值是什么样的,比如 Int8 表示每个数占 8 位

DataView 为上表中的每个类型都暴露了 get 和 set 方法,这些方法使用 byteOffset 定位要读取或写入值的位置。类型是可以互换使用的。

基础知识:一个字节占 8 位

// 在内存中分配 2 个字节并声明一个 DataView
const buf = new ArrayBuffer(2)
const view = new DataView(buf)

// 说明整个缓冲区的所有二进制为都是 0
// 检查第一个和第二个字节的字符
console.log(view.getInt8(0)) // 0
console.log(view.getInt8(1)) // 0
// 检查整个 ArrayBuffer
console.log(view.getInt16(0)) // 0

// 将整个 ArrayBuffer 都设置为 1
// 255 的二进制表示 11111111 (2^8 - 1)
view.setUint8(0, 255)
// 255 的十六进制表示形式 0xFF
view.setUint8(1, 0xFF)

// 现在,ArrayBuffer 中都是 1
// 如果把它当成有符号整数来读取,则每个字节或者整个 ArrayBuffer 存储的其实是 -1 的二补数
console.log(view.getInt16(0)) // -1
console.log(view.getInt8(0)) // -1
console.log(view.getInt8(1))  // -1
复制代码

如果最后三行的读取不理解一定要动笔写一下,如果忘了 二补数 的计算方式,回头看一下 JavaScript 基础知识 - 上 的位运算部分。

边界情形

DataView 完成读、写操作的前提是 ArrayBuffer 必须有充足的空间,否则会抛出 RangeError。

// 声明一个 4 字节的空间
const buf = new ArrayBuffer(4)
const view = new DataView(buf)
// 读取前 4 个字节,第 0 位 31 位
console.log(view.getInt32(0)) // 0
// 读取后 4 个字节,第 32 位到 63 位,越界了
console.log(view.getInt32(1)) // RangeError
// 写入也一样
view.setInt32(0, 10) // 10
view.setInt32(1, 10) // RangeError
复制代码

DataView 在写入 ArrayBuffer 时会尽力将一个值转为还适当的类型,如果无法转换,则抛出错误。

const buf = new ArrayBuffer(1)
const view = new DataView(buf)

// 存储位 8 位有符号整数,将 1.5 转化为 1
view.setInt8(0, 1.5)
console.log(view.getInt8(0)) // 1

view.setInt8(0, [4])
console.log(view.getInt8(0)) // 4

view.setInt8(0, 'f')
console.log(view.getInt8(0)) // 0

view.setInt8(0, Symbol()) // 无法转换,抛出 TypeError
复制代码

定型数组

定型数组是另一种操作 ArrayBuffer 的视图。虽然概念上和 DataView 接近,但定型数组的区别在于,它特定于一种 ElementType 且遵循系统原生的字节序。

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

由于定型数组的二进制表示对操作系统而言是一种容易使用的格式,JavaScript 引擎可以重度优化算术运算、按位运算和其它对定型数组的常见操作,因此使用它们速度更快。

创建定型数组有以下集中方式:读取已有的 ArrayBuffer、使用构造函数声明、填充可迭代结构,填充基于任意类型的定型数组,以及使用 .from() 和 .of() 方法。

// 创建一个 12 字节的 ArrayBuffer
const buf = new ArrayBuffer(12)
// 创建一个引用该 ArrayBuffer 的 Int32Array
const ints = new Int32Array(buf)
// 这个定型数组的每个元素需要 4 个字节的空间,因此 ints 的长度为 3
console.log(ints.length) // 3

// 通过构造函数创建一个长度为 6 的 Int32Array
const ints1 = new Int32Array(6)
console.log(ints1.length) // 6
console.log(ints1.buffer.byteLength) // 24,每个元素占 4 个字节(32 位)

// 创建一个包含[2, 4, 6, 8] 的 Int32Array
const ints2 = new Int32Array([2, 4, 6, 8])
console.log(ints2.length) // 4
console.log(ints2.buffer.byteLength) // 16
console.log(ints2[1]) // 4

// 通过复制 ints2 创建一个 Int16Array
const ints3 = new Int16Array(ints2)
console.log(ints3.length) // 4
console.log(ints3.buffer.byteLength) // 8,每个元素占 2 个字节(16位)
console.log(ints3[1]) // 4

// 基于普通数组创建 Int16Array
const ints4 = Int16Array.from([2, 4, 6, 8])
console.log(ints4.length) // 4
console.log(ints4.buffer.byteLength) // 8
console.log(ints4[1]) // 4

// 基于传入的参数创建一个 Float32Array
const floats = Float32Array.of(3.14, 2.7, 1.6)
console.log(floats.length) // 3
console.log(floats.buffer.byteLength) // 12,每个元素占 4 个字节
console.log(floats[1]) // 2.700000047683716
复制代码

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

console.log(Int16Array.BYTES_PER_ELEMENT) // 2
console.log(Int32Array.BYTES_PER_ELEMENT) // 4
const ints = new Int32Array(4)
const floats = new Float64Array()
console.log(ints.BYTES_PER_ELEMENT) // 4
console.log(floats.BYTES_PER_ELEMENT) // 8

// 如果定型数组声明时没有用任何值初始化,则其关联的 ArrayBuffer 会以 0 填充
console.log(ints[0], ints[1], ints[2], ints[3]) // 0 0 0 0
复制代码

定型数组的行为

经过上面的示例练习,发现,除了字节、位以及相应的 ElementType的概念之外,定型数组和普通的数组似乎没什么区别。

是这样的,普通数组的操作符、方法、属性定型数组基本上都可以使用,具体的包括如下内容,使用方法和数组一致:

其中返回新数组的方法也会返回包含同样元素类型(element type)的新的定型数组:

const ints = new Int16Array([1, 2, 3])
const doubleInts = ints.map(item => item * 2)
console.log(doubleInts instanceof Int16Array) // true
复制代码

定型数组有一个 Symbol.iterator 符号属性,因此可以通过 for of 循环和扩展运算符来操作:

const ints = new Int16Array([1, 2, 3])
for (const int of ints) {
  // 依次输出 1 2 3
  console.log(int)
}
console.log(Math.max(...ints)) // 3
复制代码

合并、复制和修改定型数组

由于定型数组是使用 ArrayBuffer 来存储数据,而 ArrayBuffer 一旦创建则无法调整其大小。因此,以下会更改数组长度的方法不能用于定型数组

  • concat(),虽然不会更改原数组,但也不能用于定型数组,因此定型数组不具备原生数组的拼接能力,原因是定型数组合并结果不确定。
  • pop()
  • push()
  • shift()
  • splice()
  • unshift()

定型数组提供了两个新方法,可以快速的向外或内复制数据:

  • set(),将提供的数组或定型数组的值复制到当前定型数组中指定的索引位置

    // 第二个参数索引位置可选,默认为 0
    // curTypedArr.set(array or typedArray, idx)
    const arr = [1, 2, 3]
    const ints = new Int8Array(3)
    ints.set(arr, 0) // Int8Array(3) [1, 2, 3]
    // 溢出报错
    // ints.set(arr, 1)
    复制代码
  • subarray(),和 set() 方法相反,从定型数组中复制指定索引范围内的值到新的定型数组,然后返回,接收两个可选参数,开始和结束索引

const source = Int16Array.of(1, 2, 3, 4)
// 拷贝整个定型数组
const fullCopy = source.subarray()
console.log(fullCopy) // Int16Array(4) [1, 2, 3, 4]
// 拷贝指定范围
const halfCopy = source.subarray(1, 3)
console.log(halfCopy) // Int16Array(2) [2, 3]
复制代码

下溢和上溢

定型数组中指定索引位置上的值下溢和上溢不会影响到其它索引上的值,但仍然需要考虑数组的元素应该是什么类型,防止发生溢出。

// 长度为 2 的无符号整数数组,总共 2 个字节(16位)
// 每个元素的表示范围是 0 ~ 255(2^8 - 1)
const uInts = new Uint8Array(2)
// 上溢,取低 8 位
uInts[0] = 256
// 下溢,取 -1 的二补数对应的值
uInts[1] = -1
console.log(uInts) // Uint8Array(2) [0, 255]
复制代码

256 的 二进制表示形式为 1 0 0 0 0 0 0 0 0 => 9位 255 的 二进制表示形式为 1 1 1 1 1 1 1 1 => 8位 -1 的 二进制表示形式为 - 0 0 0 0 0 0 0 1 => 在 Uint8Array 中每个元素占 8 位

上溢只取最低有效位数,下溢自动取值的二补数表示形式对应的值。

除了 8 种元素类型之外,还有一种“夹板”数组类型:Uint8ClampedArray,不允许任何方向的溢出。超出最大值 255 的值会被向下舍入为 255,而小于最小值 0 的值会被向上舍入为 0。

const clampedInts = new Uint8ClampedArray([-1, 256])
console.log(clampedInts) // Uint8ClampedArray(2) [0 255]
复制代码

Uint8ClampedArray 是 HTML5 canvas 元素的历史留存。除非真的做跟 canvas 相关的开发,否则不要使用它。

Map

ES6 之前一般用 Object 来实现键/值存储。Map 是 ES6 新增特性,是一种新的集合类型,为这门语言带来了真正的键/值存储机制。Map 的大多特性都可以通过 Object 类型实现,但二者之间还是存在一些细微的差异。

基本 API

  • 创建映射,new Map(),Map 构造函数也可接收一个可选的可迭代对象

  • size,使用 Map实例的 size 属性获取映射中键值对的数量

  • set(),使用 Map 实例的 set() 方法可以向应设置再添加键/值对,支持链式调用,因为 set() 方法返回 Map 实例

  • has(),查询映射是否存在指定键

  • get(),从映射中获取指定键的值

  • delete(),从映射中删除指定的键/值对

  • clear(),清除映射中的所有键/值对

    // 创建一个空的 Map 实例(映射)
    const m = new Map()
    console.log(m) // Map(0) {}
    // 使用可迭代的二维数组初始化映射
    const m1 = new Map([
      ['key1', 'val1'],
      ['key2', 'val2'],
    ])
    console.log(m1) // Map(2) {"key1" => "val1", "key2" => "val2"}
    // 使用自定义的迭代器初始化映射
    const m2 = new Map({
      [Symbol.iterator]: function* () {
        yield ['k1', 'v1']
        yield ['k2', 'v2']
      }
    })
    console.log(m2.size) // 2
    // 使用 set() 方法添加键/值对
    m2.set('k3', 'v3').set('k4', 'v4')
    console.log(m2.size) // 4
    // has、get
    console.log(m2.has('k3')) // true
    console.log(m2.get('k3')) // v3
    // delete、clear
    console.log(m2.delete('k3')) // 
    console.log(m2.has('k3')) // false
    m2.clear()
    console.log(m2.size) // 0
    // 映射期待的键/值对, 无论是否提供
    const m3 = new Map([[]])
    console.log(m3.has(undefined)) // true
    console.log(m3.get(undefined)) // undefined
    复制代码

与 Object 只能使用数值、字符串、符号作为键不同,Map 可以使用任何 JavaScript 数据类型作为键。Map 内部使用 SameValueZero 比较操作(ECMAScript 规范的内部定义,语言中不能使用),相当于使用严格相等的标准来检查键的匹配性。SameValueZero 比较也会带来一些奇特的现象:

const m = new Map()
const a = 0 / "", b = 0 / "" // NaN
const pz = +0, nz = -0
console.log(a === b) // false
console.log(pz === nz) // true
m.set(a, 'foo')
m.set(pz, 'bar')
console.log(m.get(b)) // foo
console.log(m.get(nz)) // bar
复制代码

顺序和迭代

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

Map 实例可以通过 entries() 方法(或者 Symbol.iterator 属性,它引用 entries()) 取得迭代器。

const m = new Map([
  ['k1', 'v1'],
  ['k2', 'v2']
])

console.log(m.entries === m[Symbol.iterator]) // true

// entries
for (let pair of m.entries()) {
  console.log(pair)
}
// ['k1', 'v1']
// ['k2', 'v2']

// Symbol.iterator
for (let pair of m[Symbol.iterator]()) {
  console.log(pair)
}
// ['k1', 'v1']
// ['k2', 'v2']

// 扩展操作符
console.log([...m]) // [['k1', 'v1'], ['k2', 'v2']]

// forEach
m.forEach((val, key) => console.log([key, val]))
// ['k1', 'v1']
// ['k2', 'v2']

// keys
for (let key of m.keys()) {
  console.log(key)
}
// 'k1'
// 'k2'

// values
for (let val of m.values()) {
  console.log(val)
}
// 'v1'
// 'v2'
复制代码

Object 和 Map 对比

如果你不需要极致的性能,两者都可以,如果需要,则总体来说 Map 的性能更优。

  • 内存占用,Map < Object,相同内存大小的情况下,Map 大约可以比 Object 多存储 50% 的键/值对。

  • 插入性能,Map > Object

  • 查找速度,Map < Object,Object 的的查找速度在某些情况下会比 Map 更好,比如把 Object 当数组使用时,浏览器可以对其进行优化,在内存中使用更高效的布局。这对 Map 来说是不可能的。

  • 删除性能,Map > Object,delete 删除 Object 属性的性能一直以来饱受诟病,之前提到过这个问题,会破坏其快速访问模式提供的性能优化(静态变量降为动态变量),所以一般会降属性值设置为 null 或 undefined 来实现删除。Map 的 delete() 操作都比插入和查找更快。

WeakMap

WeakMap(弱映射)是 ECMAScript 6 新增的集合类型,它是一种增强的键值对存储机制。是 Map 的“兄弟”类型,其 API 也是 Map 的子集。WeakMap 中的 “weak(弱)” 描述的是 JavaScript 垃圾回收程序对待“弱映射”中的键的处理方式。

弱键

WeakMap 中的 "weak" 表示弱映射中的键是 “弱弱的拿着”。意思就是说,这些键不属于正式的引用,不会组织垃圾回收。当引用释放后对应的键值很快就会被垃圾回收。这点和 Map 不同,Map 中的对应项会编程无法直接访问的数据。

API

WeakMap 的 API 是 Map 的一个子集,除了以下内容之外 和 Map 没有任何区别:

  • WeakMap 的键必须是引用类型,如果提供非引用的类型的键会导致整个初始化失败

  • WeakMap 不可迭代,因为 WeakMap 中的键值对在任何时候都可能被销毁,所以没必要提供迭代能力。当然,clear() 也不存在。因为不可迭代,所以如果没有键的引用,则无法从弱映射中取的对应的值,即便代码可以访问 WeakMap 实例,也没办法取得其中的内容(Map 可以通过 迭代的方式查看)。

应用场景

WeakMap 实例和现有 JavaScript 对象有着很大不同。我觉得它的主要使用场景就是:你定义的一些数据的存在或销毁需要由它的关联对象的存在与否来决定。比如 它在 Vue3 响应式原理中的使用,以及下面这个例子。

DOM 节点元数据

因为 WeakMap 实例不会阻止来及回收,所以非常适合保存关联元数据。

const m = new Map()
const loginButton = document.querySelector('#login')
// 给这个节点关联一些元数据
m.set(loginButton, { disabled: true})
复制代码

如果上面代码执行后页面中的登陆按钮被移除了,但由于 Map 映射中还保存着对按钮的引用,所以对应的 DOM 节点仍然逗留子啊内存中得不到释放,除非明确将其从映射中删除或者映射本身被销毁。

但如果这里使用弱映射就没这个问题了,当 DOM 节点被删除以后,垃圾回收程序就可以立即释放其内存。

const wm = new WeakMap()
const loginButton = document.querySelector('#login')
// 给这个节点关联一些元数据
wm.set(loginButton, { disabled: true})
复制代码

Set

Set(集合)是 ECMAScript 6 新的一种集合类型,一种新的集合数据结构。Set 很像加强版的 Map,它们大多数 API 和行为都是共有的。

基本 API

  • new Set(),创建集合,接收一个可选的可迭代对象作为构造函数的参数

  • ins.size,获取集合中元素的个数

  • ins.add(xx),向集合中添加新元素,返回集合实例(支持链式调用)

  • ins.has(xx),判断集合中是否存在指定元素

  • ins.delete(xx),从集合中删除指定元素,返回一个布尔值,表示集合中是否存在要删除的值

  • ins.clear(),清空集合

const s = new Set(['v1', 'v2'])
console.log(s.size) // 2
s.add('v3')
console.log(s) // Set(3) {"v1", "v2", "v3"}
console.log(s.has('v3')) // true
s.delete('v3')
console.log(s.has('v3')) // false
s.clear()
console.log(s.size) // 0
复制代码

集合还有一个常用的功能就是去重,使用 SameValueZero 操作,基本上相当于使用严格相等的标准来判断元素的匹配性

const s = new Set(['v1', 'v2', 'v1', 'v2'])
console.log(s) // Set(2) {"v1", "v2"}
复制代码

顺序和迭代

Set 会维护值插入时的顺序,所以支持按顺序迭代。可以通过集合实例上的迭代器来按顺序迭代集合中的内容。

通过 values() 方法及其别名 keys() 或者 Symbole.iterator 属性(它引用 values()) 取得迭代器。

扩展操作符的语言很简洁,但尽可能避免集合和数组之间的相互转换能够节省对象初始化成本。

const s = new Set([1, 2])
console.log(s.values === s[Symbol.iterator]) // true
console.log(s.keys === s[Symbol.iterator]) // true
for (const v of s.values()) {
  console.log(v)
}
// 1
// 2
for (const v of s[Symbol.iterator]()) {
  console.log(v)
}
// 1
// 2
console.log([...s]) // [1, 2]
for (const pair of s.entries()) {
  console.log(pair)
}
// [1, 1]
// [2, 2]
s.forEach((val, key) => {
  console.log(key, ' => ', val)
})
// 1 => 1
// 2 => 2
复制代码

WeakSet

WeakSet(弱集合)也是 ECMAScript 6 新增的集合类型,和 WeakMap 类似,WeakSet 中的 "weak" 描述的是 JavaScript 垃圾回收程序对待 “弱集合” 中值的方式。

基本 API

WeakSet 的 API 是 Set 的一个子集,除了以下内容之外 和 Set 没有任何区别:

WeakSet 的键必须是引用类型,如果提供非引用的类型的键会导致整个初始化失败

WeakSet 不可迭代,因为 WeakSet 中的值在任何时候都可能被销毁,所以没必要提供迭代能力。当然,clear() 也不存在。因为不可迭代,所以如果没有引用对象,则无法从弱集合中取得值,即便代码可以访问 WeakSet 实例,也没办法取得其中的内容。

弱值

WeakSet 中的 "weak" 表示弱集合的值是 “弱弱地拿着”。意思是,这些值不属于正式的引用,不会阻止垃圾回收。也就是说如果集合中的对象如果没有其它引用存在,则会被当作垃圾回收,然后这个对象就会从弱集合中消失。

应用场景

应用场景和 WeakMap 一样。

链接

文章分类
前端
文章标签