Set & Map

Proxy

Reflect

Promise

Iterator

核心理解:ITERATOR 为数据结构提供了一个统一的访问机制的接口。

1. 简要概述

  1. 解释:Iterator(遍历器)是一种为各种不同的数据结构提供统一的访问机制的接口。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。

  2. Iterator 的作用

    • 为各种数据结构,提供一个统一的、简便的访问接口
    • 使得数据结构的成员能够按某种次序排列
    • for...of 循环提供支持
  3. Iterator 的遍历原理:Iterator 是一个指针对象,最初指向数据结构的起始位置。它具有一个 next 方法。每次调用 next 方法时,指针会移动到下一个成员,并返回一个对象,其中包含两个属性:value 表示当前成员的值,done 是一个布尔值,用于指示遍历是否已结束。

  4. Iterator 接口的类型定义

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    interface Iterable<T> {
    [Symbol.iterator](): Iterator<T>;
    }

    interface Iterator<T> {
    next(value?: any): IterationResult<T>;
    }

    interface IterationResult<T> {
    value: T;
    done: boolean;
    }

2. Iterator 接口

2.1 Symbol.iterator

  1. 解释:只要一个数据结构具有 Symbol.iterator 属性,就可以被视为“可遍历的”(iterable),这意味着该数据结构部署了 Iterator 接口Symbol.iterator 属性是一个函数,它是该数据结构默认的迭代器生成函数。调用这个函数会返回一个迭代器。

  2. 遍历方式:对于一个可遍历的数据结构 ITERABLE,有以下两种方式遍历其中的数据成员,

    • for...of

      1
      2
      3
      for (let x of ITERABLE) {
      console.log(x);
      }
    • while

      1
      2
      3
      4
      5
      6
      7
      var $iterator = ITERABLE[Symbol.iterator]();
      var $result = $iterator.next();
      while (!$result.done) {
      var x = $result.value;
      console.log(x);
      $result = $iterator.next();
      }
  3. 原生部署 Iterator 接口的数据结构:Array、Map、Set、String、TypedArray、函数的 arguments 对象、NodeList 对象

    对象没有原生部署 Iterator 接口,因为对象的数据是无序的。因此,给对象部署遍历器接口,就相当于部署一种线性变换

    for...of 遍历字符串时,甚至会正确识别 32 位的 UTF-16 字符。

    1
    2
    3
    for (let x of 'a\uD83D\uDC0A\u9CC4\u9C7C') {
    console.log(x); // "a","🐊","鳄","鱼"
    }
    1
    2
    3
    4
    5
    6
    const arr = [1, 2, 3];
    const iterator = arr[Symbol.iterator]();
    console.log(iterator.next()); // { "value": 1, "done": false }
    console.log(iterator.next()); // { "value": 2, "done": false }
    console.log(iterator.next()); // { "value": 3, "done": false }
    console.log(iterator.next()); // { "value": undefined, "done": true }

2.2 接口部署

  1. 解释:为了让数据结构是可遍历的,就要为其部署 Iterator 接口,即在数据结构的 Symbol.iterator 属性上添加遍历器生成方法(原型链上添加也奏效)。

  2. Iterator 接口部署示例

  • 对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    let obj = {
    data: [ 'hello', 'world' ],
    [Symbol.iterator]() {
    const self = this;
    let index = 0;
    return {
    next() {
    if (index < self.data.length) {
    return {
    value: self.data[index++],
    done: false
    };
    }
    return { value: undefined, done: true };
    }
    };
    }
    };
  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    class RangeIterator {
    constructor(start, stop) {
    this.value = start;
    this.stop = stop;
    }

    [Symbol.iterator]() { return this; }

    next() {
    var value = this.value;
    if (value < this.stop) {
    this.value++;
    return {done: false, value: value};
    }
    return {done: true, value: undefined};
    }
    }
  • 类数组对象

    1
    2
    3
    4
    5
    6
    7
    let iterable = {
    0: 'a',
    1: 'b',
    2: 'c',
    length: 3,
    [Symbol.iterator]: Array.prototype[Symbol.iterator]
    };

    如果一个对象存在数值键名和 length 属性,就称之为类数组对象,此时部署 Iterator 接口时可以直接引用数组的 Iterator 接口

  1. 使用说明

    • 普通对象部署数组的 Symbol.iterator 方法,并无效果。
    • 如果 Symbol.iterator 方法对应的不是遍历器生成函数(即会返回一个遍历器对象),解释引擎将会报错。

