# JavaScript 高级程序设计(第 4 版)

JavaScript 高级程序设计

# 第1章 什么是JavaScript

# 第2章 HTML中的JavaScript

# 第3章 语法

# 第4章 变量、作用域与内存

本章内容

  • 通过变量使用原始值与引用值
  • 理解执行上下文
  • 垃圾回收

正如ECMA-262所规定的,JavaScript变量是松散类型的,而且变量不过就是特定时间点一个特定值的名称而已

# 4.1 原始值与引用值

ECMAScript变量可以包含两种不同类型的数据:原始值和引用值。原始值(primitive value)就是最简单的数据,引用值(reference value)则是由多个值构成的对象。

在把一个值赋给变量时,JavaScript引擎必须确定这个值是原是指还是引用值。保存原始值的变量是**按值(by value)**访问的,因为我们操作的就是存储变量中的实际值。

引用值是保存在内存中的对象。与其他语言不同,JavaScript不允许直接访问内存地址,因此也就不能直接操作对象所在的内存空间。在操作对象时,实际上操作的是对该对象的**引用(reference)而非实际的对象本身。为此,保存引用值的变量是按引用(by reference)**访问的。

# 4.1.1 动态属性

  • 原始值不能有属性,尽管尝试给原始值添加属性也不会报错。但是访问原始值的属性都会为undefined
  • 只有引用值可以动态添加属性
let name = 'Nicholas'
name.age = 27
console.log(name.age) // undefined
1
2
3
  • 原始类型的初始化只使用原始字面量形式。即使使用了new关键字,则JavaScript会创建一个Object类型的实例,但是其行为类似于原始值。
let name = new String('Matt')
name.age = 26
console.log(name.age) // 26
console.log(typeof name)
1
2
3
4

# 4.1.2 复制值

  • 在通过变量把一个原始值赋值到另一个变量时,原始值会被复制到新变量的位置。
  • 复制引用值的变量时,存储在变量中的值也会被复制到新变量所在的位置。区别在于,此时复制的值实际上是一个指针,它指向存储在内存中的对象。

# 4.1.3 传递参数

  • ECMAScript 中的所有函数的参数都是按值传递的。

    函数外的值会被复制到函数内部的参数中,就像从一个变量复制到另一个变量一样。

    如果是原始值,那么就跟原始值变量的复制一样,如果是引用值,那么就跟引用值变量的复制一样。

  • 在按值传递参数时,值会被复制到一个局部变量(即一个命名参数,或者按照ECMAScript的话说,就是arguments对象中的一个槽位)。在按引用传递参数时,值在内存中的位置会被保存在一个局部变量,这意味着对本地的修改会反应到函数外部。

function addTen(num){
    num += 10
    return num
}
let count = 10
let result = addTen(count)
console.log(count) // 10 , 没有变化
console.log(result) // 20

function setName(obj){
    // 当函数内部给 obj 设置了 name 属性时,函数外部的对象也会反映这个变化,因为 obj 指向的对象保存在全局作用域的堆内存上
    obj.name = 'Nicholas'
}
let person = new Object()
setName(person)
console.log(person.name) // 'Nicholas'

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

为了证明参数为对象时,仍是按值传递的,再举一个例子:

function setName(obj){
	obj.name = 'Nicholas'
    obj = new Object()
    obj.name = 'Greg'
}
let person = new Object()
setName(person)
console.log(person.name) // 'Nicholas'
1
2
3
4
5
6
7
8

函数中参数的值改变之后,原始的引用仍然没变。当 obj 在函数内部被重写时,它变成了一个指向本地对象的指针。而那个本地对象在函数执行结束时就被销毁了。

# 4.1.4 确定类型

使用typeof来操作符最适合用来判断原始类型的数据。

TIP

ECMA-262规定,任何实现内部 [[Call]] 方法的对象都应该在 typeof 检测时返回 "function" 。

因为Chrome浏览器和Safari浏览器中的正则表达式实现了这个方法,所以 typeof 对正则表达式也返回 "function" 。在IE和Firef?x中, typeof 对正则表达式返回 "object" 。

