高碑店网站建设,仿糗事百科网站,珠海网站建设尚古道策略,东莞人才招聘信息前端面试宝典总结之JavaScript#xff08;1#xff09;
本文章 对各大学习技术论坛知识点#xff0c;进行总结、归纳自用学习#xff0c;共勉#x1f64f;
上一篇#x1f449;: 前端面试宝典总结2-CSS#xff08;2#xff09; 文章目录 前端面试宝典总结之JavaScrip…前端面试宝典总结之JavaScript1
本文章 对各大学习技术论坛知识点进行总结、归纳自用学习共勉
上一篇: 前端面试宝典总结2-CSS2 文章目录 前端面试宝典总结之JavaScript11. JavaScript有哪些数据类型它们的区别2.数据类型检测的方式有哪些1typeof2instanceof3 constructor4Object.prototype.toString.call()5展开运算符 和 Object.assign() 3. 展开运算符的作用及使用场景4.intanceof 操作符的实现原理及实现5.为什么0.10.2 ! 0.3如何让其相等6. 如何获取安全的 undefined 值7.typeof NaN 的结果是什么8.isNaN 和 Number.isNaN 函数的区别9.Proxy 可以实现什么功能10. 对象与数组的解构的理解11.12.对this对象的理解13.箭头函数有哪些特点 14.call() 和 apply() 的区别15.实现call、apply 及 bind 函数16.对Promise的理解17.Promise的基本用法18.Promise解决了什么问题19.Promise.all和Promise.race的区别的使用场景20.对async/await 的理解21.await 到底在等什么22.async/await的优势23.async/await对比Promise的优势24.async/await 如何捕获异常 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.数据类型检测的方式有哪些
1typeof
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其他判断都正确。
2instanceof
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); // trueinstanceof只能正确判断引用数据类型而不能判断基本数据类型。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); // trueconstructor有两个作用一是判断数据的类型二是对象实例通过 constrcutor 对象访问它的构造函数。需要注意如果创建一个对象来改变它的原型constructor就不能用来判断数据类型了如下
function Fn(){};Fn.prototype new Array();var f new Fn();console.log(f.constructorFn); // false
console.log(f.constructorArray); // true4Object.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)的结果不一样这是为什么
这是因为toString是Object的原型方法而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作为键的属性。 如果目标对象是null或undefined会自动将其替换为{}这可能导致意外行为。
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); // 94.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.10.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 ___ 没有返回值因此返回结果是 undefined。void 并不改变表达式的结果只是让表达式不返回值。因此可以用 void 0 来获得 undefined。
let explicitlyUndefined void 0;console.log(explicitlyUndefined ); // 输出为 undefined// 使用 void 运算符确保返回 undefined
let conditionCheckResult void someFunction();console.log(conditionCheckResult ); // 输出为 undefined7.typeof NaN 的结果是什么
typeof NaN 的结果是 number。NaNNot 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在上述代码中通过自定义 set 和 get 函数的方式在原本的逻辑中插入了我们的函数逻辑实现了在对对象任何属性进行读写时发出通知。 如果需要实现一个 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箭头函数没有自己的 thisargumentssuper 3箭头函数 this 只会从自己的作用域链的上一层继承 this。
14.call() 和 apply() 的区别
它俩作用一模一样区别仅在于传入参数的形式的不同
call 传入的参数数量不固定跟 apply 相同的是第一个参数也是代表函数体内的 this 指向从第二个参数开始往后每个参数被依次传入函数。apply 接受两个参数第一个参数指定了函数体内 this 对象的指向第二个参数为一个带下标的集合这个集合可以为数组也可以为类数组apply 方法把这个集合中的元素作为参数传递给被调用的函数。
15.实现call、apply 及 bind 函数
1call 函数的实现步骤
判断调用对象是否为函数即使是定义在函数的原型上的但是可能出现使用 call 等方式调用的情况。判断传入上下文对象是否存在如果不存在则设置为 window 。处理传入的参数截取第一个参数后的所有参数。将函数作为上下文对象的一个属性。使用上下文对象来调用这个方法并保存返回结果。删除刚才新增的属性。返回结果。
Function.prototype.customCall function(context) {// 判断调用对象if (typeof this ! function) {console.error(type error);}// 获取参数let args [...arguments].slice(1),result null;// 判断 context 是否传入如果未传入则设置为 windowcontext context || window;// 将调用函数设为对象的方法context.fn this;// 调用函数result context.fn(...args);// 将属性删除delete context.fn;return result;
};2apply 函数的实现步骤
判断调用对象是否为函数即使是定义在函数的原型上的但是可能出现使用 call 等方式调用的情况。判断传入上下文对象是否存在如果不存在则设置为 window 。将函数作为上下文对象的一个属性。判断参数值是否传入使用上下文对象来调用这个方法并保存返回结果。删除刚才新增的属性返回结果
Function.prototype.customApply function(context) {// 判断调用对象是否为函数if (typeof this ! function) {throw new TypeError(Error);}let result null;// 判断 context 是否存在如果未传入则为 windowcontext context || window;// 将函数设为对象的方法context.fn this;// 调用方法if (arguments[1]) {result context.fn(...arguments[1]);} else {result context.fn();}// 将属性删除delete context.fn;return result;
};3bind 函数的实现步骤
判断调用对象是否为函数即使是定义在函数的原型上的但是可能出现使用 call 等方式调用的情况。保存当前函数的引用获取其余传入参数值。函数内部使用 apply 来绑定函数调用需要判断函数作为构造函数的情况这个时候需要传入当前函数的 this 给 apply 调用其余情况都传入指定的上下文对象。
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各种异步操作都可以用同样的方法进行处理。 1Promise的实例有三个状态:
Pending进行中Resolved已完成Rejected已拒绝
当把一件事情交给promise时它的状态就是Pending任务完成了状态就变成了Resolved、没有完成失败了就变成了Rejected。
2Promise的实例有两个过程
pending - fulfilled : Resolved已完成pending - rejectedRejected已拒绝 注意一旦从进行状态变成为其他状态就永远不能更改状态了。
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 codeif (/* 异步操作成功 */){resolve(value);} else {reject(error);}
});一般情况下都会使用new Promise()来创建promise对象但是也可以使用promise.resolve和promise.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
2Promise方法 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最后的状态在执行完then或catch指定的回调函数以后都会执行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的区别的使用场景
1Promise.all Promise.all可以将多个Promise实例包装成一个新的Promise实例。同时成功和失败的返回值是不同的成功的时候返回的是一个结果数组而失败的时候则返回最先被reject失败状态的值。 Promise.all中传入的是数组返回的也是是数组并且会将进行映射传入的promise对象返回的值是按照顺序在数组中排列的但是注意的是他们执行的顺序并不是按照顺序的除非可迭代对象为空。 需要注意Promise.all获得的成功结果的数组里面的数据顺序和Promise.all接收到的数组顺序是一致的这样当遇到发送多个请求并根据请求顺序获取和使用数据的场景就可以使用Promise.all来解决。
2Promise.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 worldconsole.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\testnode . // 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-JavaScript2