2.3 调用场合

  1. 数组和 Set 的解构赋值

    1
    2
    3
    4
    5
    const arr = [1,2];
    const [x, y] = arr; // x = 1, y = 2

    const set = new Set(['hello', 'world']);
    const [a, b] = set; // a = "hello", b = "world"
  2. 扩展运算符 ...(只要某个数据结构部署了 Iterator 接口,就可以对它使用扩展运算符,将其转为数组)

    1
    2
    3
    4
    5
    const str = 'hello world';
    console.log([...str]); // ["h", "e", "l", "l", "o", " ", "w", "o", "r", "l", "d"]

    const arr = [1, 2, 3];
    console.log([0, ...arr, 4]); // [0, 1, 2, 3, 4]
  3. yield*

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    const generator = function* () {
    yield 1;
    yield* [2, 3, 4];
    yield 5;
    };

    const iterator = generator();

    console.log(iterator.next()); // { "value": 1, "done": false }
    console.log(iterator.next()); // { "value": 2, "done": false }
    console.log(iterator.next()); // { "value": 3, "done": false }
    console.log(iterator.next()); // { "value": 4, "done": false }
    console.log(iterator.next()); // { "value": 5, "done": false }
    console.log(iterator.next()); // { "value": undefined, "done": true }
  4. 数组的遍历(包括以数组作为参数的场合)

    • for ... of 循环

    • Array.from()

    • Map(), Set(), WeakMap(), WeakSet()

    • Promise.all()Promise.race()

    • ······

2.4 几个概念

Symbol.iterator、Iterator 接口、可遍历的、for...of

  • 数据结构具有 Symbol.iterator 属性,亦即数据结构部署了 Iterator 接口,亦即数据结构是可遍历的,亦即数据结构可以使用 for...of 遍历其数据成员。
  • for...of 循环内部调用的是数据结构的 Symbol.iterator 方法。

3. Iterator 结构

  1. 解释:Iterator 本质上是一个对象,必须包含 next() 方法,可选包含 return()throw() 方法。

    • next():用于遍历数据结构中的所有数据成员,返回值是一个对象,其结构为 {value: any, done: boolean},其中 value 表示当前遍历的数据成员,done 表示数据结构是否遍历完毕。
    • return():当 for...of 循环因为 breakthrow 语句提前退出时,就会调用该方法。
    • throw():主要配合 Generator 函数使用,详见下述关于 Generator 函数的笔记。
  2. 示例

    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
    const iterableObj = {
    data: [1, 2, 3, 4, 5],
    [Symbol.iterator]() {
    const self = this;
    let index = 0;
    let value: number;
    return {
    next() {
    if (index < self.data.length) {
    value = self.data[index++];
    console.log(`正在遍历数据 ${value}`);
    return { value, done: false };
    } else {
    console.log('数据结构遍历结束');
    return { value: undefined, done: true };
    }
    },
    return() {
    console.log(`return 方法在遍历 ${value} 时被调用了`);
    return { value }
    }
    }
    }
    }

    for (let value of iterableObj) {
    // "正在遍历数据 1"
    console.log(value); // 1
    break; // "return 方法在遍历 1 时被调用了"
    }

    try {
    for (let value of iterableObj) {
    // "正在遍历数据 1"
    console.log(value); // 1
    throw new Error('error in for...of'); // "return 方法在遍历 1 时被调用了"
    }
    } catch (e:any) {
    console.log(e.message); // "error in for...of"
    }

    注意:如果在 for...of 循环中,由于 throw 语句导致调用了可迭代对象的 Iterator 的 return() 方法,那么会先执行 return() 方法,然后再抛出错误。

4. 生成数据结构

数组、Set、Map 都具有以下方法,调用后返回一个遍历器,用于遍历生成数据结构的数据成员。

  • entries():返回一个遍历器对象,用于遍历形如 [键名,键值] 的数组。对于数组,每次遍历的是 [索引值,元素值];对于 Set,每次遍历的是 [元素值,元素值];对于 Map,每次遍历的是 [键名,键值]
  • keys():返回一个遍历器对象,用于遍历所有的键名。
  • values():返回一个遍历器对象,用于遍历所有的键值。

5. 遍历语法比较