let s = "Nicholas";
let b = true;
let i = 22;
let u;
let sb = Symbol()
let f = function(){}
console.log(typeof s);  // 'string'
console.log(typeof i);  // 'number'
console.log(typeof b);  // 'boolean'
console.log(typeof u);  // 'undefined'
console.log(typeof sb);  // 'symbol'
console.log(typeof f);  // 'function'
1
2
3
4
5
6
7
8
9
10
11
12

使用 instanceof操作符来判断引用类型的数据,如果变量是给定引用类型(由其原型链决定,将在第8章?细介绍)的实例,则 instanceof 操作符返回 true 。

// 具体语法如下
const result = variable instanceof constructor

// 示例
 const person = new Object()
 const colors: any = []
 const reg = /^\d{n}$/
 const date = new Date()
 function foo<T>(arg: T): T {
     return arg
 }

console.log(person instanceof Object) // true
console.log(colors instanceof Array) // true
console.log(reg instanceof RegExp) // true
console.log(date instanceof Date) // true
console.log(foo instanceof Function) // true
// 所有其它的引用类型返回都为true, 
console.log(colors instanceof Object) // true
console.log(reg instanceof Object) // true
console.log(date instanceof Object) // true
console.log(foo instanceof Object) // true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
  • 按照定义,所有引用值都是Object的实例,因此通过instanceof操作符检测任何引用值和Object构造函数都会返回true
  • 如果用instanceof检测原始值,则始终会返回false,因为原始值都不是对象。

# 4.2 执行上下文与作用域

JavaScript中有三种执行上下文类型:

  • 全局执行上下文—— 这是默认或者说基础的上下文,任何不在函数内部的代码都在全局上下文中。它会执行两件事:创建一个全局的window对象(浏览器环境下),并设置this的值等于这个全局对象。一个程序中只会存在一个全局执行上下文。
  • 函数执行上下文—— 每当一个函数调用时,都会为该函数创建一个新的上下文。每个函数都有它自己的执行上下文,不过是在函数被调用时创建的。函数上下文可以有任意多个。每当一个新的执行上下文被创建,它会按定义的顺序执行一系列步骤。
  • Eval函数执行上下文—— 执行在eval函数内部的代码也会有它属于自己的执行上下文,eval慎用,所以不做过多讨论。

# 4.2.1 作用域链增强

  • try/catch语句的catch块—— 会创建一个新的变量对象,这个变量对象会包含要抛出的错误对象的声明。
  • with语句—— 会向作用域链前端添加指定的对象。

# 4.2.2 变量声明

  • var —— 声明会被拿到函数或全局作用域的顶部,位于作用域中所有代码之前。这个现象叫作“提升”(hoisting)。

    如果存在函数声明和变量声明(注意:仅仅是声明,还没被赋值),而且变量与函数是同名的情况,函数的优先级更高一些。

    编译器询问作用域是否已经有同名的变量存在同一个作用域集合中,如果存在,编译器会忽略该声明;否则编译器会要求作用于在当前作用域集合中声明一个新的变量,并且命名。

  console.log(a)
  a()
  var a = 3
  function a() { console.log(10) }
  console.log(a)
  a = 6
  a()
  
  // 实际编译结果
  function a(){ alert(10) } // a: function
  var a 					// a: undefined
  alert(a)				    // 弹出 function
  a()					    // function a 执行:alert(10)	
  a = 3 					// a: 3
  alert(a)
  a = 6					    // a:6
  a()						// typeError
  
  // 最终执行结果
  // 'function a(){ alert(10) }'
  // 10
  // 3
  // typeError: a is not a function
  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
  • let—— 在同一作用域内不能声明多次。重复的 var 声明会被忽略,而重复的 let声明会抛出 SyntaxError 错误。

严格来讲,let在JavaScript运行中会被提升,但是由于“暂时性死区”的缘故,实际上不能在声明之前使用let变量。因此,从写JavaScript代码的角度说,let的提升跟var是不一样的。

  • const—— 使用 const 声明的变量必须同时初始化为某个值。一经声明,在其生命周期的 任何时候都不能再重新赋予新值,但对象的键(属性)则不受限制。 如果想让整个对象不可修改,可以使用Object.freeze(),这样再给属性值赋值时虽然不会报错,但是会静默失败。
