0%

学习笔记 2020 10 31

学习笔记 2020-10-31

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

期约与异步函数

期约

期约拓展
  1. 期约取消

    ECMAScript 规范未曾涉及期约取消这个特性。

    我们可以通过在现有实现基础上提供一种临时性的封装,以实现取消期约的功能。

    class CancelToken {
      constructor(cancelFn) {
        this.promise = new Promise((resolve, reject) => {
          cancelFn(resolve);
        });
      }
    }
    <button id="start">Start</button>
    <button id="cancel">Cancel</button>
    <script>
      class CancelToken {
        constructor(cancelFn) {
          this.promise = new Promise((resolve, reject) => {
            cancelFn(() => {
              setTimeout(console.log, 0, 'delay cancelled');
              resolve();
            });
          });
        }
      }
      const startButton = document.querySelector('#start');
      const cancelButton = document.querySelector('#cancel');
      function cancellableDelayedResolve(delay) {
        setTimeout(console.log, 0, 'set delay');
        return new Promise((resolve, reject) => {
          const id = setTimeout(() => {
            setTimeout(console.log, 0, 'delayed resolve');
            resolve();
          }, delay);
          const cancelToken = new CancelToken(cancelCallback =>
            cancelButton.addEventListener('click', cancelCallback)
          );
          cancelToken.promise.then(() => clearTimeout(id));
        });
      }
      startButton.addEventListener('click', () => cancellableDelayedResolve(1000));
    </script>
  2. 期约进度通知

    ECMAScript6 期约不支持进度追踪,可以通过扩展来实现。

    class TrackablePromise extends Promise {
      constructor(executor) {
        const notifyHandlers = [];
        super((resolve, reject) => {
          return executor(resolve, reject, status => {
            notifyHandlers.map(handler => handler(status));
          });
        });
        this.notifyHandlers = notifyHandlers;
      }
      notify(notifyHandler) {
        this.notifyHandlers.push(notifyHandler);
        return this;
      }
    }
    let p = new TrackablePromise((resolve, reject, notify) => {
      function countdown(x) {
        if (x > 0) {
          notify(`${20 * x}% remaining`);
          setTimeout(() => countdown(x - 1), 1000);
        } else {
          resolve();
        }
      }
      countdown(5);
    });

异步函数

异步函数也称为 async/await 语法关键字,是 ES6 期约模式在 ECMAScript 函数中的应用。 async/await 是 ES8 规范新增的。

  1. async

    该关键字用于声明异步函数。可以用在函数声明、函数表达式、箭头函数和方法上。

    使用 async 关键字可以让函数具有异步特征,但总体上其代码仍然是同步求值的。

    异步函数如果使用 return 关键字返回值,会被 Promise.resolve() 包装成一个期约对象。

    在异步函数中抛出错误会返回拒绝的期约。

    拒绝期约的错误不会被异步函数捕获。

    async function foo() {
      console.log(1);
      throw 3;
    }
    // 给返回的期约添加一个拒绝处理程序
    foo().catch(console.log);
    console.log(2);
    // 1
    // 2
    // 3
    
    async function foo() {
      console.log(1);
      Promise.reject(3);
    }
    // Attach a rejected handler to the returned promise
    foo().catch(console.log);
    console.log(2);
    // 1
    // 2
    // Uncaught (in promise): 3
  2. await

    使用 await 关键字可以暂停异步函数代码的执行,等待期约解决。

    async function foo() {
      let p = new Promise((resolve, reject) => setTimeout(resolve, 1000, 3));
      console.log(await p);
    }
    foo();
    // 3

    await 关键字会暂停执行异步函数后面的代码,让出 JS 运行时的执行线程。await 关键字尝试解包对象的值,然后将这个值传给表达式,再异步恢复异步函数的执行。

    await 期待一个实现 thenable 接口的对象,但常规的值也可以。如果是实现 thenable 接口的对象,则这个对象可以由 await 来解包。否则,将这个值当作已经解决的期约。