这里展示其他循环的缺点,与 for...of 循环的优点。

  1. for 循环

    • 写法比较麻烦
  2. 数组的 forEach 方法

    • 无法中途跳出 forEach 循环
  3. for...in 循环(用于遍历键名)

    • 遍历数组时,遍历的键名是索引数字对应的字符串

      1
      2
      3
      4
      const arr = ['boo', 'foo', 'zoo'];
      for(let idx in arr){
      console.log(idx); // '0', '1', '2'
      }
    • 会遍历原型上的键名

      1
      2
      3
      4
      5
      6
      7
      8
      9
      const obj1 = {
      name:'Jack',
      age:19
      }
      const obj2 = Object.create(obj1);
      obj2.gender = 'Male';
      for(let key in obj2){
      console.log(key); // "gender", "name", "age"
      }
    • 会以任意顺序遍历键名

  4. for...of 循环(用于遍历可遍历对象的数据成员)

    • 简洁的语法
    • 有序遍历
    • 可中途跳出循环(与 breakcontinue 配合使用)
    • 遍历所有数据结构的统一接口

Generator 函数

核心理解:GENERATOR 的执行结果是 ITERATOR,同时这个 ITERATOR 可以看作是 GENERATOR 的实例。

1. 简要概述

  1. 解释:Generator 函数是 ES6 提供的一种异步编程解决方案,可以从以下两种角度来理解,

    • 语法上
      • Generator 函数是一个状态机,封装了多个内部状态。
      • Generator 函数是一个遍历器生成函数,执行后返回一个遍历器,可以依次遍历 Generator 函数内部的每一个状态。
    • 形式上
      • Generator 函数使用 function* 来定义,即 function 关键字和函数名之间有一个星号 *
      • Generator 函数内部使用 yield 表达式定义不同的内部状态。
  2. Generator 函数的执行流程

    • 调用 Generator 函数时,该函数不会立即执行,而是返回一个遍历器对象,该对象是指向函数内部的指针。

    • 每次调用遍历器对象的 next() 方法时,Generator 函数会从上次暂停的地方继续执行,直到遇到 yield 表达式或 return 语句。next() 方法返回一个对象,其结构为 {value: any, done: boolean}。其中,value 表示 Generator 函数当前执行位置的值(即 yieldreturn 后的表达式的值),而 done 是一个布尔值,指示遍历是否已完成。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      function* helloWorldGenerator() {
      yield 'hello';
      yield 'world';
      return 'ending';
      }

      var hw = helloWorldGenerator();
      console.log(hw.next()); // { "value": "hello", "done": false }
      console.log(hw.next()); // { "value": "world", "done": false }
      console.log(hw.next()); // { "value": "ending", "done": true }
      console.log(hw.next()); // { "value": undefined, "done": true }
  3. yield 表达式:基于上述内容可知,Generator 函数是一种可以暂停执行的函数,而 yield 表达式就是暂停标志

    • 惰性求值(Lazy Evaluation):yield 表达式后的表达式只有当调用 next() 方法、内部指针指向该语句时才会执行。

    • 暂缓执行函数:如果 Generator 函数中不使用 yield 表达式,那么只有调用该函数返回的遍历器对象的 next() 方法后,该函数的内部代码才会执行。

    • 使用说明

      • yield 表达式只能用在 Generator 函数里面。

      • yield 表达式如果用在另一个表达式之中,必须放在圆括号里面。

        1
        2
        3
        function* demo() {
        console.log('Hello' + (yield 123));
        }
      • yield 表达式用作函数参数或放在赋值表达式的右边,可以不加括号。

        1
        2
        3
        4
        function* demo() {
        foo(yield 'a', yield 'b');
        let input = yield;
        }
  4. 使用说明

    • 可以把 Generator 函数赋值给对象的 Symbol.iterator 属性,从而使得该对象具有 Iterator 接口,使其成为可遍历对象。

    • Generator 函数执行后,返回一个遍历器对象。该对象本身也具有Symbol.iterator 属性,执行后返回自身。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      function* numbers() {
      yield 1;
      yield 2;
      yield 3;
      }

      const iterator = numbers();

      console.log(iterator[Symbol.iterator]() === iterator); // true
    • 在使用 for...of 循环遍历 Generator 函数返回的遍历器时,遍历结果不包括 Generator 函数中 return 语句返回的值。return 语句仅用于标识迭代的结束,而其返回的值不会被 for...of 循环捕获。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      function* numbers() {
      yield 1;
      yield 2;
      return 3;
      yield 4;
      }

      console.log([...numbers()]); // [1, 2]

      console.log(Array.from(numbers())); // [1, 2]

      const [x, y] = numbers();
      console.log(x, y); // 1, 2

      for (let x of numbers()) {
      console.log(x) // 1, 2
      }

      所有与 for...of 遍历原理相同的语法,都会得到相同的结果。

    • 如果一个对象的属性是 Generator 函数,可以简写成下面的形式。

      1
      2
      3
      4
      5
      let obj = {
      * myGeneratorMethod() {
      ···
      }
      };
    • Generator 函数执行返回的遍历器是 Generator 函数的实例,继承了 Generator 函数的 prototype 对象上的方法。

