前端面试宝典总结3-JavaScript(1)

发布于:2024-06-04 ⋅ 阅读:(157) ⋅ 点赞:(0)

前端面试宝典总结之JavaScript(1)

本文章 对各大学习技术论坛知识点,进行总结、归纳自用学习,共勉🙏

上一篇👉: 前端面试宝典总结2-CSS(2)

1. JavaScript有哪些数据类型,它们的区别?

数据类型 类别 描述 特性 例子
布尔值 (Boolean) 原始数据类型 表示真或假的逻辑值,只有true和false两种取值 let isTrue = true; -
数字 (Number) 原始数据类型 表示数值,包括整数和浮点数 let age = 25; -
字符串 (String) 原始数据类型 表示文本数据,由零个或多个字符组成 let greeting = “Hello”; -
undefined 原始数据类型 表示未赋值的变量或未定义的属性的默认值 let uninitialized; -
null 原始数据类型 表示空值或意图上的"无",用于清空对象引用 let empty = null; -
Symbol 原始数据类型 ES6引入,表示唯一且不可变的原始值,常用于对象的键 let uniqueKey = Symbol(); -
对象 (Object) 引用数据类型 包含属性(键值对)和方法的集合,可以嵌套其他数据类型 let person = {name: “Alice”}; -
数组 (Array) 引用数据类型 有序的值集合,元素可以是任何数据类型,长度可变 let numbers = [1, 2, 3]; -
函数 (Function) 引用数据类型 可重复使用的代码块,可以接受参数并返回值,也可作为对象的方法 function sayHi() { alert(“Hi”); } -

JavaScript有以下数据类型:
(1)原始数据类型(Primitive data types):

  • 布尔值(Boolean): 表示真或假的值。只有两个可能的值:true(真)和false(假)。
  • 数字(Number): 表示数值。可以是整数或浮点数。
  • 字符串(String): 表示文本数据。由字符组成的一串字符序列。
  • undefined: 表示未定义的值。当变量被声明但未赋值时,默认为 undefined。
  • null: 表示空值或不存在的对象。
  • Symbol: 在 ES6 中引入的新数据类型,表示唯一的、不可变的值。

(2)引用数据类型(Reference data types):

  • 对象(Object): 表示键值对的集合。可以包含其他数据类型的属性和方法。
  • 数组(Array): 表示有序的值的集合。可以包含多种数据类型的元素。
  • 函数(Function): 是一段可重复调用的代码块。

这些数据类型之间的区别在于它们的特性和用途:

  • 原始数据类型是简单的数据类型,它们是不可变的,即它们的值不能被修改。
  • 引用数据类型是复杂的数据类型,它们是可变的,即可以修改它们的值。
  • 原始数据类型的赋值是通过复制值本身来进行的。当将一个原始数据类型的值赋给另一个变量时,它们之间是独立的,修改其中一个不会影响另一个。
  • 引用数据类型的赋值是通过引用来进行的。当将一个引用数据类型的值赋给另一个变量时,它们共享同一个引用,修改其中一个会影响另一个。
  • 原始数据类型在比较时是按值进行比较,即比较它们的实际值。
  • 引用数据类型在比较时是按引用进行比较,即比较它们是否引用同一个对象。

特性总结
原始数据类型:值不可变,赋值是值传递,比较按值进行。
引用数据类型:值可变,赋值是引用传递,比较按引用进行,即比较是否指向内存中同一地址。

2.数据类型检测的方式有哪些

(1)typeof
console.log(typeof 1);               // number
console.log(typeof true);            // boolean
console.log(typeof 'str');           // string
console.log(typeof {});              // object
console.log(typeof []);              // object   [] 在JavaScript中也被视为一种对象类型 
console.log(typeof function(){});    // function
console.log(typeof undefined);       // undefined
console.log(typeof null);            // object 被认为是JavaScript的一个设计缺陷 在javaScript最初的设计中,null 被视为对象类型的空值

其中数组、对象、null都会被判断为object,其他判断都正确。

(2)instanceof

instanceof可以正确判断对象的类型,其内部运行机制是判断在其原型链中能否找到该类型的原型。

console.log(2 instanceof Number);                    // false
console.log(true instanceof Boolean);                // false 
console.log('str' instanceof String);                // false 
 
console.log([] instanceof Array);                    // true
console.log(function(){} instanceof Function);       // true
console.log({} instanceof Object);                   // true

instanceof只能正确判断引用数据类型,而不能判断基本数据类型。instanceof 运算符可以用来测试一个对象在其原型链中是否存在一个构造函数的 prototype 属性。

(3) constructor
console.log((2).constructor === Number); // true
console.log((true).constructor === Boolean); // true
console.log(('str').constructor === String); // true
console.log(([]).constructor === Array); // true
console.log((function() {}).constructor === Function); // true
console.log(({}).constructor === Object); // true

constructor有两个作用,一是判断数据的类型,二是对象实例通过 constrcutor 对象访问它的构造函数。需要注意,如果创建一个对象来改变它的原型,constructor就不能用来判断数据类型了:如下

function Fn(){};
 
Fn.prototype = new Array();
 
var f = new Fn();
 
console.log(f.constructor===Fn);    // false
console.log(f.constructor===Array); // true
(4)Object.prototype.toString.call()

Object.prototype.toString.call() 使用 Object 对象的原型方法 toString 来判断数据类型:

var a = Object.prototype.toString;
 