const o = Object.freeze({})
o.name = 'Kyle'
console.log(o.name) // undefined
1
2
3

由于const声明暗示变量的值是单一类型且不可修改的,编译器可以将所有的实例替换成实际值,而不会通过查询表进行变量查找。谷歌V8引擎就执行这种优化。

  • 标识符查找—— 访问局部变量比访问全局变量要快,因为不用切换作用域。

如果局部上下文中有一个同名的标识符,那就不能在该上下文中引用父级上下文中的同名标识符,如下所示:

const color = 'blue'
function getColor(){
	const color = 'red'
	return color
}

console.log(getColor()) // red
1
2
3
4
5
6
7

# 4.3 垃圾回收

JavaScript是使用垃圾回收的语言,也就是说执行环境负责在代码执行时管理内存。通过自动内存管理实现内存分配和闲置资源回收:确定哪个变量不会再使用,然后释放它占用的内存。这个过程是周期性的,即垃圾回收程序每隔一定时间(或者说在代码执行过程中某个预定的收集时间)就会自动执行。

# 4.3.1 标记清理

垃圾回收程序运行的时候,会标记内存中储存的所有变量(记住,标记方法有很多种)。然后它会将所有在上下文中的变量,以及被在上下文中的变量引用的变量的标记去掉。在此之后再被加上标记的变量就是待删除的了,原因是任何在上下文中的变量都访问不到它们了。随后垃圾回收程序做一次内存清理,销毁带标记的所有值并收回它们的内存。

2008年后,几乎所有不同浏览器都采用标记清理策略,只是垃圾回收的频率有所差异。

# 4.3.2 引用计数

另一种垃圾回收策略引用计数,其思路就是对每个值都记录它被引用的次数。

  • 声明变量并且给它赋一个引用值时,这个值的引用数为1。
  • 如果同一个值又被赋给另一个变量,那么引用次数加1。
  • 如果保存该值的引用的变量被其它值给覆盖了,那么引用数减1。
  • 当一个值的引用次数为0时,说明无法再访问这个值了,因此就可以安全地回收其内存了。

引用计数容易遇到一个严重的问题:循环引用(套娃):

function problem(){
  let objA = new Object()
  let objB = new Object()
  objA.a = objB
  ObjB.b = objA
}
1
2
3
4
5
6

# 4.3.3 性能

无论什么时候开始收集垃圾,都能让它尽快结束工作。

现代浏览器回收程序基本上都是根据已分配对象的大小和数量来判断的。根据V8团队2016年的一篇博文的说法:“在一次完整的垃圾回收之后,V8的堆增长策略会根据活跃对象的数量外加一些余量来确定何时再次进行垃圾回收”

IE固定的内存分配数的策略,256个变量、4096个对象/数组或64kb字符串都会触发其垃圾回收程序。这样做很可能导致垃圾回收程序过于频繁,严重影响性能。这个问题一直到IE7才解决。

# 4.3.4 内存管理

出于安全考虑,系统中浏览器被分配到的内存往往比桌面软件要少的多,原因是避免运行大量JavaScript的网页耗尽系统内存而导致操作系统崩溃。

  • 优化内存的最佳手段就是保证在执行代码时只保存必要的数据,如果数据不再必要,则将它设置为null
  • 使用constlet,因为这两个声明符都是以块而非函数为作用域。
  • 尽量避免对象进行动态属性的增删,建议将要添加的属性先显示定义,或者在将要删除的属性值设置为null
  • 内存泄漏容易常见于全局变量、定时器以及闭包等几个场景。
  • 使用对象池和静态分配的手段是一种相对极端的优化方式,需要权衡。