2. next 方法的参数

  1. 解释:yield 表达式本身没有返回值,或者说总是返回 undefinednext 方法可以带一个参数,该参数就会被当作上一个 yield 表达式的返回值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function* foo(x) {
    var y = 2 * (yield (x + 1));
    var z = yield (y / 3);
    return (x + y + z);
    }

    var b = foo(5);
    b.next() // { value:6, done:false }
    b.next(12) // { value:8, done:false }
    b.next(13) // { value:42, done:true }
  2. 使用说明

    • 通过 next() 方法的参数,可以在 Generator 函数运行的不同阶段,从外部向内部注入不同的值,从而调整函数行为。
    • 第一次调用 next() 方法时,传递参数是无效的。

3. Generator 函数的原型方法

Generator 函数的执行结果是一个遍历器,可以被视为 Generator 函数的实例对象。因此,下述的 throw()return() 方法都是遍历器对象所调用的方法。

3.1 Generator.prototype.throw()

  1. 解释:在 Generator 函数体外抛出错误,该错误可以在 Generator 函数体内被捕获。

  2. 语法:integrator.throw(e)

    • e:被抛出的错误,建议为 Error 对象的实例。
  3. 使用说明

    • throw 方法抛出的错误要被内部捕获,前提是必须至少执行过一次 next 方法,否则,由于 Generator 函数还没有开始执行,throw 方法抛出的错误只能抛出在函数外部。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      var gen = function* gen() {
      try {
      yield 1;
      yield 2;
      } catch (e) {
      yield 3;
      }
      yield 4;
      }

      var g = gen();
      console.log(g.throw(new Error()))// "执行 JavaScript 失败:"
    • throw 方法被内部捕获以后,会附带执行到下一条 yield 表达式,这种情况下等同于执行一次 next 方法。也就是说,throw 方法执行后,也会返回一个结构为 {value: any, done: boolean} 的对象。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      var gen = function* gen() {
      try {
      yield 1;
      yield 2;
      } catch (e) {
      yield 3;
      }
      yield 4;
      }

      var g = gen();
      console.log(g.next())// { value:1, done:false }
      console.log(g.throw(new Error())) // { value:3, done:false }
      console.log(g.next()) // { value:4, done:false }
      console.log(g.next()) // { value:undefined, done:true }
      console.log(g.next()) // { value:undefined, done:true }
    • 一旦 Generator 执行过程中抛出错误,且没有被内部捕获,就不会再执行下去了。如果此后还调用 next 方法,将返回一个 value 属性等于 undefineddone 属性等于 true 的对象,即 JavaScript 引擎认为这个 Generator 已经运行结束了。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      var gen = function* gen() {
      yield 1;
      yield 2;
      yield 3;
      yield 4;
      }

      var g = gen();
      try {
      console.log(g.next()) // { value:1, done:false }
      console.log(g.throw(new Error()))
      } catch (e) {
      console.log(g.next()) // { value:undefined, done:true }
      console.log(g.next()) // { value:undefined, done:true }
      }

3.2 Generator.prototype.return()

  1. 解释:在 Generator 函数体外返回给定的值,并且终结遍历 Generator 函数。

  2. 语法:integrator.return(value)

    • value:返回的值。如果不提供该参数,则默认 value = undefined
  3. 使用说明

    • 如果 Generator 函数内部有 try...finally 代码块,且正在执行 try 代码块,那么 return 方法会导致立刻进入 finally 代码块,执行完以后,再返回 return 方法指定的返回值,此时整个函数才会结束。

    • throw 方法类似,return 方法执行后,也会返回一个结构为 {value: any, done: boolean} 的对象。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      var gen = function* gen() {
      try {
      yield 1;
      yield 2;
      } catch (e) {
      yield 3;
      }finally{
      yield 5;
      yield 6;
      }
      yield 4;
      }

      var g = gen();
      console.log(g.next())// { value:1, done:false }
      console.log(g.throw(new Error())) // { value:3, done:false }
      console.log(g.return(100)) // { value:5, done:false }
      console.log(g.next()) // { value:6, done:false }
      console.log(g.next()) // { value:100, done:true }
      console.log(g.next()) // { value:undefined, done:true }

next()、throw()、return() 对比

