0%

学习笔记 2020 11 01

学习笔记 2020-11-01

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

期约与异步函数

异步函数

异步函数策略
  1. 实现 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
  2. 利用平行执行

    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 按顺序收到了每个期约的值。

  3. 串行执行期约

    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 传递了每个函数的返回值。

  4. 栈追踪与内存管理

    期约与异步函数的功能有相当程度的重叠,但它们在内存中的表示则差别很大。

    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 对象的位置可以通过不同的属性和方法来确定。现代浏览器提供了 screenLeftscreenTop 属性,用于表示窗口相对于屏幕左侧和顶部的位置。

可以使用 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

outerWidthouterHeight 返回浏览器窗口自身的大小。( 不管是在最外层 window 上使用,还是在窗格 <frame> 中使用 )。

innerWidthinnerHeight 返回浏览器窗口中页面视口的大小( 不包含浏览器边框和工具栏 )。

document.documentElement.clientWidthdocument.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 属性,不存在的话,检查页面是否处于标准模式,是的话使用 documentElementclient 属性,否则使用 bodyclient 属性。

可以使用 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 。该方法还可以指定语言并设置区别大小之类的附加规则。

任务

  1. 首字母大写

    写一个函数 ucFirst(str),并返回首字母大写的字符串 str,例如:

    ucFirst("john") == "John";
    function ucFirst(str) {
      if (!str) return str;
      return str[0].toUpperCase() + str.slice(1);
    }
  2. 检查 spam

    写一个函数 checkSpam(str),如果 str 包含 viagraXXX 就返回 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;
    }
  3. 截断文本

    创建函数 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;
    }
  4. 提取货币

    我们有以 "$120" 这样的格式表示的花销。意味着:先是美元符号,然后才是数值。

    创建函数 extractCurrencyValue(str) 从字符串中提取数值并返回。

    例如:

    alert( extractCurrencyValue('$120') === 120 ); // true
    function extractCurrencyValue(str) {
      return +str.slice(1);
    }