停止和恢复执行
async function foo() {
  console.log(2);
  setTimeout(() => {
    console.log(10);
  }, 0);
  console.log(await new Promise((resolve, reject) => resolve(6)));
  setTimeout(() => {
    console.log(11);
  }, 0);
  console.log(7);
}
async function bar() {
  console.log(4);
  console.log(await 8);
  console.log(9);
}
console.log(1);
foo();
console.log(3);
bar();
console.log(5);
// 1
// 2
// 3
// 4
// 5
// 6
// 7
// 8
// 9
// 10
// 11

await 后面的值可用时会被添加到消息队列,等到同步线程执行完毕才会执行.

现代 JavaScript 教程

数字调用 toString 方法时需要使用两个点,防止识别为小数点。

任务

  1. 来自访问者的数字的总和

    let first = +prompt('first number', 0);
    let second = +prompt('second number', 0);
    alert(first + second);
  2. 为什么 6.35.toFixed(1) == 6.3?

    根据文档,Math.roundtoFixed 都将数字舍入到最接近的数字:0..4 会被舍去,而 5..9 会进一位。

    例如:

    alert( 1.35.toFixed(1) ); // 1.4

    在下面这个类似的示例中,为什么 6.35 被舍入为 6.3 而不是 6.4

    alert( 6.35.toFixed(1) ); // 6.3

    如何以正确的方式来对 6.35 进行舍入?

    在内部,6.35 的小数部分是一个无限的二进制。在这种情况下,它的存储会造成精度损失。

    让我们来看看:

    alert( 6.35.toFixed(20) ); // 6.34999999999999964473

    精度损失可能会导致数字的增加和减小。在这种特殊的情况下,数字变小了一点,这就是它向下舍入的原因。

    那么 1.35 会怎样呢?

    alert( 1.35.toFixed(20) ); // 1.35000000000000008882

    在这里,精度损失使得这个数字稍微大了一些,因此其向上舍入。

    如果我们希望以正确的方式进行舍入,我们应该如何解决 6.35 的舍入问题呢?

    在进行舍入前,我们应该使其更接近整数:

    alert( (6.35 * 10).toFixed(20) ); // 63.50000000000000000000

    请注意,63.5 完全没有精度损失。这是因为小数部分 0.5 实际上是 1/2。以 2 的整数次幂为分母的小数在二进制数字系统中可以被精确地表示,现在我们可以对它进行舍入:

    alert( Math.round(6.35 * 10) / 10); // 6.35 -> 63.5 -> 64(rounded) -> 6.4
  3. 重复,直到输入的是一个数字

    创建一个函数 readNumber,它提示输入一个数字,直到访问者输入一个有效的数字为止。

    结果值必须以数字形式返回。

    访问者也可以通过输入空行或点击“取消”来停止该过程。在这种情况下,函数应该返回 null

    function readNumber() {
      let input = prompt('input', '');
      if (input === null || input.trim() === '') {
        return null;
      }
      while (isNaN(parseInt(input))) {
        input = prompt('input', '');
        if (input === null || input.trim() === '') {
          return null;
        }
      }
      return parseInt(input);
    }

    参考解决方案:

    function readNumber() {
      let num;
    
      do {
        num = prompt("Enter a number please?", 0);
      } while ( !isFinite(num) );
    
      if (num === null || num === '') return null;
    
      return +num;
    }
    
    alert(`Read: ${readNumber()}`);

    该解决方案有点复杂,因为我们需要处理 null 和空行。

    所以,我们实际上接受输入,直到输入的是一个“常规数字”。null(取消)和空行都符合该条件,因为在数字形式中它们是 0。

    在我们停止之后,我们需要专门处理 null 和空行(返回 null),因为将它们转换为数字将返回 0。

  4. 一个偶发的无限循环

    这是一个无限循环。它永远不会结束。为什么?

    let i = 0;
    while (i != 10) {
      i += 0.2;
    }

    因为 0.2 累加永远不会等于10。

  5. 从 min 到 max 的随机数

    内建函数 Math.random() 会创建一个在 01 之间(不包括 1)的随机数。

    编写一个 random(min, max) 函数,用以生成一个在 minmax 之间的随机浮点数(不包括 max))。

    运行示例:

    alert( random(1, 5) ); // 1.2345623452
    alert( random(1, 5) ); // 3.7894332423
    alert( random(1, 5) ); // 4.3435234525
    function random(min, max) {
      return Math.random() * (max - min) + min;
    }