next()throw()return() 本质上都是让 Generator 函数恢复执行,并且使用不同的语句替换 yield 表达式。假设遍历器对象当前指向 let result = yield x + y,那么以下方法的执行相当于将 yield 表达式替换为,

  • next(para)let result = para
  • throw(e)let result = throw(e)
  • return(value)let result = return value

4. yield* 表达式

  1. 解释:在 Generator 函数中使用 yield* 表达式,相当于委托执行另一个遍历器对象或可遍历对象的所有元素。yield* 会自动遍历该对象的所有元素,并对每个元素单独使用 yield 表达式进行产出

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    function* foo() {
    yield 'a';
    yield 'b';
    return 'hello';
    }

    function* bar() {
    yield 'x';
    const value = yield* foo();
    console.log(value);
    yield 'y';
    }

    const i = bar();
    console.log(i.next().value) // "x"
    console.log(i.next().value) // "a"
    console.log(i.next().value) // "b"
    // "hello"
    console.log(i.next().value) // "y"
    console.log(i.next().value) // undefined
  2. 语法

    • yield* 遍历器对象
    • yield* 可遍历对象
  3. 使用说明

    • 如果 yield* 后面跟随的是一个 Generator 函数的遍历器,并且该 Generator 函数包含 return 语句,那么可以使用 var value = yield* iterator 的形式来获取 return 语句返回的值

5. Generator 与协程

  1. 协程(coroutine):一种程序运行的方式,可以理解成 “协作的线程” 或 “协作的函数”。
  2. 协程的实现方式
    • 单线程实现:特殊的子例程(subroutine)
    • 多线程实现:特殊的线程
  3. 协程 Vs 子例程
    • 子例程
      • 一个调用栈。
      • 堆栈式“后进先出”的执行方式,只有当调用的子函数完全执行完毕,才会结束执行父函数。
    • 协程
      • 多个调用栈。
      • 多个线程(单线程情况下,即多个函数)可以并行执行,但是只有一个线程(或函数)处于正在运行的状态,其他线程(或函数)都处于暂停态(suspended),线程(或函数)之间可以交换执行权。
  4. 协程 Vs 普通线程
    • 普通线程:同一时间可以有多个线程处于运行状态。
    • 协程:同一时间运行的协程只能有一个,其他协程都处于暂停状态。
  5. Generator 函数 - 半协程:Generator 函数被称为“半协程”(semi-coroutine),意思是只有 Generator 函数的调用者,才能将程序的执行权还给 Generator 函数。此外,Generator 使用 yield 表达式交换控制权。
  6. Generator 函数 - 上下文:Generator 函数执行产生的上下文环境,一旦遇到 yield 命令,就会暂时退出堆栈,但是并不消失,里面的所有变量和对象会冻结在当前状态。等到对它执行 next 命令时,这个上下文环境又会重新加入调用栈,冻结的变量和对象恢复执行。

Generator 函数的异步应用

async 函数

ArrayBuffer

1. 简要概述

  1. 解释:ArrayBuffer 对象代表内存之中的一段二进制数据,开发者可以通过视图TypedArrayDataView)进行操作。由于视图部署了数组接口,因此可以使用数组的方法操作内存ArrayBuffer 对象、TypedArray 视图和 DataView 视图统称为二进制数组

  2. 视图:TypedArray 视图用于读写简单类型的二进制数据;DataView 视图支持自定义复合格式的视图,因此用于读写复杂类型的二进制数据。

    • TypedArray 视图:9 种类型,如 Uint8Array 数组视图、Int16Array 数组视图、Float32Array 数组视图等,对应 9 种数据类型。

    • DataView 视图:可以自定义每个字节对应的数据类型,如第一个字节是 Uint8,第二、三个字节是 Int16,第四个字节开始是 Float32等,支持 8 种数据类型(与 TypedView 视图相比,不支持 Uint8C 数据类型)。

      数据类型 字节长度 含义 对应的 C 语言类型
      Int8 1 8 位带符号整数 signed char
      Uint8 1 8 位不带符号整数 unsigned char
      Uint8C 1 8 位不带符号整数(自动过滤溢出) unsigned char
      Int16 2 16 位带符号整数 short
      Uint16 2 16 位不带符号整数 unsigned short
      Int32 4 32 位带符号整数 int
      Uint32 4 32 位不带符号的整数 unsigned int
      Float32 4 32 位浮点数 float
      Float64 8 64 位浮点数 double

