学习笔记 2020-10-31
JavaScript 高级程序设计(第4版) 阅读记录
期约与异步函数
期约
期约拓展
期约取消
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>
期约进度通知
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 规范新增的。
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
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 方法时需要使用两个点,防止识别为小数点。
任务
来自访问者的数字的总和
let first = +prompt('first number', 0); let second = +prompt('second number', 0); alert(first + second);
为什么 6.35.toFixed(1) == 6.3?
根据文档,
Math.round
和toFixed
都将数字舍入到最接近的数字: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
重复,直到输入的是一个数字
创建一个函数
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。
一个偶发的无限循环
这是一个无限循环。它永远不会结束。为什么?
let i = 0; while (i != 10) { i += 0.2; }
因为 0.2 累加永远不会等于10。
从 min 到 max 的随机数
内建函数
Math.random()
会创建一个在0
到1
之间(不包括1
)的随机数。编写一个
random(min, max)
函数,用以生成一个在min
到max
之间的随机浮点数(不包括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; }