console.log(a.call(2));
console.log(a.call(true));
console.log(a.call('str'));
console.log(a.call([]));
console.log(a.call(function(){}));
console.log(a.call({}));
console.log(a.call(undefined));
console.log(a.call(null));

同样是检测对象obj调用toString方法,obj.toString()的结果和Object.prototype.toString.call(obj)的结果不一样,这是为什么?

这是因为toStringObject的原型方法,而Array、function等类型作为Object的实例,都重写了toString方法。不同的对象类型调用toString方法时,根据原型链的知识,调用的是对应的重写之后的toString方法(function类型返回内容为函数体的字符串,Array类型返回元素组成的字符串…),而不会去调用Object上原型toString方法(返回对象的具体类型),所以采用obj.toString()不能得到其对象类型,只能将obj转换为字符串类型;因此,在想要得到对象的具体类型时,应该调用Object原型上的toString方法 描述文字优化

(5)展开运算符 和 Object.assign()

在JavaScript中,扩展运算符(Spread Operator,…)和Object.assign()方法都可以用于合并对象,但它们之间存在一些差异和各自的独特用途。

特性/方法 扩展运算符 (…) Object.assign()
主要用途 数组和对象的展开、函数参数展开、解构赋值 合并对象属性到目标对象
语法 -对象:{…obj1, key: value, …obj2}。 -数组:[…array1, …array2, item] Object.assign(target, …sources)
目标对象控制 无显式目标对象控制,直接在字面量中使用或函数参数展开 显式指定目标对象,可以是已存在的对象
源对象数量 无限制,直接在使用场景中体现 无限制,作为参数传入
拷贝类型 浅拷贝,包括嵌套对象 浅拷贝,包括嵌套对象
处理Symbol键 对象展开时不处理 处理Symbol键属性
返回值 无直接返回值(在数组和解构中是操作效果,在对象展开中直接影响字面量) 返回合并后的目标对象
对null/undefined目标对象处理 仅在对象展开时有效,不涉及此问题 自动替换null或undefined为目标对象{}
适用场景示例 简洁合并对象或数组、函数调用参数扩展、解构赋值 动态合并多个对象到已有对象,初始化配置
通过上述表格,您可以清晰地对比扩展运算符和Object.assign()在语法、功能、控制选项、处理细节等方面的异同,以及它们各自适用的典型场景。 - -
  • 展开运算符 (…)
    语法:
    在对象中:{...obj1, key: value, ...obj2}
    在数组中:[...array1, ...array2, item]

    特点:
    直接在语法层面展开对象或数组,更加简洁。
    可以用于数组和对象的解构赋值,以及函数调用时的参数列表扩展。
    对于对象,它不会复制继承的属性,只拷贝自身可枚举属性。
    对象展开是浅拷贝,嵌套对象仍共享同一引用。

let outObj = {
  inObj: {a: 1, b: 2}
}
let newObj = {...outObj}
newObj.inObj.a = 2
console.log(outObj) // {inObj: {a: 2, b: 2}}
  • Object.assign()

    语法:Object.assign(target, ...sources),将一个或多个源对象的可枚举属性复制到目标对象。

    特点:
    更加灵活,可以指定目标对象,可以合并多个源对象。
    返回目标对象,可以链式调用。
    浅拷贝,对于嵌套对象也是共享引用。
    除了拷贝可枚举属性外,还会处理Symbol作为键的属性。
    如果目标对象是nullundefined,会自动将其替换为{},这可能导致意外行为。

let outObj = {
  inObj: {a: 1, b: 2}
}
let newObj = Object.assign({}, outObj)
newObj.inObj.a = 2
console.log(outObj) // {inObj: {a: 2, b: 2}}

总结
相似点:两者都实现了对象的浅拷贝,适合于合并多个对象的属性。
不同点:扩展运算符提供了更简洁的语法,直接在对象或数组字面量中使用,同时支持数组的展开。
Object.assign()提供了更多的控制,允许明确指定目标对象,并且可以合并更多源对象,但需注意其对null或undefined目标对象的特殊处理。
根据具体需求选择合适的方法:对于简单合并且追求代码简洁性时,扩展运算符更优;需要更多控
制或合并多个源对象时,Object.assign()可能更适合。

3. 展开运算符的作用及使用场景

(1)对象扩展运算符
对象的扩展运算符(...)用于取出参数对象中的所有可遍历属性,拷贝到当前对象之中。

let bar = { a: 1, b: 2 };
let baz = { ...bar }; // { a: 1, b: 2 }

上述方法实际上等价于:

let bar = { a: 1, b: 2 };
let baz = Object.assign({}, bar); // { a: 1, b: 2 }

Object.assign方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。Object.assign方法的第一个参数是目标对象,后面的参数都是源对象。(如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性)。

同样,如果用户自定义的属性,放在扩展运算符后面,则扩展运算符内部的同名属性会被覆盖掉。

let bar = {a: 1, b: 2};
let baz = {...bar, ...{a:2, b: 4}};  // {a: 2, b: 4}

利用上述特性就可以很方便的修改对象的部分属性。在redux中的reducer函数规定必须是一个纯函数,reducer中的state对象要求不能直接修改,可以通过扩展运算符把修改路径的对象都复制一遍,然后产生一个新的对象返回。

需要注意:扩展运算符对对象实例的拷贝属于浅拷贝。

(2)数组扩展运算符
数组的扩展运算符可以将一个数组转为用逗号分隔的参数序列,且每次只能展开一层数组。