# 4.4 小结

  • JavaScript 变量可以保存两种类型的值:原始值和引用值。原始值类型包含6种:UndefinedNullBooleanNumberStringSymbol

    • 原始值大小固定,因此保存在栈内存
    • 从一个变量到另一个变量复制原始值会创建该值的第二个副本。
    • 引用值是对象,存储在堆内存上。
    • 包含引用值的变量实际上只包含指向相应对象的一个指针地址,而不是对象本身。
    • 从一个变量到另一个变量复制引用值只会复制指针,因此结果是两个变量都指向同一个对象。
    • typeof操作符可以确定值的原始类型,而instanceof操作符用于确保值的引用类型。
  • 任何变量(不论是原始值还是引用值)都存在于某个执行上下文中(也称为作用域)。这个上下文决定了变量的生命周期,以及它们可以访问代码的哪些部分。

    • 执行上下文分为全局上下文、函数上下文和块级上下文。
    • 代码执行流每进入一个新的上下文,都会创建一个作用域链,用于搜索变量和函数。
    • 函数或块的局部上下文不仅可以访问自己作用于内的变量,也可以访问任何包含(父级)上下文乃至全局上下文中的变量。
    • 全局上下文只能访问全局上下文中的变量和函数,不能直接访问局部上下文中的任何数据。
    • 变量的执行上下文用于确定什么时候释放内存。
  • JavaScript是使用垃圾回收的编程语言,开发者不需要操心内存分配和回收。

    • 离开作用域的值会被自动标记为可回收,然后 在垃圾回收期间被删除。
    • 主流的垃圾回收算法是标记清理,即先给当前不使用的值加上标记,再回来回收它们的内存。
    • 引用计数是另一种垃圾回收策略,需要记录值被引用了多少此。JavaScript引擎不再使用这种算法,但是某些旧版本IE仍会受这种算法影响,原因是JavaScript会访问非原生JavaScript对象(如DOM元素)。
    • 引用计数在代码中存在循环引用时会出现的问题。
    • 解除变量的引用不仅可以消除循环引用,而且对垃圾回收也有帮助。为促进内存回收,全局对象及其属性和循环引用都应该在不需要时解除引用。

# 第5章 基本引用类型

# 第6章 集合引用类型

# 第7章 迭代器与生成器

# 第8章 对象、类与面向对象编程

# 8.1 理解对象

# 8.2 创建对象

# 8.3 继承

# 8.4 类

# 8.5 小结

# 第9章 代理与反射

# 第10章 函数

本章内容

  • 函数表达式、函数声明及箭头函数
  • 默认参数及扩展操作符
  • 使用函数实现递归
  • 使用闭包实现私有变量

函数实际上也是个对象。每个函数都是Function类型的实例,而Function也有属性和方法,跟其它引用类型一样。函数名就是指向函数对象的指针,而且不一定与函数本身紧密绑定。

函数的定义:

  • 使用function关键字定义
function sum (num1, num2) {
	return num1 + num2
}
1
2
3
  • 使用变量名定义一个匿名函数
let sum = function(num1, num2) {
return num1 + num2
}
1
2
3
  • 使用Function构造函数定义**(不推荐)**

不推荐使用构造函数这种语法来定义函数,因为这段代码会被解释执行两次:第一次是将它当作常规ECMAScript代码,第二次是解释传给构造函数的字符串。这显然会影响性能。不过,把函数想象成对象,把函数名称想象成指针是很重要的。而上面这种语法很好的诠释了这些概念。

  let sum = new Function("num1", "num2", "return  num1 + num2")
1

# 10.1箭头函数

  • 使用箭头函数实例化的函数对象与“正式的”函数表达式创建的函数对象行为是相同的。任何可以使用函数表达式的地方,都可以使用箭头函数;
let arrowSum = ( a,  b ) => {
	return a +b
}

let functionExpressionSum = function(a, b){
    return a + b
}

console.log(arrowSum(5, 8));  // 13
console.log(functionExpressionSum(5, 8));  // 13
1
2
3
4
5
6
7
8
9
10
  • 箭头函数简洁的衣服啊非常适合嵌入函数的场景。
let ints = [1, 2, 3]

console.log(ints.map(i => i + 1)) // [2, 3, 4]
console.log(ints.map(function(i){ return i +1 })) // [2, 3, 4]
1
2
3
4