2. ArrayBuffer

  1. 解释:ArrayBuffer 对象代表内存之中的一段二进制数据,仅支持通过视图进行读写操作。

  2. 基本使用

    • ArrayBuffer 的创建:指定连续的内存区域大小为 byteLength,同时每个字节的默认值为 0。

      1
      new ArrayBuffer(byteLength)
    • ArrayBuffer 的操作(通过视图)

      • DataView 视图:指定 ArrayBuffer 实例为参数创建视图实例。

        1
        2
        const dataView = new DataView(buffer);
        console.log(dataView.getInt8(0));
      • TypedArray 视图:指定 ArrayBuffer 实例为参数创建视图实例。

        1
        2
        const int8Array = new Int8Array(buffer);
        console.log(int8Array.length);

        注:与 DataView 视图不同,TypedArray 视图是一组构造函数,代表不同的数据格式

      • TypedArray 视图:指定 Array 实例为参数创建视图实例,此时会隐含地创建一个 ArrayBuffer 对象。

        1
        2
        const int8Array2 = new Int8Array([1, 2, 3]);
        console.log(int8Array2.length);
  3. ArrayBuffer 的属性和方法

    • ArrayBuffer.prototype.byteLength: number 返回 ArrayBuffer 所分配的内存区域的字节长度。内存区域要求连续,因此大内存区域可能会分配失败,因此可以通过如下方法检测内存是否分配成功

      1
      2
      3
      4
      5
      if (buffer.byteLength === n) {
      // 成功
      } else {
      // 失败
      }
    • ArrayBuffer.prototype.slice(start[, end]): ArrayBuffer 创建一个新的 ArrayBuffer 对象,其内存区域数据拷贝自原 ArrayBuffer 内存区域 [start end) 部分的字节数据。这里的 startend 是字节序号,如果不指定 end,则拷贝原 ArrayBufferstart 字节开始的所有字节数据。

    • ArrayBuffer.isView(obj): boolean 表示参数 obj 是否为 ArrayBuffer 的视图实例,即是否为 TypedArray 实例或 DataView 实例。

  4. Conversion:ArrayBuffer 及对应的 TypedArray <–> string

    • ArrayBuffer 及其对应的 TypedArray --> stringTextDeocder

      1
      2
      3
      4
      // Step1. 
      const decoder: TextDecoder = new TextDecoder(outputEncoding); // outputEncoding 指定解码的编码格式,默认为 'utf-8'
      // Step2.
      const output: string = decoder.decode(input); // input 的类型为 ArrayBuffer | Uint8Array | Int8Array | Uint16Array | Int16Array | Uint32Array | Int32Array
    • string --> ArrayBuffer 及其对应的 TypedArrayTextEncoder

      1
      2
      3
      4
      5
      6
      // Step1. 
      const encoder: TextEncoder = new TextEncoder();
      // Step2.
      const view: Uint8Array = encoder(input); // input 的类型为 string
      // Step3.
      const buffer: ArrayBuffer = view.buffer;

3. TypedArray

3.1 概念理解

  1. 视图(view):即对 ArrayBuffer 对象对应内存区域的字节数据的解读方式,分为 TypedArray 视图(将所有数组成员都解读为同一种数据类型)、DataView 视图(将所有数组成员都解读为不同的数据类型)。

  2. TypedArray 视图:共计 9 种,对应 9 种构造函数,9 种数组成员的数据类型

    构造函数 含义
    Int8Array 8 位有符号整数,长度 1 个字节。
    Uint8Array 8 位无符号整数,长度 1 个字节
    Uint8ClampedArray 8 位无符号整数,长度 1 个字节,溢出处理不同。
    Int16Array 16 位有符号整数,长度 2 个字节。
    Uint16Array 16 位无符号整数,长度 2 个字节。
    Int32Array 32 位有符号整数,长度 4 个字节。
    Uint32Array 32 位无符号整数,长度 4 个字节。
    Float32Array 32 位浮点数,长度 4 个字节。
    Float64Array 64 位浮点数,长度 8 个字节。
  3. TypedArray Vs. Array

    差异点 TypedArray Array
    数组成员类型是否相同
    数组成员在内存空间是否连续 不一定(密集数组连续,稀疏数组不连续)
    数组成员的默认值 0 undefined(空位)
    数据存储位置 TypedArray 视图对应的 ArrayBuffer 对象(可通过视图的 buffer 属性获取) Array 对象自身

3.2 构造函数