console.log(...[1, 2, 3])
// 1 2 3
console.log(...[1, [2, 3, 4], 5])
// 1 [2, 3, 4] 5

下面是数组的扩展运算符的应用:

  • 将数组转换为参数序列
function fetchFuntion(x, y) {
  return x + y;
}
const numbers = [1, 2];
fetchFuntion(...numbers) // 3
  • 复制数组
const arr1 = [1, 2];
const arr2 = [...arr1];
  • 合并数组
    在数组内合并数组
const arr1 = ['two', 'three'];
const arr2 = ['one', ...arr1, 'four', 'five'];
// ["one", "two", "three", "four", "five"]
  • 扩展运算符与解构赋值结合起来,用于生成数组
const [first, ...rest] = [1, 2, 3, 4, 5];
first // 1
rest  // [2, 3, 4, 5]

注意:如果将扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报错。

const [...rest, last] = [1, 2, 3, 4, 5];         // 报错
const [first, ...rest, last] = [1, 2, 3, 4, 5];  // 报错
  • 将字符串转为真正的数组
[...'hello']    // [ "h", "e", "l", "l", "o" ]
  • 任何 Iterator 接口的对象,都可以用扩展运算符转为真正的数组
    比较常见的应用是可以将某些数据结构转为数组:
// arguments对象
function foo() {
  const args = [...arguments];
}

用于替换es5中的Array.prototype.slice.call(arguments)写法。

  • 使用Math函数获取数组中特定的值
const numbers = [9, 4, 7, 1];
Math.min(...numbers); // 1
Math.max(...numbers); // 9

4.intanceof 操作符的实现原理及实现

instanceof 运算符用于判断构造函数的 prototype 属性是否出现在对象的原型链中的任何位置。

function instanceofFunction(left, right) {
  // 获取对象的原型
  let proto = Object.getPrototypeOf(left)
  // 获取构造函数的 prototype 对象
  let prototype = right.prototype; 

  // 判断构造函数的 prototype 对象是否在对象的原型链上
  while (true) {
    if (!proto) return false;
    if (proto === prototype) return true;
    // 如果没有找到,就继续从其原型上找,Object.getPrototypeOf方法用来获取指定对象的原型
    proto = Object.getPrototypeOf(proto);
  }
}

5.为什么0.1+0.2 ! == 0.3,如何让其相等

let n1 = 0.1, n2 = 0.2
console.log(n1 + n2)  // 0.30000000000000004

要想等于0.3,使用toFixed()进行转化

(n1 + n2).toFixed(2) // 注意,toFixed为四舍五入

6. 如何获取安全的 undefined 值?

因为 undefined 是一个标识符,所以可以被当作变量来使用和赋值,但是这样会影响 undefined 的正常判断。表达式 void ___ 没有返回值,因此返回结果是 undefinedvoid 并不改变表达式的结果,只是让表达式不返回值。因此可以用 void 0 来获得 undefined

let explicitlyUndefined  = void 0;

console.log(explicitlyUndefined ); // 输出为 undefined

// 使用 void 运算符确保返回 undefined
let conditionCheckResult  = void someFunction();

console.log(conditionCheckResult ); // 输出为 undefined

7.typeof NaN 的结果是什么?

typeof NaN 的结果是 "number"NaN(Not a Number)是一个特殊的数值,表示一个无效的数值结果。虽然 NaN 属于 "Not a Number" 类型,但在 JavaScript 中,typeof NaN 的结果是 "number"

这是因为在 JavaScript 中,NaN 被归类为数值类型,但它是一个特殊的、非数字的数值。NaN 的类型被视为 "number",是为了保持与 IEEE 754 浮点数规范的一致性,该规范定义了 JavaScript 中的数值类型。

验证 typeof NaN 的结果为 "number" 的示例:


console.log(typeof NaN); // 输出为 "number"

需要注意的是,NaN 不等于任何其他值,包括它自己。因此,使用 isNaN() 函数来检查一个值是否为 NaN 是更常见和可靠的方法。如下示例:

console.log(isNaN(NaN)); // 输出为 true
console.log(isNaN(42)); // 输出为 false
console.log(isNaN("Hello")); // 输出为 true,因为 "Hello" 无法转换为数值

尽管 NaN 表示一个非数字的值,但在 JavaScript 中,typeof NaN 的结果为 “number”。

8.isNaN 和 Number.isNaN 函数的区别?

  • 函数 isNaN 接收参数后,会尝试将这个参数转换为数值,任何不能被转换为数值的的值都会返回 true,因此非数字值传入也会返回 true ,会影响 NaN 的判断。
  • 函数 Number.isNaN 会首先判断传入参数是否为数字,如果是数字再继续判断是否为 NaN ,不会进行数据类型的转换,这种方法对于 NaN 的判断更为准确。

9.Proxy 可以实现什么功能?

在Vue 3中使用Proxy替代Object.defineProperty来实现更高效的数据响应式
Proxy 是 ES6 中新增的功能,它可以用来自定义对象中的操作。

let some = new Proxy(target, handler)

target 代表需要添加代理的对象,handler 用来自定义对象中的操作,比如可以用来自定义 set 或者 get 函数。

通过 Proxy 来实现一个数据响应式:

let onWatch = (obj, setBind, getLogger) => {
  let handler = {
    get(target, property, receiver) {
      getLogger(target, property)
      return Reflect.get(target, property, receiver)
    },
    set(target, property, value, receiver) {
      setBind(value, property)
      return Reflect.set(target, property, value)
    }
  }
  return new Proxy(obj, handler)
}
let obj = { a: 1 }
let p = onWatch(
  obj,
  (v, property) => {
    console.log(`监听到属性${property}改变为${v}`)
  },
  (target, property) => {
    console.log(`'${property}' = ${target[property]}`)
  }
)
p.a = 2 // 监听到属性a改变
p.a // 'a' = 2

在上述代码中,通过自定义 setget 函数的方式,在原本的逻辑中插入了我们的函数逻辑,实现了在对对象任何属性进行读写时发出通知。

如果需要实现一个 Vue 中的响应式,需要在 get 中收集依赖,在 set 派发更新,之所以 Vue3.0 要使用 Proxy 替换原本的 API 原因在于 Proxy 无需一层层递归为每个属性添加代理,一次即可完成以上操作,性能上更好,并且原本的实现有一些数据更新不能监听到,但是 Proxy 可以完美监听到任何方式的数据改变,唯一缺陷就是浏览器的兼容性不好。

10. 对象与数组的解构的理解

解构是 ES6 提供的一种新的提取数据的模式,这种模式能够从对象或数组里有针对性地拿到想要的数值。
(1)数组的解构

const [a, b, c] = [1, 2, 3]

最终,a、b、c分别被赋予了数组第0、1、2个索引位的值。

数组里的0、1、2索引位的元素值,精准地被映射到了左侧的第0、1、2个变量里去,这就是数组解构的工作模式。还可以通过给左侧变量数组设置空占位的方式,实现对数组中某几个元素的精准提取:

const [a,,c] = [1,2,3]

通过把中间位留空,可以顺利地把数组第一位和最后一位的值赋给 a、c 两个变量。

(2)对象的解构
对象解构比数组结构稍微复杂一些,也更显强大。在解构对象时,是以属性的名称为匹配条件,来提取想要的数据的。现在定义一个对象:

const stu = {
  name: 'Bob',
  age: 24
}

解构它的两个自有属性:

const { name, age } = stu

注意,对象解构严格以属性名作为定位依据,所以就算调换了 name 和 age 的位置,结果也是一样的:

const { age, name } = stu

扩展运算符被用在函数形参上时,它还可以把一个分离的参数序列整合成一个数组:

function mutiple(...args) {
  let result = 1;
  for (var val of args) {
    result *= val;
  }
  return result;
}
mutiple(1, 2, 3, 4) // 24

这里,传入 mutiple 的是四个分离的参数,但是如果在 mutiple 函数里尝试输出 args 的值,会发现它是一个数组:

function mutiple(...args) {
  console.log(args)
}
mutiple(1, 2, 3, 4) // [1, 2, 3, 4]

这就是 ...rest运算符的又一层威力了,它可以把函数的多个入参收敛进一个数组里。这一点经常用于获取函数的多余参数,或者像上面这样处理函数参数个数不确定的情况

11.

12.对this对象的理解

this 是执行上下文中的一个属性,它指向最后一次调用这个方法的对象。在实际开发中,this 的指向可以通过四种调用模式来判断。

  • 第一种是函数调用模式,当一个函数不是一个对象的属性时,直接作为函数来调用时,this 指向全局对象。
  • 第二种是方法调用模式,如果一个函数作为一个对象的方法来调用时,this 指向这个对象。
  • 第三种是构造器调用模式,如果一个函数用 new 调用时,函数执行前会新创建一个对象,this 指向这个新创建的对象。
  • 第四种是 apply 、 call 和 bind 调用模式,这三个方法都可以显示的指定调用函数的 this 指向。其中 apply 方法接收两个参数:一个是 this 绑定的对象,一个是参数数组。call 方法接收的参数,第一个是 this 绑定的对象,后面的其余参数是传入函数执行的参数。也就是说,在使用 call() 方法时,传递给函数的参数必须逐个列举出来。bind 方法通过传入一个对象,返回一个 this 绑定了传入对象的新函数。这个函数的 this 指向除了使用 new 时会被改变,其他情况下都不会改变。

这四种方式,使用构造器调用模式的优先级最高,然后是 apply、call 和 bind 调用模式,然后是方法调用模式,然后是函数调用模式。

13.箭头函数有哪些特点 ?

(1)更简洁的语法

  • 只有一个形参就不需要用括号括起来
  • 如果函数体只有一行,就不需要放到一个块中
  • 如果 return 语句是函数体内唯一的语句,就不需要 return 关键字

(2)箭头函数没有自己的 this,arguments,super
(3)箭头函数 this 只会从自己的作用域链的上一层继承 this

14.call() 和 apply() 的区别?

它俩作用一模一样,区别仅在于传入参数的形式的不同

  • call 传入的参数数量不固定,跟 apply 相同的是,第一个参数也是代表函数体内的 this 指向,从第二个参数开始往后,每个参数被依次传入函数。
  • apply 接受两个参数,第一个参数指定了函数体内 this 对象的指向,第二个参数为一个带下标的集合,这个集合可以为数组,也可以为类数组,apply 方法把这个集合中的元素作为参数传递给被调用的函数。

15.实现call、apply 及 bind 函数

