0%

学习笔记 2020 10 15

学习笔记 2020-10-15

JavaScript 高级程序设计(第4版) 阅读记录

迭代器与生成器

迭代器模式

一些结构可以称为 可迭代对象 ,因为它们实现了正式的 Iterable 接口。

可迭代对象包含有限的元素,且具有无歧义的遍历顺序。

任何实现了 Iterable 接口的数据结构都可以被实现 Iterator 接口的结构消费。

迭代器是按需创建的一次性对象,关联一个可迭代对象,迭代器会暴露迭代其关联的可迭代对象的 API ,无需了解与其关联的可迭代对象的结构,只需要知道如何取得连续的值。

  1. 可迭代协议

    实现 Iterable 接口要求同时具备两种能力:支持迭代的自我识别能力和创建实现 Iterator 接口的对象的能力。在 ECMAScript 中,必须暴露一个属性作为 默认迭代器 ,必须使用 Symbol.iterator 作为键,必须引用一个迭代器工厂函数,必须返回一个新迭代器。

    实现了 Iterable 接口的内置类型:

    • 字符串
    • 数组
    • 映射
    • 集合
    • arguments 对象
    • NodeListDOM 集合类型
    // 这些类型都实现了迭代器工厂函数
    console.log(str[Symbol.iterator]); // f values() { [native code] }
    console.log(arr[Symbol.iterator]); // f values() { [native code] }
    console.log(map[Symbol.iterator]); // f values() { [native code] }
    console.log(set[Symbol.iterator]); // f values() { [native code] }
    console.log(els[Symbol.iterator]); // f values() { [native code] }
    // 调用这个工厂函数会生成一个迭代器
    console.log(str[Symbol.iterator]()); // StringIterator {}
    console.log(arr[Symbol.iterator]()); // ArrayIterator {}
    console.log(map[Symbol.iterator]()); // MapIterator {}
    console.log(set[Symbol.iterator]()); // SetIterator {}
    console.log(els[Symbol.iterator]()); // ArrayIterator {}

    我们不需要显式调用这个工厂函数来生成迭代器,实现可迭代协议的所有类型都会自动兼容接收可迭代对象的任何语言特性。例如:

    • for-of 循环
    • 数组解构
    • 拓展操作符
    • Array.from()
    • 创建集合
    • 创建映射
    • Promise.all()
    • Promise.race()
    • yield*

    这些原生语言结构会在后台调用提供的可迭代对象的工厂函数来创建一个迭代器。

    如果对象原型链上的父类实现了 Iterable 接口,那么这个对象也就实现了这个接口。

  2. 迭代器协议

    迭代器是一种一次性使用的对象,用于迭代与其关联的可迭代对象。

    使用 next() 方法在可迭代对象中遍历数据。每次成功调用 next() ,都会返回一个 IteratorResult 对象,包含迭代器返回的 下一个值。

    IteratorResult 包含两个属性, donevalue

    done 表示是否还可以再次调用 next()

    value 表示包含可迭代对象的下一个值。

    donefalse 时可以取得 valuedonetruevalue 返回 undefined

    // 可迭代对象
    let arr = ['foo', 'bar'];
    // 迭代器工厂函数
    console.log(arr[Symbol.iterator]); // f values() { [native code] }
    // 迭代器
    let iter = arr[Symbol.iterator]();
    console.log(iter); // ArrayIterator {}
    // 执行迭代
    console.log(iter.next()); // { done: false, value: 'foo' }
    console.log(iter.next()); // { done: false, value: 'bar' }
    console.log(iter.next()); // { done: true, value: undefined }

    迭代器到达 done: true 状态后,再次调用 next() 会一直返回相同的值,即 undefined

    如果可迭代对象在迭代期间被修改,迭代器也会反映相同的变化。

    let arr = ['foo', 'baz'];
    let iter = arr[Symbol.iterator]();
    console.log(iter.next()); // { done: false, value: 'foo' }
    // 在数组中间插入值
    arr.splice(1, 0, 'bar');
    console.log(iter.next()); // { done: false, value: 'bar' }
    console.log(iter.next()); // { done: false, value: 'baz' }
    console.log(iter.next()); // { done: true, value: undefined }

    迭代器维护着一个指向可迭代对象的引用,会阻止垃圾回收程序回收可迭代对象。

    // 这个类实现了可迭代接口(Iterable)
    // 调用默认的迭代器工厂函数会返回
    // 一个实现迭代器接口(Iterator)的迭代器对象
    class Foo {
      [Symbol.iterator]() {
        return {
          next() {
            return { done: false, value: 'foo' };
          }
        };
      }
    }
    let f = new Foo();
    let h = f[Symbol.iterator]();
    // 打印出实现了迭代器接口的对象
    console.log(h); // { next: f() {} }
    console.log(h.next()); // {done: false, value: "foo"}
    console.log(h.next()); // {done: false, value: "foo"}
    console.log(h.next()); // {done: false, value: "foo"}
    console.log(h.next()); // {done: false, value: "foo"}
    // Array 类型实现了可迭代接口(Iterable)
    // 调用 Array 类型的默认迭代器工厂函数
    // 会创建一个 ArrayIterator 的实例
    let a = new Array();
    // 打印出 ArrayIterator 的实例
    console.log(a[Symbol.iterator]()); // Array Iterator {}
  3. 自定义迭代器

    任何实现 Iterator 接口的对象都可以作为迭代器使用。

    class Counter {
      // Counter 的实例应该迭代 limit 次
      constructor(limit) {
        this.count = 1;
        this.limit = limit;
      }
      next() {
        if (this.count <= this.limit) {
          return { done: false, value: this.count++ };
        } else {
          return { done: true, value: undefined };
        }
      }
      [Symbol.iterator]() {
        return this;
      }
    }
    let counter = new Counter(3);
    for (let i of counter) {
      console.log(i);
    }
    // 1
    // 2
    // 3
    for (let i of counter) { console.log(i); }
    // (nothing logged)

    以上例子创建了一个自定义迭代次数的迭代器,但它的每个实例只能被迭代一次。

    class Counter {
      constructor(limit) {
        this.limit = limit;
      }
      [Symbol.iterator]() {
        let count = 1,
          limit = this.limit;
        return {
          next() {
            if (count <= limit) {
              return { done: false, value: count++ };
            } else {
              return { done: true, value: undefined };
            }
          }
        };
      }
    }
    let counter = new Counter(3);
    for (let i of counter) {
      console.log(i);
    }
    // 1
    // 2
    // 3
    for (let i of counter) {
      console.log(i);
    }
    // 1
    // 2
    // 3

    Symbol.iterator 属性引用的工厂函数返回相同的迭代器。

    let arr = ['foo', 'bar', 'baz'];
    let iter1 = arr[Symbol.iterator]();
    console.log(iter1[Symbol.iterator]); // f values() { [native code] }
    let iter2 = iter1[Symbol.iterator]();
    console.log(iter1 === iter2); // true
  4. 提前终止迭代器

    可以使用一个可选的 return 方法来指定迭代器提前关闭时的执行逻辑。

    以下情况可以触发 return

    • for-of 循环通过 breakcontinuereturnthrow
    • 解构操作并未消费所有值

    return 方法必须返回一个有效的 IteratorResult 对象。

    class Counter {
      constructor(limit) {
        this.limit = limit;
      }
      [Symbol.iterator]() {
        let count = 1,
          limit = this.limit;
        return {
          next() {
            if (count <= limit) {
              return { done: false, value: count++ };
            } else {
              return { done: true };
            }
          },
          return() {
            console.log('Exiting early');
            return { done: true };
          }
        };
      }
    }
    let counter1 = new Counter(5);
    for (let i of counter1) {
      if (i > 2) {
        break;
      }
      console.log(i);
    }
    // 1
    // 2
    // Exiting early
    let counter2 = new Counter(5);
    try {
      for (let i of counter2) {
        if (i > 2) {
          throw 'err';
        }
        console.log(i);
      }
    } catch (e) {}
    // 1
    // 2
    // Exiting early
    let counter3 = new Counter(5);
    let [a, b] = counter3;
    // Exiting early

    数组的迭代器不能关闭,可以继续从上次离开的地方继续迭代。

    let a = [1, 2, 3, 4, 5];
    let iter = a[Symbol.iterator]();
    for (let i of iter) {
      console.log(i);
      if (i > 2) {
        break;
      }
    }
    // 1
    // 2
    // 3
    for (let i of iter) {
      console.log(i);
    }
    // 4
    // 5

    手动给不可关闭的迭代器添加 return 方法可行,在迭代器提前终止时会调用这个方法,但并不能让这个迭代器进入关闭状态。

    let a = [1, 2, 3, 4, 5];
    let iter = a[Symbol.iterator]();
    iter.return = function () {
      console.log('Exiting early');
      return { done: true };
    };
    for (let i of iter) {
      console.log(i);
      if (i > 2) {
        break;
      }
    }
    // 1
    // 2
    // 3
    // Exiting early
    for (let i of iter) {
      console.log(i);
    }
    // 4
    // 5

