最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • [JS犀牛书英文第七版笔记]9. Set,Map 和 Typed Array

    正文概述 掘金(小诸不是小猪)   2021-02-20   490

    GitHub 链接:javascript-the-definitive-guide

    上一章链接:Node 和 JavaScript 的模块化

    JavaScript 标准库(The JavaScript Standard Library)

    某些数据类型,例如数字、字符串、对象、数组,它们都是 JavaScript 中的基础,我们可以将它们认为是 JavaScript 语言本身的一部分。在这一章中,许多其他的很重要的,但是并不是 JavaScript 基础的 APIs。它们被定义成了 JavaScript 的标准库(Standard library):JavaScript 的内嵌的一些类和函数,在浏览器和 Node 中都可用。其中主要包括了:

    • Set 和 Map 类,用来用来表示值的集合和两个集合之间的映射
    • TypedArrays,是一种用数组表示二进制数据的类数组对象
    • Regular expression 和 RegExp 类,用于表示文本的格式
    • Date 类用于表示日期和时间
    • Error 类和其多样的子类,JavaScript 抛出的异常就是 Error 类的实例
    • JSON 对象,用于构建和解析 JavaScript 数据解构
    • Intl 对象,用其定义的类可以用于本地化 JavaScript 程序
    • Console 对象,其方法可以返回字符串用于 debug 和 log
    • URL 类,使解析和修改 url 变得更为简单
    • setTimeout() 和其他相关的函数,用于控制函数的执行时间

    Set 和 Map(Sets and Maps)

    JavaScript 的对象是一个十分灵活的数据类型,它可以用于将值映射到字符串上(属性名)。如果我们忽略它的值,那么对象就成了字符串的集合了。不过这样的话,仍然会有些许限制。比如对于字符串有限制(某些符号不能使用);又比如他依然会从对象的原型对象上继承某些不必要的方法。

    所以在 ES6 中,真正的 Set 和 Map 类被加入了。

    Set 类

    set 和数组一样是值的集合,不过它和数组不同,set 没有顺序和索引,而且不能有重复的值:对一个值,我们只知道他是否在 set 内,而无法判断它出现了几次。

    我们可以通过 Set() 构造函数来创建一个 Set 对象:

    let s1 = new Set(); // 一个新的空 set
    let s2 = new Set([1,s1]); // 一个拥有两个元素的 set
    

    对于构造函数 Set() 的 argument,他并不一定需要一个数组,任何可遍历对象(包括 Set 对象)都可以成为 argument:

    let s3 = new Set(s1); // 一个新的,复制了 s1 中每一个元素的 set
    let s4 = new Set('abbccccdddd'); // 四个元素的 set,包括 "a", "b", "c", "d"
    

    set 拥有一个 size 属性,等同于数组的 length 属性,返回 set 中的元素数量:

    s4.size // 4
    

    在创建 set 时,我们并不一定要同时进行初始化,我们可以新增或者移除其中的元素使用 set 上的方法。不过要记住的是,因为 set 不包含重复元素,所以在加入重复元素时并不会产生效果:

    let s = new Set(); // 新建空 set
    s.size; // 0
    s.add(1); // 加入一个数字作为元素
    s.size // 1
    s.add([1,2,3]); 加入一个数组作为元素
    s.size // 2
    s.delete(1) // true,删除成功 
    s.size // 1
    s.delete(4) // false,没有 4,删除失败
    s.delete([1,2,3]); // false,set 中数组的引用不同
    s.size // 1
    s.clear(); // 删除所有元素
    s.size // 0
    

    需要注意的是:

    • add() 方法只能接受一个 argument,即使传入数组,也只不过是将数组直接加入 set 作为元素。add() 的返回值是调用其的 set,所以可以使用 chained 方法调用:

      s.add(1).add(2).add(3);
      
    • delete() 方法也只能接受一个 argument,然后删除一个元素,不过返回值是一个布尔值,用来表示是否成功删除

    • set 的元素是通过全等 === 来判断的,一个 set 可以同时包含 1 和 '1',因为它们并不全等。对于对象也是通过全等进行比较的,只两个引用着同一个对象的值才可以成为全等

    set 还有一个很重要的方法是使用 has() 来判断其是否包含一个值:

    let s = new Set([1,2,3,4]);
    s.has(2) // true
    s.has('2') // false
    s.has(5) // false
    

    因为 set 不存在重复的元素,所以其 has() 方法的运行速度是被优化过的。has() 会比 includes() 快很多。所以使用数组来表示 set 对象会比使用 Set 来慢得多。

    Set 类也是可遍历的,所以可以使用 for of 来遍历其所有元素,也可以使用展开操作符来将其转换成数组。虽然 set 通常被定义为未经排序的集合,但是 JavaScript 的 Set 类其实记住 Set 元素加入时的顺序,然后 以此作为遍历顺序。除了使用 for of 之外,Set 对象上也有 forEach() 方法用于遍历:

    let s = new Set([1,2,3,4]);
    let sum = 0, product = 1;
    for (let element of s) { // 使用 for of 遍历 set
      sum += element
    }
    sum // 10
    s.forEach(element => { // 使用 Set 上的 forEach 方法遍历
      product *= element;
    });
    product // 24
    

    Map 类

    Map 对象表示了值的集合,这些值被称为 keys。每一个 key 会对应另一个值。简单地说,map 就像是一个数组,只不过使用了 keys 来代替索引。和数组一样,查找一个值的速度很快(但不如数组),而且和 map 大小无关。我们可以使用 Map() 构造函数来创建一个新的 map:

    let m1 = new Map(); // 创建一个新的空 map
    let m2 = new Map([ // 使用字符串作为 key,数字作为值的 map
      ['a',1],
      ['b',2]
    ]);
    

    Map() 构造函数的可选 argument 需要是一个包含两个元素 [key, value] 的可遍历对象,或者一个其他的 map:

    let m3 = new Map(m2); //  复制另一个 map
    let o = { x:1, y:2}; // 两个属性的对象
    let m4 = new Map(Object.entries(o)); // 这样也可以,因为 Object.entries 返回可遍历的 [key, value]
    

    Map 对象上也有许多好用的方法:

    let m = new Map(); // 创建新的空 map
    m.size // 0 没有 key
    m.set('a',1); // 加入 key 为 'a',value 为 1 的 pair
    m.set('b',2); // 加入 key 为 'b',value 为 2 的 pair
    m.size // 2 有两个 keys
    m.get('b'); // 1,取得 key 'b' 所对应的值
    m.get('c'); // undefined
    m.has('b'); // true,判断是否有 key 'b'
    m.has(2); // false,只能判断 key,不能判断值
    m.delete('a') // true,删除 key 'a' 和其的值
    m.delete('a') // false,'a' 已经被删除了
    m.clear() // 删除所有元素
    

    和 set 一样,所有 JavaScript 的值都可以成为 key 或者 value,这也包含了 null、undefined、NaN 和所有引用类型(如对象)。使用 has() 和 get() 时也是通过全等 === 进行比较的。

    Map 对象也是可遍历的,每一个遍历值会返回 [key, value]。对 Map 对象使用展开操作符会得到类似于我们传入构造函数那样的 array of arrays:

    let m = new Map([["x", 1], ["y", 2]]);
    [...m] //  [["x", 1], ["y", 2]]
    for (let [key, value] of m) { // 使用 for of 遍历 map,顺序也是插入顺序
      ...
    }
    m.forEach((value, key) => { // 使用 Map 上的 forEach 方法遍历,顺序也是插入顺序
      ... // 注意,value 是第一个参数,而 key 是第二个
    });
    

    如果我们只想遍历 map 的 keys 或着 values 可以使用 keys() 或 valuse() 方法,它们会返回只包含 keys 或者 values 的可遍历对象。(还有 entries() 方法用于返回 key value pair):

    let m = new Map([["x", 1], ["y", 2]]);
    m.keys(); // MapIterator {"x", "y"}
    m.values(); // MapIterator {"1", "2"}
    m.entries(); // MapIterator {"x" => 1, "y" => 2}
    // 可以对可遍历对象使用展开操作符 ... 
    [...m.keys()] // ["x", "y"]
    [...m.values()] // ["1", "2"]
    [...m.entries()] // [["x", 1], ["y", 2]]
    

    WeakMap 类和 WeakSet 类

    WeakMap 类是 Map 类的变种(而非子类)。他不会阻止它的键和值被垃圾回收程序(Garbage collection)回收。垃圾回收是一个 JavaScript 编译器重新取回不再可以被使用的内存的过程。普通的 amp 会对器键值持有强引用,所以只要 map 存在,键值就一定存在,不论是否有其他的地方在引用他们。WeakMap,则只拥有‘弱’引用,如果键值不再 reachable,他们的存在便不再重要,会被垃圾回收掉。

    WeakMap() 构造函数和 Map() 构造函数十分接近,不过:

    • WeakMap 的 keys 必须是对象或者数组;因为基本类型并不在垃圾回收范围之内
    • WeakMap 不可遍历,没有定义 keys()、values()、entries()、forEach()方法。因为如果它可以被遍历,它的 keys 就会被使用
    • 同理,WeakMap 也不用有 size 属性,因为 size 属性会在垃圾回收后改变

    使用 WeakMap 的主要目的就是为了防止内存泄漏(通常会在缓存时使用)。

    WeakSet 对于 Set 的改变基本等同于 WeakMap 对于 Map 的改变。

    类型化数组和二进制文件(Typed Arrays and Binary Data)

    一般的 JavaScript 数组可以拥有任何类型的元素,并且长度也是动态的。JavaScript 的实现对它们进行了很多的优化,所以一般情况下都非常快。尽管如此,JavaScript 的数组和更底层语言(如 C 或 Java)中的数组类型还是有很大区别的。

    类型化数组(Typed arrays) 使在 ES6 引入的新特性。它们更接近于底层语言。Typed arrays 从技术层面上讲其实并不是数组(Array.isArray() 会返回 false),但是它们拥有所有数组所拥有的方法。Typed arrays 和普通数组的区别主要是:

    • typed array 的元素均为数字,并且可以指定数字的类型(signed integer、unsigned integer、IEEE-754 浮点数)和长度( 8 bits 到 64 bits)。
    • 在创建 typed array 时,必须指定其长度;typed array 的长度在定义以后不能再改变
    • 在创建 typed array 时,所有的元素都被初始化为 0

    Typed Array 类型

    JavaScript 并没有定义 typed array 类,其原因是这一共有 11 种不同的 typed array,他们拥有不同的元素类型和构造函数:

    • 构造函数 --- 数字类型
    • Int8Array() --- signed bytes
    • Uint8Array() --- unsigned bytes
    • Uint8ClampedArray() --- unsigned bytes without rollover
    • Int16Array() --- signed 16-bit short integers
    • Uint16Array() --- unsigned 16-bit short integers
    • Int32Array() --- signed 32-bit integers
    • Uint32Array() --- unsigned 32-bit integers
    • BigInt64Array() --- signed 64-bit BigInt values (ES2020 新增)
    • BigUint64Array() --- unsigned 64-bit BigInt values (ES2020 新增)
    • Float32Array() --- 32-bit floating-point value
    • Float64Array() --- 64-bit floating-point value (也就是 JavaScript 中的 Number)

    [JS犀牛书英文第七版笔记]9. Set,Map 和 Typed Array

    由 Int 开始命名的类型是 1,2,4 字节(8,16,32 位)的有符号整数的数组;由 Uint 开始命名的则是同长度的正整数的数组;而 BigInt 类型则是拥有 8字节(64位),表示了 JavaScript 中 BigInt 值的数组;Float64Array 的元素就是 JavaScript 中的数字;Float32Array 中的元素则拥有更低的进度和更短的长度(C 和 Java 中的 float);Unit8ClampedArray 则是 Uint8Array 的变种,它们都可以用来表示 0 到 255 的值。如果想起存储了大于 255 或者小于 0 的值时,Uint8Array 会进行回绕(wraps around),所以会得到不一样的值;而 Uint8ClampedArray 则会进行类型检验,若大于 255 或小于 0 则会编程 255 或 0(这会在 canvas 的底层 API 操作像素颜色时被用到)。

    每一个构造函数都拥有 BYTES_PER_ELEMENT 的属性用来表示其字节数。

    创建 Typed Array

    创建 typed array 最简单的方法就是调用其构造函数,并传入长度作为 argument:

    let bytes = new Uint8Array(1024); // 1024 字节
    let matrix = new Float64Array(9); // 3x3 矩阵
    let point = new Int16Array(3); // 3D 中的一个点
    let rgba = new Uint8ClampedArray(4); // 4 字节的 RGBA 像素值
    let sudoku = new Int8Array(81); // 9x9 的数独
    

    创建完成后,其中的属性都被保证初始化为 0.0 或者 0.0n(BigInt)。如果在构造时就已经知道想要传入的值的话,也可以直接定义值。每一个 typed array 的构造函数都拥有 from() 和 of() 的静态方法,使用起来和数组的一样:

    let white = Uint8ClampedArray.of(255,255,255,0); // RGBA 中的 opaque white
    let white2 = Uint32Array.from(white); // 转换成 32 位整数
    

    最后,这还有一种创建 typed array 的方法,这需要使用到 ArrayBuffer 类。一个 ArrayBuffer 是一个指向内存的不透明引用。我们可以使用构造函数来创建:

    let buffer = new ArrayBuffer(1024*1024);
    buffer.byteLength // 1024*1024,一兆内存
    

    我们并不能在 ArrayBuffer 类上直接进行字节的读写,不过我们可以在其内存中创建 typed array,然后对内存进行读写。我们只需要在调用 typed array 构造函数时传入 ArrayBuffer 作为第一个argument,array buffer 中 一个字节的 offset 作为第二个 argument,typed array 的长度作为第三个 argument 就可以了。第二个和第三个的 argument 是可选的,如果全部省略,则会用 array buffer 中所有内存创建该 typed array;如果省略第三个,他会从创建从 start index 到 end 中所有剩余内存的 typed array。

    使用 Typed Array

    在 typed array 创建完成后,我们可以对其进行读写,就如同我们读写数组一样。只不过读写速度和内存使用率会根据你定义的 typed array 而大大缩小。虽然 typed array 并不是数组,不过它们重新实行了几乎所有数组的方法。

    英文 typed array 的长度是固定的,所以 length 是一个只读的值,然后会改变长度的方法也没有被实现。(例如 push(), pop(), shift(), splice())

    Typed Array 的属性和方法

    除去基础的数组方法之外,typed array 还实现了一些它们私有的方法。set() 方法可以设置多个 typed array 的元素:

    let bytes = new Uint8Array(1024); // 一个 1k 的 buffer
    let pattern = new Uint8Array([1,2,3,4]); // 一个四字节的数组
    bytes.set(pattern); // 将 bytes 的开始的字节改成 pattern 中的字节
    bytes.set(pattern, 4); // 将 bytes 的从 offset(第二个 argument)开始的字节改成 pattern 中的字节
    bytes.slice(0,10); // 返回新的 typed array [1,2,3,4,1,2,3,4,0,0]
    

    typed array 还实现了一个 subarray() 的方法,它的 arguments 和 slice() 的一样,用法也十分酷似。不过它们是有区别的。 slice() 会返回一个新的,独立的 typed array。这意味着它不会和其原来的 typed array 公用同一段内存。而 subarray() 则不同,它不会在内存中创建新的 typed array,而是直接返回其原数组中的那一段的引用:

    let a = new Int16Array([1,2,3,4,5]); // 五个 short integer
    let aSub = a.subarray(1,4); // [2,3,4]
    let aSlice = a.slice(1,4); // [2,3,4]
    
    a[2] = 33; // a = [1,2,33,4,5]
    aSub; // [2,33,4] subarray 会根据原数组的值而改动
    aSlice // [2,3,4] slice 则不会
    

    每一个 typed array 都拥有三个关于其存在的 buffer 的属性:

    aSub.buffer // ArrayBuffer 对象用于储存 typed array
    aSub.buffer === a.buffer // 指向同一个 buffer
    aSub.byteOffset // 2,subarray 的开始位置
    aSub.byteLength // 6,subarray 的字节数(三个 16 位整数,每一个使用 2 字节)
    aSub.buffer.byteLength // 10 底层的 buffer 有 10 字节
    a.length * a.BYTES_PER_ELEMENT === a.byteLength // true,根据长度和元素字节数计算字节长度
    

    字节序和 DataView 类

    出于性能考虑,typed array 会使用其底层硬件的原生字节序(endianness)。而字节序有两种:

    • little-endian,typed array 的字节会从低位到高位进行排序
    • big-endian,typed array 的字节会从高位到低位进行排序

    目前大多是 CPU 是使用 little-endian 的,但是许多网络协议或者二进制文件则需要 big-endian 排序。在使用从网络上下载的 typed array 表示的文件时,你并不知道其字节序,这时候我们就需要用到 DataView 类了。它可以让我们指定读写的顺序:

    // 假设我们有一个名为 bytes 的 typed array
    let view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
    let int1 = view.getInt32(0); // 根据 big-endian 从第 0 个字节读取第一个整数
    let int2 = view.getInt32(4,false); // 根据 big-endian 从第 4 个字节读取第二个整数
    let int3 = view.getInt32(8,true); // 根据 little-endian 从第 8 个字节读取第三个整数
    view.setInt32(8,int,false); // 以 big-endian 写入 
    

    DataView 对 10 种 typed array 各定义了一个 get 方法,名为 getInt16(), getUint32(), getBigInt64() 等等。第一个 argument 是其 byte offset;除去 getInt8() 和 getUint8() 之外,所有方法还有第二个可选的布尔值 argument,若为 true,用 little-endian 顺序,若为 false,用 big-endian 顺序。

    DataView 对 10 种 typed array 也各定义了一个 set 方法。第一个 argument 是写入的开始的 offset;第二个 argument 是写入的值;除去 getInt8() 和 getUint8() 之外,所有方法还有第三个可选的布尔值 argument,若为 true,用 little-endian 顺序,若为 false,用 big-endian 顺序。

    typed arrays 和 DataView 类给了我们在解压 ZIP 文件,或从 JPEG 文件提取元数据的所有必须的工具。


    起源地下载网 » [JS犀牛书英文第七版笔记]9. Set,Map 和 Typed Array

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元