(1)call 函数的实现步骤:

  • 判断调用对象是否为函数,即使是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。
  • 判断传入上下文对象是否存在,如果不存在,则设置为 window
  • 处理传入的参数,截取第一个参数后的所有参数。
  • 将函数作为上下文对象的一个属性。
  • 使用上下文对象来调用这个方法,并保存返回结果。
  • 删除刚才新增的属性。
  • 返回结果。
Function.prototype.customCall= function(context) {
  // 判断调用对象
  if (typeof this !== "function") {
    console.error("type error");
  }
  // 获取参数
  let args = [...arguments].slice(1),
    result = null;
  // 判断 context 是否传入,如果未传入则设置为 window
  context = context || window;
  // 将调用函数设为对象的方法
  context.fn = this;
  // 调用函数
  result = context.fn(...args);
  // 将属性删除
  delete context.fn;
  return result;
};

(2)apply 函数的实现步骤:

  • 判断调用对象是否为函数,即使是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。
  • 判断传入上下文对象是否存在,如果不存在,则设置为 window
  • 将函数作为上下文对象的一个属性。
  • 判断参数值是否传入
  • 使用上下文对象来调用这个方法,并保存返回结果。
  • 删除刚才新增的属性
  • 返回结果
Function.prototype.customApply = function(context) {
  // 判断调用对象是否为函数
  if (typeof this !== "function") {
    throw new TypeError("Error");
  }
  let result = null;
  // 判断 context 是否存在,如果未传入则为 window
  context = context || window;
  // 将函数设为对象的方法
  context.fn = this;
  // 调用方法
  if (arguments[1]) {
    result = context.fn(...arguments[1]);
  } else {
    result = context.fn();
  }
  // 将属性删除
  delete context.fn;
  return result;
};

(3)bind 函数的实现步骤:

  • 判断调用对象是否为函数,即使是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。
  • 保存当前函数的引用,获取其余传入参数值。
  • 函数内部使用 apply 来绑定函数调用,需要判断函数作为构造函数的情况,这个时候需要传入当前函数的 thisapply 调用,其余情况都传入指定的上下文对象。
Function.prototype.customBind = function(context) {
  // 判断调用对象是否为函数
  if (typeof this !== "function") {
    throw new TypeError("Error");
  }
  // 获取参数
  var args = [...arguments].slice(1),
    fn = this;
  return function Fn() {
    // 根据调用方式,传入不同绑定值
    return fn.apply(
      this instanceof Fn ? this : context,
      args.concat(...arguments)
    );
  };
};

16.对Promise的理解

Promise概念 描述
定义 Promise是JavaScript中的一个对象,用于封装异步操作的结果,提供统一的API处理异步操作的不同状态(完成或失败)。
解决的问题 解决了回调地狱问题,提高异步代码的可读性和可维护性。
Promise状态 说明
Pending 初始状态,异步操作正在进行,未完成也未失败。
Resolved 成功状态,异步操作顺利完成,可以访问到结果。
Rejected 失败状态,异步操作失败,可以捕获到原因。
Promise方法 功能
then 注册成功状态的回调,链式调用,返回新的Promise。
catch 注册失败状态的回调,处理Promise链中的错误。
finally 不论成功或失败都会执行的回调,用于资源清理。
resolve 创建已解决的Promise,常用于自定义Promise内部。
reject 创建已拒绝的Promise,常用于自定义Promise内部。
Promise特点 描述
不可变性 Promise状态一旦改变(Resolved或Rejected)就不能再变。
链式调用 可以通过.then连续调用来组织多个异步操作。
错误捕获 错误可以通过.catch集中处理,保证流程可控。
Promise缺点 说明
无法取消 一旦创建并启动,无法取消操作。
错误传播 若未被捕获,内部错误可能被忽视,导致难以调试。
状态不确定性 Pending时,无法准确知道操作的进度。

Promise是异步编程的一种解决方案,它是一个对象,可以获取异步操作的消息,他的出现大大改善了异步编程的困境,避免了地狱回调,它比传统的解决方案回调函数和事件更合理和更强大。

所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。
(1)Promise的实例有三个状态:

  • Pending(进行中)
  • Resolved(已完成)
  • Rejected(已拒绝)

当把一件事情交给promise时,它的状态就是Pending,任务完成了状态就变成了Resolved、没有完成失败了就变成了Rejected

(2)Promise的实例有两个过程:

  • pending -> fulfilled : Resolved(已完成)
  • pending -> rejected:Rejected(已拒绝)

注意:一旦从进行状态变成为其他状态就永远不能更改状态了。

Promise的特点:

  • 对象的状态不受外界影响。promise对象代表一个异步操作,有三种状态,pending(进行中)、fulfilled(已成功)、rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态,这也是promise这个名字的由来——“承诺”;
  • 一旦状态改变就不会再变,任何时候都可以得到这个结果。promise对象的状态改变,只有两种可能:从pending变为fulfilled,从pending变为rejected。这时就称为resolved(已定型)。如果改变已经发生了,你再对promise对象添加回调函数,也会立即得到这个结果。这与事件(event)完全不同,事件的特点是:如果你错过了它,再去监听是得不到结果的。

Promise的缺点:

  • 无法取消Promise,一旦新建它就会立即执行,无法中途取消。
  • 如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。
  • 当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