生成器

生成器是 ES6 新增的结构,拥有在一个函数块内暂停和恢复代码执行的能力。

  1. 生成器基础

    生成器的形式是一个函数,在函数名称前加一个星号表示它是一个生成器。

    // 生成器函数声明
    function* generatorFn() {}
    // 生成器函数表达式
    let generatorFn = function* () {};
    // 作为对象字面量方法的生成器函数
    let foo = {
      *generatorFn() {}
    };
    // 作为类实例方法的生成器函数
    class Foo {
      *generatorFn() {}
    }
    // 作为类静态方法的生成器函数
    class Bar {
      static *generatorFn() {}
    }
    // 等价的生成器函数:
    function* generatorFnA() {}
    function *generatorFnB() {}
    function * generatorFnC() {}
    // 等价的生成器方法:
    class Foo {
      *generatorFnD() {}
      * generatorFnE() {}
    }

    箭头函数不能用于定义生成器函数。

    调用生成器函数会产生一个生成器对象,一开始处于暂停执行的状态。

    生成器对象也实现了 Iterator 接口,具有 next() 方法。

    next 方法返回值类似于迭代器,有一个 done 属性和一个 value 属性。

    function* generatorFn() {
      return 'foo';
    }
    let generatorObject = generatorFn();
    console.log(generatorObject); // generatorFn {<suspended>}
    console.log(generatorObject.next()); // { done: true, value: 'foo' }
    console.log(generatorObject.next()); // { done: true, value: undefined }

    生成器函数只会在初次调用 next 方法后开始执行。

