《JavaScript 高级程序设计》第8-9章 函数&期约与异步函数

2024年03月12日

8.函数

  • ECMAScript 中的函数实际上是 Function 对象的实例,而函数名则是指向函数对象的指针,并且不一定和函数本身紧密绑定
  • ES6 新增了使用 => 语法定义函数表达式的能力,箭头函数不能使用 arguments、super 和 new.target ,也不能用作构造函数。此外,箭头函数也没有 prototype 属性
  • ES6 中所有的函数对象都会暴露一个只读的 name 属性,其中包括关于函数的信息。如果函数是一个 get 、set 或使用 bind 实例化的函数,name 属性标识符前会加上一个前缀
javascript 复制代码
function foo() {}
console.log(foo.bind(null).name) // bound foo

let dog = {
  years: 1,
  get age() {
    return this.years
  }
  set age(newAge) {
    this.years = newAge
  }
}
let propertyDescriptor = Object.getPropertyDescriptor(dog, 'age')
console.log(propertyDescriptor.get.name) // get age
console.log(propertyDescriptor.set.name) // set age
  • ECMAScript 函数的参数在内部表现为一个数组,所以函数及不关心传入的参数个数,也不关心参数类型。使用 function 关键字定义函数时,可以在内部访问 arguments 对象取得传入的每个参数值
  • ECMAScript 中所有参数都是按值传递
  • ES6支持显式定义默认参数,在参数后面用 = 就可以为参数赋默认值。默认值不限于原始值和对象,也可以使用调用函数返回的值(在函数被调用时求值)
  • 多个参数定义默认值和使用 let 顺序声明变量一样,也遵循“暂时性死区原则”,并且存在于自己的作用域中
  • ES6 新增了扩展操作符 ...,既可以用用于调用函数时传参,也可以用于定义函数参数
  • 函数声明会在任何代码执行之前被读取并添加到执行上下文,这个过程叫做函数声明提升,函数表达式则不会
  • arguments 对象是一个类数组对象,有一个 callee 属性值为指向其所在函数的指针
  • 标准函数中 this 引用的是把函数当成方法调用的上下文对象,称为 this 值;箭头函数中 this 引用的是函数定义时的上下文对象
  • ES6 新增了判断函数是否使用 new 关键字调用的 new.target 属性,如果使用 new 关键字调用,则该属性引用被调用的构造函数
  • 所有的函数都是对象,所以有自己的属性和方法:
    • length 属性:保存函数定义的命名参数的个数
    • prototype 属性:保存引用类型所有实例的方法(toString、valueOf 等)
    • apply 方法:接收两个参数,分别为函数内 this 的值和一个参数数组
    • call 方法:与 apply 类似,接收函数内 this 的值以及若干个参数(逐个传入)
    • bind 方法:创建一个新的函数实例,其 this 值绑定到传给 bind 的对象
  • 闭包指的是引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现的。由于闭包会保留它们包含函数的作用域,所以更加占用内存
  • 立即调用的匿名函数称为立即调用的函数表达式(IIFE)。在 ES6 块级作用域出现之前,使用 IIFE 可以模拟块级作用域
  • 私有属性是常规的类的公有属性(字段、方法)的对应。私有属性通过添加#前缀来创建,在类的外部无法合法地引用
  • 私有属性不是原型继承模型的一部分,因为它们只能在当前类内部被访问,而且不能被子类继承。不同类的私有属性名称之间没有任何交互。它们是附加在每个实例上的外部元数据,由类本身管理。因此,Object.freeze()Object.seal() 对私有属性没有影响

9.期约与异步函数

异步编程

  • 同步行为对应内存中顺序执行的处理器指令;异步行为类似于系统中断,即当前进程外部的实体可以触发代码执行
  • 早期 JavaScript 中使用定义回调函数来表明异步操作完成。串联多个异步操作通常需要深度嵌套的回调函数(回调地狱)来解决

期约(Promise)

期约基础

ES6 新增的引用类型 Promise,可以通过 new 操作符来实例化并传入执行器(executor)函数作为参数。
期约是一个有状态的对象,处于以下3种状态之一:

  1. 待定(pending)
  2. 兑现(fulfilled,也称“解决”,resolved)
  3. 拒绝(rejected)