注:JavaScript 允许基于同一 ArrayBuffer 对象建立多个视图,此时每个视图可以根据自己的理解去修改相应内存区域的数据,同时一个视图对内存的更改会在其他视图上反映出来。

  1. TypedArray(buffer: ArrayBuffer, byteOffset: number=0[, length: number]) 表示从 buffer 的第 byteOffset 个字节开始创建视图,同时确保视图的成员数量为 length。其中 byteOffset0-based 表示,表示视图开始的字节序号,默认值为 0;length 表示视图的成员数量,因此视图对应内存区域的长度 = length * 每个成员字节数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    const buf = new ArrayBuffer(8); // 对应 8 个字节的内存区域(第 0 ~ 7 个字节)
    const v1 = new Int32Array(buf); // 可操作第 0 ~ 7 个字节,每 4 个字节被解析为一个 int32 类型的数组成员,因此视图数组长度为 2
    const v2 = new Uint8Array(buf, 2); // 可操作第 2 ~ 7 个字节,每 1 个字节被解析为一个 uint8 类型的数组成员,因此视图数组长度为 6
    const v3 = new Int16Array(buf, 2, 2); // 可操作第 2 ~ 5(2 + 2 * 2 - 1) 个字节,每 2 个字节被解析为一个 int16 类型的数组成员,此时视图数组长度被指定为 2
    console.log(`v1.length=${v1.length}, v2.length=${v2.length}, v3.length=${v3.length}`); // 2, 6, 2
    v1[0] = 0x12345678; // 修改第 0 ~ 3 个字节的值为 0x12345678,此时 v2[0] 表示第 2 个字节的值为 0x34, 表示第 2 ~ 3 个字节的值为 0x1234
    console.log(buf); // [Uint8Contents]: <78 56 34 12 00 00 00 00>(ArrayBuffer 十六进制表示的每个字节的值)
    console.log(`v1[0]=0x${v1[0].toString(16)}, v2[0]=0x${v2[0].toString(16)}, v3[0]=0x${v3[0].toString(16)}`); // 0x12345678, 0x34, 0x1234

    注:创建视图时,byteOffset 必须与所建立的数据类型一致,即 byteOffset 必须是数组成员的字节数的整数倍,否则会报错。

    1
    const v4 = new Int16Array(buf, 1, 2); // RangeError: start offset of Int16Array should be a multiple of 2
  2. TypedArray(length) 表示创建一个成员数量为 length 的视图,此时视图实际上创建了一个大小为 length * 视图成员字节数ArrayBuffer。其中 length 表示视图成员数量;视图创建的 ArrayBuffer 可以通过视图的 buffer 属性获取。

    1
    2
    3
    const f64a = new Float64Array(8); // 视图成员数量为 8,对应的内存区域大小为 8 * 8 = 64 字节
    console.log(`f64a.length=${f64a.length}, f64a.buffer.byteLength=${f64a.buffer.byteLength}`); // 8, 64
    console.log(f64a.buffer); // [Uint8Contents]: <00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00>(ArrayBuffer 十六进制表示的每个字节的值)
  3. TypedArray(typedArray) 表示基于给定视图创建一个新的视图,此时新视图对应的 ArrayBuffer 与给定视图的 ArrayBuffer 不是同一个,可以理解为两个 ArrayBuffer 内存区域不同,但是数据相同,即数据发生了拷贝。

    1
    2
    3
    4
    const view1 = new Int16Array([1, 2, 3, 4]);
    const view2 = new Int16Array(view1);
    view1[0] = 123; // 此时 view1 对应的内存区域数据为 123 2 3 4,view2 对应的内存区域的数据为 1 2 3 4,即两个视图的内存区域相互独立互不干扰
    console.log(`view1[0]=${view1[0]}, view2[0]=${view2[0]}`); // 123, 1
  4. TypedArray(arrayLikeObject) 表示基于给定类数组对象创建一个视图,此时视图实际上开辟了一个新的 ArrayBuffer,与给定类数组对象无关,即数据发生了拷贝。

    1
    2
    const view = new Int16Array([1, 2, 3, 4]); // 视图的成员数量为给定的类数组对象的成员数量。
    console.log(`view.length=${view.length}`); // 4

    注:TypedArray 视图可以通过以下三种方法转换为普通数组:[...typedArray], Array.from(typedArray), Array.prototype.slice.call(typedArray)