面试题

  1. 请写函数实现,利用对象中的数据渲染模板,并返回最终结果。
    let template =
      '我们的公司是{{company}},我们属于{{group.name}},我们主要业务包括{{group.job[0]}}、{{group["job"][1]}}';
    
    let obj = {
      company: 'xxxx股份有限公司',
      group: {
        name: 'xx研发部',
        job: ['aaa', 'bbb']
      }
    };
    function tmpReplace(tmp) {
      return tmp.replace(/{{(.*?)}}/g, function (node, key) {
        let keys = [];
        let res = obj[key] || obj;
        keys = key.split('.');
        if (keys.length) {
          keys.forEach((i, idx) => {
            keys.splice(idx, 1, ...i.split(/[\[|\]]/));
          });
        } else keys = key.split(/[\[|\]]/);
        keys.forEach(i => {
          i = i.replace(/\"/g, '');
          let j = res[i];
          if (j) res = j;
        });
        return res;
      });
    }
    
    console.log(tmpReplace(template));
    // 我们的公司是xxxx股份有限公司,我们属于xx研发部,我们主要业务包括aaa、bbb
  2. 请写出上述代码浏览器执行后,控制台的打印顺序。
    console.log('script start');
    
    setTimeout(() => {
      console.log('setTimeout');
    }, 0);
    
    Promise.resolve()
      .then(function () {
        console.log('promise1');
      })
      .then(function () {
        console.log('promise2');
      });
    
    console.log('script end');
    
    // script start
    // script end
    // promise1
    // promise2
    // setTimeout

    此题需要理解宏任务微任务的概念,第一行和第十五行的打印都是同步任务,所以直接打引。第三行的定时器是宏任务,执行到此处会将回调函数放入宏任务队列,Promise 是微任务,第八行的 .then 的回调属于异步,先放入微任务队列,然后继续执行同步代码,所以第十五行先于 promise 打印。执行完同步任务,开始确认微任务队列,先打印 promise1 ,然后执行第二个 .then ,再次增加一个微任务,继续执行微任务,最后再去确认宏任务队列,打印定时器的输出。

  3. css 实现如下三栏布局,要求两边各 150px ,中间自适应。写两种方法。
    <div class="box">
      <div class="left"></div>
      <div class="center"></div>
      <div class="right"></div>
    </div>
    // 使用 flex 布局
    .box {
      width: 100%;
      height: 500px;
      display: flex;
    }
    .box > div {
      height: 100%;
    }
    .box .left {
      width: 150px;
      background-color: red;
    }
    .box .center {
      flex-grow: 1;
      background-color: lightblue;
    }
    .box .right {
      width: 150px;
      background-color: lightgoldenrodyellow;
    }
    // 使用绝对定位
    .box {
      position: relative;
      width: 100%;
      height: 500px;
    }
    .box > div {
      position: absolute;
      top: 0;
      height: 100%;
    }
    .box .left {
      left: 0;
      width: 150px;
      background-color: red;
    }
    .box .center {
      left: 150px;
      right: 150px;
      background-color: lightblue;
    }
    .box .right {
      right: 0;
      width: 150px;
      background-color: lightgoldenrodyellow;
    }
    // 使用 table 布局
    .box {
      display: table;
      width: 100%;
      height: 500px;
    }
    .box > div {
      display: table-cell;
      height: 100%;
    }
    .box .left {
      width: 150px;
      background-color: red;
    }
    .box .center {
      background-color: lightblue;
    }
    .box .right {
      width: 150px;
      background-color: lightgoldenrodyellow;
    }
    // 使用 grid 布局
    .box {
      display: grid;
      grid-template-columns: 150px auto 150px;
      width: 100%;
      height: 500px;
    }
    .box > div {
      height: 100%;
    }
    .box .left {
      width: 150px;
      background-color: red;
    }
    .box .center {
      background-color: lightblue;
    }
    .box .right {
      width: 150px;
      background-color: lightgoldenrodyellow;
    }

    image-20201015111101303

  4. 请用 promise 实现函数,要求每5秒获取一个随机数,如果小于 5 ,立即结束函数,如果大于 5 ,则 60 秒后函数运行结束。
    function myRandom() {
      setInterval(() => {
        _fx();
      }, 5000);
      function _fx() {
        return new Promise((resolve, reject) => {
          let r = Math.random() * 10 + 1;
          if (r > 5) {
            resolve();
            console.log('函数立即结束');
          } else {
            setTimeout(() => {
              resolve();
              console.log('函数 60s 后结束');
            }, 3000);
          }
        });
      }
    }
    myRandom();

    其实这道题没太看懂题目要求,不太懂这个逻辑,好像这个函数的随机数只会获取一次,一次后这个数必定会有个结束,要么是立即要么是延时。又或者是只是单纯延迟这一次的随即调用。