总结Promise 对象是异步编程的一种解决方案,最早由社区提出。Promise 是一个构造函数,接收一个函数作为参数,返回一个 Promise 实例。一个 Promise 实例有三种状态,分别是pending、resolved 和 rejected,分别代表了进行中、已成功和已失败。实例的状态只能由 pending 转变 resolved 或者rejected 状态,并且状态一经改变,就凝固了,无法再被改变了。
状态的改变是通过 resolve() 和 reject() 函数来实现的,可以在异步操作结束后调用这两个函数改变 Promise 实例的状态,它的原型上定义了一个 then 方法,使用这个 then 方法可以为两个状态的改变注册回调函数。这个回调函数属于微任务,会在本轮事件循环的末尾执行。

注意: 在构造 Promise 的时候,构造函数内部的代码是立即执行的

17.Promise的基本用法

(1)创建Promise对象
Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。

Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject。
const promise = new Promise(function(resolve, reject) {
  // ... some code
  if (/* 异步操作成功 */){
    resolve(value);
  } else {
    reject(error);
  }
});

一般情况下都会使用new Promise()来创建promise对象,但是也可以使用promise.resolvepromise.reject这两个方法:

  • Promise.resolve
    Promise.resolve(value)的返回值也是一个promise对象,可以对返回值进行.then调用,代码如下:
Promise.resolve(123).then(function(value){
  console.log(value); // 打印出123
});

resolve(123)代码中,会让promise对象进入确定(resolve状态),并将参数123传递给后面的then所指定的onFulfilled 函数;
创建promise对象可以使用new Promise的形式创建对象,也可以使用Promise.resolve(value)的形式创建promise对象;

  • Promise.reject
    Promise.reject 也是new Promise的快捷形式,也创建一个promise对象。代码如下:
Promise.reject(new Error("php是世界上最好的语言!"));

就是下面的代码new Promise的简单形式:

new Promise(function(resolve,reject){
   reject(new Error("php!"));
});

下面是使用resolve方法和reject方法:

function fetchPromise(ready) {
  return new Promise(function(resolve,reject){
    if(ready) {
      resolve("hello world");
    }else {
      reject("No thanks");
    }
  });
};
// 方法调用
fetchPromise(true).then(function(msg){
  console.log(msg);
},function(error){
  console.log(error);
});

上面的代码的含义是给fetchPromise方法传递一个参数,返回一个promise对象,如果为true的话,那么调用promise对象中的resolve()方法,并且把其中的参数传递给后面的then第一个函数内,因此打印出 'hello world', 如果为false的话,会调用promise对象中的reject()方法,则会进入then的第二个函数内,会打印No thanks

(2)Promise方法
Promise有五个常用的方法:then()、catch()、all()、race()、finally

  • then()
    Promise执行的内容符合成功条件时,调用resolve函数,失败就调用reject函数。Promise创建完了,那该如何调用呢?
promise.then(function(value) {
  // success
}, function(error) {
  // failure
});

then方法可以接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为resolved时调用,第二个回调函数是Promise对象的状态变为rejected时调用。其中第二个参数可以省略。 then方法返回的是一个新的Promise实例(不是原来那个Promise实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法。

当要写有顺序的异步事件时,需要串行时,可以这样写:

let promise = new Promise((resolve,reject)=>{
    ajax('first').success(function(res){
        resolve(res);
    })
})
promise.then(res=>{
    return new Promise((resovle,reject)=>{
        ajax('second').success(function(res){
            resolve(res)
        })
    })
}).then(res=>{
    return new Promise((resovle,reject)=>{
        ajax('third').success(function(res){
            resolve(res)
        })
    })
}).then(res=>{
    
})

那当要写的事件没有顺序或者关系时,可以使用all 方法来解决。

  • catch()
    Promise对象除了有then方法,还有一个catch方法,该方法相当于then方法的第二个参数,指向reject的回调函数。不过catch方法还有一个作用,就是在执行resolve回调函数时,如果出现错误,抛出异常,不会停止运行,而是进入catch方法中。
promise.then((data) => {
     console.log('resolved',data);
},(err) => {
     console.log('rejected',err);
     }
); 
promise.then((data) => {
    console.log('resolved',data);
}).catch((err) => {
    console.log('rejected',err);
});
  • all()
    all方法可以完成并行任务, 它接收一个数组,数组的每一项都是一个promise对象。当数组中所有的promise的状态都达到resolved的时候,all方法的状态就会变成resolved,如果有一个状态变成了rejected,那么all方法的状态就会变成rejected
let promise1 = new Promise((resolve,reject)=>{
	setTimeout(()=>{
       resolve(1);
	},2000)
});
let promise2 = new Promise((resolve,reject)=>{
	setTimeout(()=>{
       resolve(2);
	},1000)
});
let promise3 = new Promise((resolve,reject)=>{
	setTimeout(()=>{
       resolve(3);
	},3000)
});
Promise.all([promise1,promise2,promise3]).then(res=>{
    console.log(res);
    //结果为:[1,2,3] 
})

调用all方法时的结果成功的时候是回调函数的参数也是一个数组,这个数组按顺序保存着每一个promise对象resolve执行时的值。

  • race()
    race方法和all一样,接受的参数是一个每项都是promise的数组,但是与all不同的是,当最先执行完的事件执行完之后,就直接返回该promise对象的值。如果第一个promise对象状态变成resolved,那自身的状态变成了resolved;反之第一个promise变成rejected,那自身状态就会变成rejected
let promise1 = new Promise((resolve,reject)=>{
	setTimeout(()=>{
       reject(1);
	},2000)
});
let promise2 = new Promise((resolve,reject)=>{
	setTimeout(()=>{
       resolve(2);
	},1000)
});
let promise3 = new Promise((resolve,reject)=>{
	setTimeout(()=>{
       resolve(3);
	},3000)
});
Promise.race([promise1,promise2,promise3]).then(res=>{
	console.log(res);
	//结果:2
},rej=>{
    console.log(rej)};
)

那么race方法的实际作用, 就是当要做一件事,超过多长时间就不做了,可以用这个方法来解决:

Promise.race([promise1,timeOutPromise(5000)]).then(res=>{})
  • finally()
    finally方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是ES9( ES2018) 引入标准的。
promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});