3.3 数组方法

  1. TypedArray 可用的数组方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    TypedArray.prototype.copyWithin(target, start[, end=this.length])
    TypedArray.prototype.entries()
    TypedArray.prototype.every(callbackfn, thisArg?)
    TypedArray.prototype.fill(value, start=0, end=this.length)
    TypedArray.prototype.filter(callbackfn, thisArg?)
    TypedArray.prototype.find(predicate, thisArg?)
    TypedArray.prototype.findIndex(predicate, thisArg?)
    TypedArray.prototype.forEach(callbackfn, thisArg?)
    TypedArray.prototype.indexOf(searchElement, fromIndex=0)
    TypedArray.prototype.join(separator)
    TypedArray.prototype.keys()
    TypedArray.prototype.lastIndexOf(searchElement, fromIndex?)
    TypedArray.prototype.map(callbackfn, thisArg?)
    TypedArray.prototype.reduce(callbackfn, initialValue?)
    TypedArray.prototype.reduceRight(callbackfn, initialValue?)
    TypedArray.prototype.reverse()
    TypedArray.prototype.slice(start=0, end=this.length)
    TypedArray.prototype.some(callbackfn, thisArg?)
    TypedArray.prototype.sort(comparefn)
    TypedArray.prototype.toLocaleString(reserved1?, reserved2?)
    TypedArray.prototype.toString()
    TypedArray.prototype.values()
  2. 使用说明

    • TypedArray 数组没有 concat 方法,可以通过以下函数实现多个 TypedArray 数组的合并操作。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      function concatenate(resultConstructor, ...arrays) {
      let totalLength = 0;
      for (let arr of arrays) {
      totalLength += arr.length;
      }
      let result = new resultConstructor(totalLength);
      let offset = 0;
      for (let arr of arrays) {
      result.set(arr, offset);
      offset += arr.length;
      }
      return result;
      }

      concatenate(Uint8Array, Uint8Array.of(1, 2), Uint8Array.of(3, 4)

      注:TypedArray 可以称之为数组,因为其实现了数组接口;也可称之为视图,因为其可以对 ArrayBuffer 对应的内存区域进行操作。

    • TypedArray 数组部署了 Iterator 接口,即 TypedArray 数组是可迭代对象,可以通过 for...of 遍历。

      1
      2
      3
      4
      let ui8 = Uint8Array.of(0, 1, 2);
      for (let byte of ui8) {
      console.log(byte);
      }

3.4 字节序

  1. 大端字节序(Big-Endian):重要的字节排在前边,不重要的字节排在后边。换句话说,高位字节排在内存低位,低位字节排在内存高位。

    • 示例:0x12345678 的大端字节序的存储为 12 34 56 78(内存地址从低到高)。
    • 场景:网络传输(TCP/IP 协议等)。
  2. 小端字节序(Little-Endian,默认):不重要的字节排在前边,重要的字节排在后边。换句话说,低位字节在内存低位,高位字节在内存高位。

    注:TypedArray 视图采取小端字节序读写数据,不支持修改字节序。但是,DataView 视图支持以指定的字节序读写数据。

    • 示例:0x12345678 的小端字节序的存储为 78 56 34 12(内存地址从低到高)。
    • 场景:x86 处理器、Windows 系统。
  3. 字节序判断

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    const BIG_ENDIAN = Symbol('BIG_ENDIAN');
    const LITTLE_ENDIAN = Symbol('LITTLE_ENDIAN');

    function getPlatformEndianness() {
    let arr32 = Uint32Array.of(0x12345678);
    let arr8 = new Uint8Array(arr32.buffer);
    switch ((arr8[0]*0x1000000) + (arr8[1]*0x10000) + (arr8[2]*0x100) + (arr8[3])) {
    case 0x12345678:
    return BIG_ENDIAN;
    case 0x78563412:
    return LITTLE_ENDIAN;
    default:
    throw new Error('Unknown endianness');
    }
    }

3.5 属性和方法

  1. 静态属性

    • TypedArray.BYTES_PER_ELEMENT 获取指定视图的数组成员所占据的字节数。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      Int8Array.BYTES_PER_ELEMENT // 1
      Uint8Array.BYTES_PER_ELEMENT // 1
      Uint8ClampedArray.BYTES_PER_ELEMENT // 1
      Int16Array.BYTES_PER_ELEMENT // 2
      Uint16Array.BYTES_PER_ELEMENT // 2
      Int32Array.BYTES_PER_ELEMENT // 4
      Uint32Array.BYTES_PER_ELEMENT // 4
      Float32Array.BYTES_PER_ELEMENT // 4
      Float64Array.BYTES_PER_ELEMENT // 8
  2. 实例属性

    • TypedArray.prototype.BYTES_PER_ELEMENTTypedArray.BYTES_PER_ELEMENT 相同。

4. DataView