如果只有一个参数,那也可以不用括号。只有没有参数,或者多个参数的情况下,才需要使用括号。

箭头函数虽然语法简洁,但也有很多场景不适用。

  • 箭头函数不能使用argumentssuper、和new.target
  • 不能用做构造函数;
  • 没有prototype属性。

# 10.2 函数名

因为函数名就是指向函数的指针,所以它们跟其他包含对象指针的变量具有相同的行为。这意味着一个函数可以有多个名称,如下所示:

function sum (num1, num2) {
	return num1 + num2
}
console.log(sum(10, 10)) // 20

let anotherSum = sum
console.log(anotherSum(10, 10)) // 20

sum = null 
console.log(anotherSum(10, 10)) // 20
1
2
3
4
5
6
7
8
9
10

所有的函数对象都会暴露一个只读的name属性,其中包含关于函数的信息。多数情况下,这个属性中保存的就是一个函数标识符,或者说是一个字符串化的变量名。即使函数没有名称,也会如实显示成空字符串。如果是使用Function构造函数创建的,则会表示成"anonymous"。

function foo() { }
let bar = function() {}
let baz = () => ({})

console.log(foo.name) // 'foo'
console.log(bar.name) // 'bar'
console.log(baz.name) // 'baz' 
console.log((()=>{}).name) // ''
console.log((new Function()).name) // 'anonymous'
1
2
3
4
5
6
7
8
9

如果函数是一个获取函数、设置函数,或者使用bind()实例化,那么标识符前面会加上一个前缀:

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.getOwnPropertyDescriptor(dog, 'age')

console.log(propertyDescriptor?.get?.name) // 'get age'

console.log(propertyDescriptor?.set?.name) // 'set age'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 10.3 理解参数

ECMAScript函数的参数在内部表现为一个类数组对象(非Array的实例)。函数被调用时总会接收一个数组,但是函数本身并不关心数组中包含了什么。在使用function关键字定义(非箭头)函数时,可以在函数内部访问arguments对象,从中取得传进来的每个参数值。

arguments可以使用下标索引来访问(arguments[0]arguments[1]),使用arguments.length来访问参数的数量。

function howManyArgs() {
    console.log(arguments[0], arguments[1], arguments.length)
}

howManyArgs() // undefined, undefined , 0
howManyArgs("string", 45) // 'string', 45 , 2
1
2
3
4
5
6

aguments对象可以与形参一起使用,比如:

function doAdd(num1, num2) {
	if(arguments.length === 1) {
        console.log(num1 + 10)
    } else if(arguments.length === 2) {
        console.log(arguments[0] + num2)
    }
}
1
2
3
4
5
6
7

非严格模式下,aguments对象的值始终会与对应的参数同步,比如:

function doAdd(num1, num2){
    arguments[1] = 10
    console.log(arguments[0] + num2)
}

doAdd(20, 20) // 30
doAdd(20) // NaN
1
2
3
4
5
6
7
  • num2arguments[1]的内存地址并不一样,只不过会保持同步而已;
  • 修改形参num2不会影响arguments[1]对象中相应的值;
  • 如果没有显式的传入形参,通过设置arguments[1]索引设置并不会映射到第二个形参上,因为arguments对象的长度是根据调用函数时传入的参数个数,而非定义函数时给出的命名参数个数确定的
  • 严格模式下,重写arguments对象会导致语法错误。

箭头函数内访问内arguments关键字会报错,但是可以访问其包装函数的arguments对象,比如:

const foo = () => {
    console.log(arugments[0])
}
foo(5)  // ReferenceError: arugments is not defined


function foo() {
   const bar = () => {
        console.log(arugments[0]) 
   } 
}
foo(5) // 5
1
2
3
4
5
6
7
8
9
10
11
12

# 10.4 没有重载

因为ECMAScirpt函数的参数是由包含零个或多个值表示的。没有函数签名,自然也没有重载。

function foo() {
    console.log('foo')
}

function foo(str){
    console.log(str)
}

// 相当于
let foo = function() {
    console.log('foo')
}

