学习笔记 2020-11-01
JavaScript 高级程序设计(第4版) 阅读记录
期约与异步函数
异步函数
异步函数策略
实现 sleep()
async function sleep(delay) { return new Promise(resolve => setTimeout(resolve, delay)); } async function foo() { const t0 = Date.now(); await sleep(1500); // 暂停约 1500 毫秒 console.log(Date.now() - t0); } foo(); // 1502
利用平行执行
async function randomDelay(id) { // 延迟 0~1000 毫秒 const delay = Math.random() * 1000; return new Promise(resolve => setTimeout(() => { console.log(`${id} finished`); resolve(); }, delay) ); } async function foo() { const t0 = Date.now(); for (let i = 0; i < 5; ++i) { await randomDelay(i); } console.log(`${Date.now() - t0}ms elapsed`); } foo(); // 0 finished // 1 finished // 2 finished // 3 finished // 4 finished // 877ms elapsed
就算这些期约之间没有依赖,异步函数也会依次暂停,等待每个超时完成。这样可以保证执行顺序。
如果顺序不是必须保证的,那么可以先一次性初始化所有期约,再分别等待它们的结果。
async function randomDelay(id) { // 延迟 0~1000 毫秒 const delay = Math.random() * 1000; return new Promise(resolve => setTimeout(() => { console.log(`${id} finished`); resolve(id); }, delay) ); } async function foo() { const t0 = Date.now(); const promises = Array(5) .fill(null) .map((_, i) => randomDelay(i)); for (const p of promises) { console.log(`awaited ${await p}`); } console.log(`${Date.now() - t0}ms elapsed`); } foo(); // 1 finished // 0 finished // awaited 0 // awaited 1 // 2 finished // awaited 2 // 4 finished // 3 finished // awaited 3 // awaited 4 // 964ms elapsed
期约没有按顺序执行,但 await 按顺序收到了每个期约的值。
串行执行期约
async function addTwo(x) { return x + 2; } async function addThree(x) { return x + 3; } async function addFive(x) { return x + 5; } async function addTen(x) { for (const fn of [addTwo, addThree, addFive]) { x = await fn(x); } return x; } addTen(9).then(console.log); // 19
此处 await 传递了每个函数的返回值。
栈追踪与内存管理
期约与异步函数的功能有相当程度的重叠,但它们在内存中的表示则差别很大。
function fooPromiseExecutor(resolve, reject) { setTimeout(reject, 1000, 'bar'); } function foo() { new Promise(fooPromiseExecutor); } foo(); // Uncaught (in promise) bar // setTimeout (async) // fooPromiseExecutor // foo
这是拒绝期约的栈追踪信息。
JS 引擎在创建期约时尽可能保留完整的调用栈。在抛出错误时,调用栈可以由运行时的错误处理逻辑获取,因而就会出现在栈追踪信息中。
function fooPromiseExecutor(resolve, reject) { setTimeout(reject, 1000, 'bar'); } async function foo() { await new Promise(fooPromiseExecutor); } foo(); // Uncaught (in promise) bar // foo // async function (async) // foo
BOM
window 对象
BOM 的核心是 window 对象,表示浏览器的实例。window 对象是 ECMAScript 中的 Global 对象,也是浏览器窗口的 JavaScript 接口。
Global 作用域
通过 var
声明的所有全局变量和函数都会变成 window 对象的属性和方法。
访问未声明的变量会抛出错误,但是可以在 window 对象上查询是否存在可能未声明的变量。
// 这会导致抛出错误,因为 oldValue 没有声明
var newValue = oldValue;
// 这不会抛出错误,因为这里是属性查询
// newValue 会被设置为 undefined
var newValue = window.oldValue;
窗口关系
top 对象始终指向最上层窗口,即浏览器窗口本身。parent 对象则始终指向当前窗口的父窗口。如果当前窗口是最上层窗口,则 parent 等于 top 等于 window 。最上层的 window 如果不是通过 window.open
打开的,那么其 name 属性就不会包含值。
self 对象始终指向 window 。实际上,self 和 window 就是同一个对象。
这些都是 window 对象的属性。
窗口位置与像素比
window 对象的位置可以通过不同的属性和方法来确定。现代浏览器提供了 screenLeft
和 screenTop
属性,用于表示窗口相对于屏幕左侧和顶部的位置。
可以使用 moveTo()
和 moveBy()
方法移动窗口。moveTo
接收要移动到的新位置的绝对坐标 x 和 y 。moveBy
接收相对当前位置在两个方向上移动的像素数。
CSS 像素是 Web 开发中使用的统一像素单位。这个单位的背后是一个角度:0.0213°
。如果屏幕距离人眼是一臂长,则以这个角度计算的 CSS 像素大小约为 1/96 英寸。这样定义像素大小是为了在不同设备上统一标准。不同像素密度的屏幕下会有不同的缩放系数,以便把物理像素( 屏幕实际的分辨率 )转换为 CSS 像素( 浏览器报告的虚拟分辨率 )。
手机屏幕的物理分辨率可能是 1920x1080 。但因为其像素可能非常小,所以浏览器就需要把其分辨率降为较低的逻辑分辨率,比如 640x320 。这个物理像素与 CSS 像素之间的转换比率由 window.devicePixelRatio
属性提供。对于前面的例子,该属性的值就是 3 。于是,12 像素( CSS 像素)就会用 36 像素的物理像素来显示。
window.devicePixelRatio
实际上与每英寸像素数( DPI )是对应的。 DPI 表示单位像素密度,而 window.devicePixelRatio
表示物理像素与逻辑像素之间的缩放系数。
窗口大小
所有现代浏览器都支持 4 个属性:
innerWidth
innerHeight
outerWidth
outerHeight
outerWidth
和 outerHeight
返回浏览器窗口自身的大小。( 不管是在最外层 window 上使用,还是在窗格 <frame>
中使用 )。
innerWidth
和 innerHeight
返回浏览器窗口中页面视口的大小( 不包含浏览器边框和工具栏 )。
document.documentElement.clientWidth
和 document.documentElement.clientHeight
返回页面视口的宽度和高度。
let pageWidth = window.innerWidth,
pageHeight = window.innerHeight;
if (typeof pageWidth != 'number') {
if (document.compatMode == 'CSS1Compat') {
pageWidth = document.documentElement.clientWidth;
pageHeight = document.documentElement.clientHeight;
} else {
pageWidth = document.body.clientWidth;
pageHeight = document.body.clientHeight;
}
}
检查是否存在 inner
属性,不存在的话,检查页面是否处于标准模式,是的话使用 documentElement
的 client
属性,否则使用 body
的 client
属性。
可以使用 resizeTo()
和 resizeBy()
方法调整窗口大小。resizeTo()
接收新的宽度和高度值,resizeBy()
接收缩放比例。
视口位置
度量文档相对于视口滚动距离的属性有两对,返回相等的值。
window.pageXoffset/window.scrollX
window.pageYoffset/window.scrollY
可以使用 scroll()
、scrollTo()
和 scrollBy()
方法滚动页面。三个方法都接收表示相对视口距离的 x 和 y 坐标。前两个方法中表示要滚动到的坐标,最后一个方法表示滚动的距离。
这几个方法也都接收一个 ScrollToOptions
字典,除了提供偏移值,还可以通过 behavior
属性告诉浏览器是否平滑滚动。
// 正常滚动
window.scrollTo({
left: 100,
top: 100,
behavior: 'auto'
});
// 平滑滚动
window.scrollTo({
left: 100,
top: 100,
behavior: 'smooth'
});
现代 JavaScript 教程
字符串
字符串的内部格式始终是 UTF-16
。它不依赖于页面编码。
反引号允许我们在第一个反引号之前指定一个模板函数 。
语法是:
func`string`
函数 func
被自动调用,接收字符串和嵌入式表达式,并处理它们。
访问字符的方法有方括号和调用 str.charAt(pos)
。区别是,没有找到时,方括号返回 undefined
,而函数调用返回一个空字符串。
字符串也可以使用 for-of
遍历。
查找子字符串的方法:
str.indexOf(substr, ?pos)
,从给定位置pos
开始,在str
中查找substr
。str.lastIndexOf(substr, pos)
旧代码中会使用
if (~str.indexOf(...))
来判断是否存在。因为~
符号对数字转换成 32-bit 整数,然后按位取反。所以 n 会转化为 -(n+1) 。计算机中负数以补码形式存在,如 1 的 32-bit 取反后正好是 -2 的补码形式。str.includes(substr, pos)
str.startsWith
str.endsWith
获取子字符串的方法:
slice(start, ?end)
,返回从 start 开始到但不包含的 end 的部分。支持负参数,表示从末尾开始。substring(start, ?end)
,返回字符串在 start 和 end 之间的部分,允许 start 大于 end 。不支持负参数。所有负参数被视为 0 。substr(start, ?length)
,返回从 start 开始的给定 length 部分。支持负参数。
比较字符串:
str.codePointAt(pos)
返回在pos
位置的字符代码。str.charCodeAt(pos)
str.fromCodePoint(code)
通过code
创建字符。str.fromCharCode(code)
str.localeCompare(str2)
会根据语言规则返回一个整数,若str
小于str2
则返回负数,若str
大于str2
正数,如果相等则返回0
。该方法还可以指定语言并设置区别大小之类的附加规则。
任务
首字母大写
写一个函数
ucFirst(str)
,并返回首字母大写的字符串str
,例如:ucFirst("john") == "John";
function ucFirst(str) { if (!str) return str; return str[0].toUpperCase() + str.slice(1); }
检查 spam
写一个函数
checkSpam(str)
,如果str
包含viagra
或XXX
就返回true
,否则返回false
。函数必须不区分大小写:
checkSpam('buy ViAgRA now') == true checkSpam('free xxxxx') == true checkSpam("innocent rabbit") == false
function checkSpam(str) { if (str.toLowerCase().includes('viagra') || str.toUpperCase().includes('XXX')) return true; return false; }
截断文本
创建函数
truncate(str, maxlength)
来检查str
的长度,如果超过maxlength
—— 应使用"…"
来代替str
的结尾部分,长度仍然等于maxlength
。函数的结果应该是截断后的文本(如果需要的话)。
例如:
truncate("What I'd like to tell on this topic is:", 20) = "What I'd like to te…" truncate("Hi everyone!", 20) = "Hi everyone!"
function truncate(str, maxlength) { if (str.length > maxlength) { return str.slice(0, maxlength - 1) + '…'; } return str; }
提取货币
我们有以
"$120"
这样的格式表示的花销。意味着:先是美元符号,然后才是数值。创建函数
extractCurrencyValue(str)
从字符串中提取数值并返回。例如:
alert( extractCurrencyValue('$120') === 120 ); // true
function extractCurrencyValue(str) { return +str.slice(1); }