上面代码中,不管promise最后的状态,在执行完thencatch指定的回调函数以后,都会执行finally方法指定的回调函数。

下面是一个例子,服务器使用 Promise 处理请求,然后使用finally方法关掉服务器。

server.listen(port)
  .then(function () {
    // ...
  })
  .finally(server.stop);

finally方法的回调函数不接受任何参数,这意味着没有办法知道,前面的 Promise 状态到底是fulfilled还是rejected。这表明,finally方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果。finally本质上是then方法的特例:

promise.finally(() => {
  // 语句
});
// 等同于
promise
.then(
  result => {
    // 语句
    return result;
  },
  error => {
    // 语句
    throw error;
  }
);

上面代码中,如果不使用finally方法,同样的语句需要为成功和失败两种情况各写一次。有了finally方法,则只需要写一次。

18.Promise解决了什么问题

在工作中经常会碰到这样一个需求,比如我使用ajax发一个A请求后,成功后拿到数据,需要把数据传给B请求;那么需要如下编写代码:


let fs = require('fs')
fs.readFile('./a.txt','utf8',function(err,data){
  fs.readFile(data,'utf8',function(err,data){
  
    fs.readFile(data,'utf8',function(err,data){
      console.log(data)
    })
  })
})

上面的代码有如下缺点:

  • 后一个请求需要依赖于前一个请求成功后,将数据往下传递,会导致多个ajax请求嵌套的情况,代码不够直观。
  • 如果前后两个请求不需要传递参数的情况下,那么后一个请求也需要前一个请求成功后再执行下一步操作,这种情况下,那么也需要如上编写代码,导致代码不够直观。

Promise出现之后,代码变成这样:

let fs = require('fs')
function read(url){
  return new Promise((resolve,reject)=>{
    fs.readFile(url,'utf8',function(error,data){
      error && reject(error)
      resolve(data)
    })
  })
}
read('./a.txt').then(data=>{
  return read(data) 
}).then(data=>{
  return read(data)  
}).then(data=>{
  console.log(data)
})

这样代码看起了就简洁了很多,解决了地狱回调的问题。

19.Promise.all和Promise.race的区别的使用场景

(1)Promise.all
Promise.all可以将多个Promise实例包装成一个新的Promise实例。同时,成功和失败的返回值是不同的,成功的时候返回的是一个结果数组,而失败的时候则返回最先被reject失败状态的值。
Promise.all中传入的是数组,返回的也是是数组,并且会将进行映射,传入的promise对象返回的值是按照顺序在数组中排列的,但是注意的是他们执行的顺序并不是按照顺序的,除非可迭代对象为空。
需要注意,Promise.all获得的成功结果的数组里面的数据顺序和Promise.all接收到的数组顺序是一致的,这样当遇到发送多个请求并根据请求顺序获取和使用数据的场景,就可以使用Promise.all来解决。

(2)Promise.race
Promse.race就是赛跑的意思,意思就是说,Promise.race([p1, p2, p3])里面哪个结果获得的快,就返回那个结果,不管结果本身是成功状态还是失败状态。当要做一件事,超过多长时间就不做了,可以用这个方法来解决:

Promise.race([promise1,timeOutPromise(5000)]).then(res=>{})

20.对async/await 的理解

async/await其实是Generator 的语法糖,它能实现的效果都能用then链来实现,它是为优化then链而开发出来的。从字面上来看,async'异步'的简写,await则为等待,所以很好理解async 用于申明一个 function 是异步的,而 await 用于等待一个异步方法执行完成。当然语法上强制规定await只能出现在asnyc函数中。async函数返回值 如下:

async function fetchAsync(){
   return 'hello world';
}
let result = fetchAsync(); 
console.log(result)

async 函数返回的是一个 Promise 对象。async 函数(包含函数语句、函数表达式、Lambda表达式)会返回一个 Promise 对象,如果在函数中 return 一个直接量,async 会把这个直接量通过 Promise.resolve() 封装成 Promise 对象。

async 函数返回的是一个 Promise 对象,所以在最外层不能用 await 获取其返回值的情况下,当然应该用原来的方式:then() 链来处理这个 Promise 对象,如下:

async function fetchAsync(){
   return 'hello world'
}
let result = fetchAsync() 
console.log(result)
result.then(v=>{
    console.log(v)   // hello world
})

如果 async 函数没有返回值,它会返回 Promise.resolve(undefined)

Promise 的特点——无等待,所以在没有 await 的情况下执行 async 函数,它会立即执行,返回一个 Promise 对象,并且,绝不会阻塞后面的语句。这和普通返回 Promise 对象的函数并无二致。

注意:Promise.resolve(x) 可以看作是 new Promise(resolve => resolve(x)) 的简写,可以用于快速封装字面量对象或其他对象,将其封装成 Promise 实例。

21.await 到底在等什么?