foo = function(str) {
    console.log(str)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 10.5 默认参数值

在ECMAScript5.1及之前,实现默认参数的一种常用方式就是检测某个参数是否等于undefined

function makeKing(name) {
    neme = (typeof name !== 'undefined') ? name : 'Kyle'
    return `King ${name } VIII`
}
console.log(makeKing()) // 'King Henry VIII'
console.log(makeKing('Louis')) // 'King Louis VIII'
1
2
3
4
5
6

在ECMAScript6之后支持显式定义默认参数:

function makeKing(name = 'Henry') {
	return `King ${name} VIII`;
}
console.log(makeKing()) // 'King Henry VIII'
console.log(makeKing('Louis')) // 'King Louis VIII'
1
2
3
4
5

# 10.6 参数扩展与收集

# 10.7 函数声明与函数表达式

# 10.8 函数作为值

# 10.9 函数内部

# 10.10 函数属性与方法

# 10.11 函数表达式

# 10.12 递归

# 10.13 尾调用优化

# 10.14 闭包

# 10.15 立即调用的函数表达式

# 10.16 私有变量

# 10.17 小结

# 第11章 期约与异步函数

本章内容

  • 异步编程
  • 期约
  • 异步函数

# 11.1 异步编程

同步行为和异步行为的对立统一是计算机科学的一个基本概念。特别是JavaScript这种单线程事件循环模型中,同步操作与异步操作更是代码索要依赖的核心机制。异步行为是为了优化因计算量大而时间长的操作。如果在等待其他操作完成的同时,即使运行其他指令,系统也能保持稳定,那么这样做就是务实的。

重要的是,异步操作并不一定计算量大或要等很长时间。只要你不想为等待某个异步操作而阻塞线程执行,那么任何时候都可以使用。

# 11.1.1 同步与异步

// sync
let x = 3
x = x + 4

// async 
let x = 3
setTimeout(()=>x = x + 4, 1000)
1
2
3
4
5
6
7
  • 同步行为对应内存中顺序执行的处理器命令。在程序执行的每一步,都可以推断出程序的状态。这是因为后面的指令总是在前面的指令完成后才会执行。
  • 异步行为类似于系统中断,即当前进程外部的实体可以触发代码执行。异步代码不容易推断。

虽然例子中的两个结果没有什么区别,但是第二个指令块(加操作和赋值操作)是由定时器触发的,这会生成一个入队执行的中断。到底什么时候触发这个中断,这对JavaScript运行时来说是一个黑盒,因此实际上无法预知。

# 11.1.2 以往的异步编程模式

// 给异步操作提供相应成功和失败的处理回调
function double(value, success, failure){
    setTimeout(() =>{
        try{
            if(typeof value !== 'number'){
                throw 'Must provide number as first argument'
            }
            success(value * 2)
        }catch(e){
            failure(e)
        }
    })
}
1
2
3
4
5
6
7
8
9
10
11
12
13

这种模式已经不可取了,因为必须在初始化异步操作时定义回调。异步函数的返回值只在短时间内存在,只有预备好将这个短时间内存在的值作为参数的回调才能接收到它。

如果异步返回值又依赖另一个异步返回值,很容易造成嵌套回调,随着代码越来越复杂,回调策略是不具有扩展性的。回调地狱这个称呼由此产生的。

# 11.2 期约Promise

  • promise这个名字最早是又Daniel Friedman 和 David Wise在他们1976年论文《The Impact of Applicative Programming on Multiprocessing(应用程序设计编程对多处理的影响)》中提出来的。
  • Barbara Liskov 和 Liuba Shrira 在1988年发表论文,这个概念才真正确立下来。
  • 同一时期,类似的概念术语还有"终局(eventual)"、"期许(future)"、"终局(delay)"和"终局(deferred)"等,它们都是描述一种异步程序执行的机制。

# 11.2.1 Promise/A+规范

  • 早期的期约机制在jQueryDojo中以Deferred API形式出现的。
  • 2010年,CommonJS项目实现的Promise/A规范
  • 2012年,Promise/A+组织fork了CommonJS的Promise/A建议,并制定了Promise/A+规范。最终成为ECMAScript规范实现的范本。

jQuery中的Deferred对象

# 11.2.2 期约基础

  • 期约状态机

期约是一个有状态的对象,可能处于如下3种状态之一:

  1. 待定(pending)
  2. 兑现(fulfilled或resolved)
  3. 拒绝(rejected)

待定(pending)是期约的最初状态。期约可以落定(settled)为代表成功的兑现(fulfilled)状态,或是代表失败的拒绝(rejected)状态。无论落定哪个状态都是 不可逆的

期约的状态是私有的,不能通过外部JavaScript代码修改。这主要是为了避免根据读到的期约状态,以同步方式处理期约对象。

  • 解决值、拒绝理由和期约用例

期约主要是抽象地表示一个异步操作,它会实际的产生某个值,而程序期待期约状态改变时可以访问这个值。每个期约只要状态切换为“兑现”,就会有一个私有的内部值(result),类似的,切换为拒绝(rejected)时 ,会有一个私有的内部理由(reason)。无论是值还是理由,都是包含原始值或对象的不可修改的引用。二者是可选的,而且默认值都为undefined。在期约到达某个落定状态执行的异步代码始终会收到这个值或理由。

  • 通过执行函数控制期约状态
new Promise(() => setTimeout(console.log, 0, 'executor'))
setTimeout(console.log, 0 , 'promise initialized')
// 'executor'
// 'promise initialized'


// 添加setTimeout推迟切换状态
let p1 = new Promise((resolve, reject) => setTimeout(resolve, 1000))
let p2 = new Promise((resolve, reject) => setTimeout(reject, 1000))
// 在console.log打印期约的时候,还不会执行reolve()
console.log(setTimeout(console.log, 0 , p1)) // Promise {<fulfilled>: undefined}
console.log(setTimeout(console.log, 0 , p2)) // Promise {<rejected>: undefined}
1
2
3
4
5
6
7
8
9
10
11
12

无论resolve()reject()中的哪个被调用,状态转换都不可撤销了。于是继续修改状态会静默失败。

let p = new Promise((resolve, reject) => {
    resolve()
    reject() // 没有效果
})
1
2
3
4
  • Promise.resolve()/Promise.reject()
  1. 通过调用Promise.resolve()静态方法,可以实例化一个解决的期约。

     // 下面两个实例实际是一样的
     let p1 = new Promise((resolve, reject) => resolve())
     let p2 = Promise.resolve()
    
    1
    2
    3
  2. 只接收一个参数,剩余的参数会被忽略

    setTimeout(console.log, 0, Promise.resolve(4, 5, 6)) // Promise {<fulfilled>: 4}
    
    1
  3. 如果参数本身是一个期约,那它的行为就类似于一个空包装。因此,Promise.resolve()可以说是一个幂等方法:

    let p = Promise.resolve(7)
    setTimeout(console.log, 0, p === Promise.resolve(p)) // true
    setTimeout(console.log, 0, p === Promise.resolve(Promise.resolve(p))) // true
    
    1
    2
    3
  4. 最好将预期值放入对应它的静态方法不要将兑现值放入Promise.resolve(),错误对象等非预期的值放入Promise.reject()。

  5. Promise的设计很大程度上会导致一种完全不同于JavaScript的计算模式。因为这里没有通过异步模式捕获错误。期约本身是同步对象,但也是异步执行模式的媒介。下面的例子中,拒绝期约的错误并没有抛到同步代码的线程里,而是进入浏览器异步消息队列里处理的。因此,try/catch块并不能捕获该错误。

    try{
        throw new Error('foo')
    }catch(e){
        console.error(e) // Error: 'foo'
    }
    
    
    try{
        Promise.reject(new Error('bar'))
    }catch(e){
        console.error(e) // 不会执行
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12

# 11.2.3 期约的实例方法

期约实例的方法是连接外部同步代码与内部异步代码之间的桥梁。执行方法可以访问异步操作返回的数据,处理期约成功和失败的结果,连续对期约求值,或者添加只有期约进入终止状态时才会执行的代码。

  • 实现Thenable接口

  • Promise.prototype.then()

    • 接收两个参数onResolvedonRejected处理程序,分别对于“兑现”和“拒绝”状态时执行。
    • onResolvedonRejected两个操作是互斥的。
    • 任何传入then()方法的非函数类型的参数都会被静默忽略。
    • 它会返回一个新的期约实例
    • 如果没有显示的返回语句,则Promise.then()会包装默认的返回值undefined
    function onRejected(id){
        setTimeout(console.log, 0, id, 'rejected')
    }
    
    let p1 = new Promise((resolve, reject) => setTimeout(resolve, 3000))
    let p2 = new Promise((resolve, reject) => setTimeout(reject, 3000))
    
    // 不推荐, 非函数处理程序会被默认忽略
    p1.then('gobbeltygook')
    
    // 推荐写法
    p2.then(null, () => onRejected('p2'))
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    let p1 = new Promise.resolve('foo')
    
    // 若调用then()时不传处理程序,则会原样向后传,虽然这样做并没什么意义
    let p2 = p1.then()
    setTimeout(console.log, 0 ,p2) // Promise<resolved> : 'foo'
    
    // 以下几个结果都一样
    let p3 = p1.then(() => undefined)
    let p4 = p1.then(() => {})
    let p5 = p1.then(() => Promise.resolve())
    setTimeout(console.log, 0 ,p3) // Promise<resolved> : undefined
    setTimeout(console.log, 0 ,p4) // Promise<resolved> : undefined
    setTimeout(console.log, 0 ,p5) // Promise<resolved> : undefined
    
    // 以下几个结果都一样
    let p6 = p1.then(() => 'bar')
    let p7 = p1.then(() => Promise.resolve('bar'))
    setTimeout(console.log, 0 ,p3) // Promise<resolved> : 'bar'
    setTimeout(console.log, 0 ,p4) // Promise<resolved> : 'bar'
    
    // Promise.resolve()保留返回的期约
    let p8 = p1.then(() => new Promsise(() => {}))
    let p9 = p1.then(() => Promise.reject())
    // Uncauht (in promise): undefined
    
    setTimeout(console.log, 0, p8) // Promise<pending>
    setTimeout(console.log, 0, p9) // Promise<rejected> : undefined
    
    // 抛出异常则会返回拒绝的期约
    let p10 = p1.then(() => { throw 'baz'})
    // Uncauht (in promise): undefined
    
    setTimeout(console.log, 0, p9) // Promise<rejected> : 'baz'
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
  • Promise.prototype.catch()

    该方法用于给期约添加拒绝处理程序。它只接收一个参数:onRejected函数。事实上,这个方法就是一个语法糖,调用它就相当于调用了Promise.prototype.then(null, onRejected)

    let p = Promise.reject()
    let onRejected = function(e) {
        setTimeout(console.log, 0, 'rejected')
    }
    // 下面两个用例结果是一样的
    p.then(null, onRejected) // rejected
    p.catch(onRejected)      // rejected
    
    1
    2
    3
    4
    5
    6
    7
  • Promise.prototype.finally()

    • 这个方法在期约转换为解决或拒绝状态时执行,避免在onResolvedonRejected处理程序中出现冗余代码。
    • 与状态无关,大多数情况下表现为对父期约的传递。
  • 非重入期约的方法

# 11.2.4 期约连锁

# 11.2.4 期约扩展

# 11.3 异步函数

# 11.4 小结

# 第12章 BOM

# 第13章 客户端检测

# 第14章 DOM

# 第15章 DOM扩展

# 第16章 DOM2和DOM3

# 第17章 事件

# 第18章 动画与Canvas图形

# 第19章 表单脚本

# 第20章 JavaScript API

# 第21章 错误处理与调试

# 第22章 处理XML

# 第23章 JSON

# 第24章 网络请求与远程资源

# 第25章 客户端存储

# 第26章 模块

# 第27章 工作者线程

# 第28章 最佳实践

最近更新时间: 11/27/2020, 5:42:30 PM