待定是期约的最初始状态。在待定状态下,期约可以落定(settled)为代表成功的兑现状态或代表失败的拒绝状态。期约的状态是私有的,不能直接通过 JavaScript 检测到。
期约主要有两大用途。首先是抽象地表示一个异步操作,通过期约的状态表示期约是否完成。其次,期约封装的异步操作会实际生成某个值,在程序期待期约状态改变时可以访问这个值,如果被拒绝则期待拿到拒绝的理由。
为了支持上述用例,每个期约只要状态切换为兑现,就会有一个私有的内部值(value)。如果状态切换为拒绝则会有一个私有的内部理由(reason)。无论是值还是理由都是包含原始值或对象的不可修改的引用。
由于期约的状态是私有的,所以只能在期约的执行器函数中进行操作。执行器函数主要有两项职责:初始化期约的异步行为和控制状态的最终转换。控制状态转换时通过调用执行器函数的两个参数 resolve 和 reject 实现。

javascript 复制代码
let p1 = new Promise((resolve, reject) => resolve())
setTimeout(console.log, 0, p1) // Promise <resolved>
let p2 = new Promise((resolve, reject) => reject())
setTimeout(console.log, 0, p2) // Promise <rejected>

可以使用 Promise.resolve 和 Promise.reject 实例化一个兑现或拒绝的期约实例。

期约的实例方法

期约的实例方法是连接外部同步代码和内部异步代码之间的桥梁。
Promise.prototype.then 是为期约添加处理程序的主要方法。接收两个参数:onResolved 处理程序和 onRejected 处理程序,这两个参数都是可选的,如果提供则会在期约分别进入兑现和拒绝状态时执行。

javascript 复制代码
let p1 = new Promise((resolve, reject) => setTimeout(resolve, 3000))
let p2 = new Promise((resolve, reject) => setTimeout(reject, 3000))

p1.then(() => console.log('resolved'))
p2.then(null, () => console.log('rejected'))

调用 then 方法会创建一个新的期约实例,基于 onResolved 处理程序的返回值构建。如果没有处理程序则包装上一个期约解决后的值,如果没有显式的返回语句则包装默认的返回值 undefined 。
Promise.prototype.catch 用于给期约添加拒绝处理程序,这个方法实际上是 Promise.prototype.then(null, onRejected) 的语法糖。
Promise.prototype.finally 同于给期约添加 onFinally 处理程序,在期约切换为兑现或拒绝状态时都会执行。
当期约进入落定状态时,与该状态相关的处理程序仅仅会被排期,而非立即执行。跟在添加这个处理程序之后的同步代码一定会在处理程序之前先执行。这个特性称为非重入(non-reentrancy)

javascript 复制代码
let p = Promise.resolve()
p.then(() => console.log('onResolved handler'))
console.log('then() returns')
// 输出
// then() returns
// onResolved handler

如果给期约添加了多个处理程序,当期约状态变化时,相关处理程序会按照添加它们的顺序依次执行。
到落定状态后,期约会提供值或理由给相关状态的处理程序。在执行函数中,值和理由分别作为 resolve 和 reject 的第一个参数向后传递。在处理程序中,作为 onResolved 和 onRejected 处理程序的唯一参数接收。

期约连锁与期约合成

期约连锁指把期约逐个地串联起来,是一种非常有用的编程模式。之所以可以实现是因为每个期约实例的方法都会返回一个新的期约对象。
Promise 类提供将多个期约实例合成一个期约的静态方法:Promise.all 和 Promise.race 。

  • Promise.all 创建的期约会在一组期约全部解决之后再解决。接收一个迭代对象,返回一个新的期约。如果至少有一个包含的期约待定,则合成的期约待定;如果有一个包含的期约拒绝,则合成的期约拒绝且第一个拒绝的期约会将自己的理由作为合成的期约的理由,合成的期约会静默处理所有包含期约的拒绝操作;如果所有期约都成功解决,则合成期约的解决值就是所有包含期约解决值的数组
  • Promise.race 返回一个包装期约,是一组集合中最先解决或拒绝的期约的镜像。接收一个迭代对象返回一个新期约。

异步函数

async 关键字用于声明异步函数,如果异步函数使用 return 关键字返回了值,这个值会被 Promise.resolve 包装成一个期约对象。
await 关键字必须在异步函数中使用,用于暂停异步函数代码的执行,等待期约解决。JavaScript 运行时在碰到 await 关键字时,会记录在哪里暂停执行。等到 await 右边的值可用,JavaScript 运行时会向消息队列中推送一个任务恢复异步函数的执行。因此,即使 await 后面跟着一个立即可用的值,函数的其余部分也会被异步求值。

javascript 复制代码
async function foo() {
  console.log(2)
  await null
  console.log(4)
}
console.log(1)
foo()
console.log(3)

// 1
// 2
// 3
// 4

使用异步函数实现 sleep :

javascript 复制代码
async function sleep(delay) {
  return new Promise((resolve) => setTimeout(resolve, delay))
}
相关文章

Vite项目配置本地HTTPS

React Native 开发环境安装踩坑

《JavaScript 高级程序设计》第10-16章