一般来说,都认为 await 是在等待一个 async 函数完成。不过按语法说明,await 等待的是一个表达式,这个表达式的计算结果是 Promise 对象或者其它值(换句话说,就是没有特殊限定)。

因为 async 函数返回一个 Promise 对象,所以 await 可以用于等待一个 async 函数的返回值——这也可以说是 await 在等 async 函数,但要清楚,它等的实际是一个返回值。注意到 await 不仅仅用于等 Promise 对象,它可以等任意表达式的结果,所以,await 后面实际是可以接普通函数调用或者直接量的。所以下面这个示例完全可以正确运行:

function getSomething() {
    return "something";
}
async function fetchAsync() {
    return Promise.resolve("hello async");
}
async function out() {
    const v1 = await getSomething();
    const v2 = await fetchAsync();
    console.log(v1, v2);
}
out();

await 表达式的运算结果取决于它等的是什么。

  • 如果它等到的不是一个 Promise 对象,那 await 表达式的运算结果就是它等到的东西。
  • 如果它等到的是一个 Promise 对象,await 就忙起来了,它会阻塞后面的代码,等着 Promise 对象 resolve,然后得到 resolve 的值,作为 await 表达式的运算结果。
    示例如下:
function fetchAsync(x) {
    return new Promise((resolve) => { // 修正了这里的大括号
        setTimeout(() => {
            resolve(x);
        }, 3000);
    }); // 修正了这里的大括号
}

async function fetchAwait() {
    let result = await fetchAsync('hello world'); // 使用await等待fetchAsync的结果
    console.log(result); // 3秒钟之后出现"hello world"
    console.log('bug'); // 紧接着输出"bug"
}

fetchAwait(); // 确保调用了fetchAwait函数
console.log('bug'); // 立即输出"bug"

这就是 await 必须用在 async 函数中的原因。async 函数调用不会造成阻塞,它内部所有的阻塞都被封装在一个 Promise 对象中异步执行。await暂停当前async的执行,所以'bug'最先输出,'hello world'和'bug'是3秒钟后同时出现的。

22.async/await的优势

单一的 Promise 链并不能发现 async/await 的优势,但是,如果需要处理由多个 Promise 组成的 then 链的时候,优势就能体现出来了,Promise 通过 then 链来解决多层回调的问题,现在又用 async/await 来进一步优化它

假设有一个业务,分多个步骤完成,每个步骤都是异步的,而且依赖于上一个步骤的结果。仍然用 setTimeout 来模拟异步操作:

/**
 * 传入参数 n,表示这个函数执行的时间(毫秒)
 * 执行的结果是 n + 200,这个值将用于下一步骤
 */
function takeLongTime(n) {
    return new Promise(resolve => {
        setTimeout(() => resolve(n + 200), n);
    });
}
function step1(n) {
    console.log(`step1 with ${n}`);
    return takeLongTime(n);
}
function step2(n) {
    console.log(`step2 with ${n}`);
    return takeLongTime(n);
}
function step3(n) {
    console.log(`step3 with ${n}`);
    return takeLongTime(n);
}

现在用 Promise 方式来实现这三个步骤的处理:

function initializeTasks() {
    console.time("initializeTasks");
    const time1 = 300;
    step1(time1)
        .then(time2 => step2(time2))
        .then(time3 => step3(time3))
        .then(result => {
            console.log(`result is ${result}`);
            console.timeEnd("initializeTasks");
        });
}

initializeTasks();
// c:\var\test>node .  // Node.js 8.x以上 默认支持async/await 无需--harmony_async_await指令已过时
// step1 with 300
// step2 with 500
// step3 with 700
// result is 900
// initializeTasks: 1507.251ms

输出结果 result 是 step3() 的参数 700 + 200 = 900。doIt() 顺序执行了三个步骤,一共用了 300 + 500 + 700 = 1500 毫秒,和 console.time()/console.timeEnd() 计算的结果一致。

用async/await 实现:

async function initializeTasks() {
    console.time("initializeTasks");
    const time1 = 300;
    const time2 = await step1(time1);
    const time3 = await step2(time2);
    const result = await step3(time3);
    console.log(`result is ${result}`);
    console.timeEnd("initializeTasks");
}
initializeTasks();

结果和之前的 Promise 实现是一样的,但是这个代码看着清晰明了,几乎跟同步代码一样

23.async/await对比Promise的优势

async/await代码读起来更加同步,Promise虽然摆脱了回调地狱,但是then的链式调⽤也会带来额外的阅读负担

  • Promise传递中间值⾮常麻烦,⽽async/await⼏乎是同步的写法,⾮常优雅
  • 错误处理友好,async/await可以⽤成熟的try/catchPromise的错误捕获⾮常冗余
  • 调试友好,Promise的调试很差,由于没有代码块,你不能在⼀个返回表达式的箭头函数中设置断点,如果你在⼀个.then代码块中使⽤调试器的步进(step-over)功能,调试器并不会进⼊后续的.then代码块,因为调试器只能跟踪同步代码的每⼀步。

24.async/await 如何捕获异常

async function asyncFunction() {
    try {
        // 尝试执行的异步操作
        const result = await someAsyncOperation();
        console.log(result);
    } catch (error) {
        // 异步操作中抛出的错误会被这里的catch捕获
        console.error('捕获到错误:', error);
    }
}

// 调用异步函数
asyncFunction();

下一篇👉: 前端面试宝典总结3-JavaScript(2)