【H2O2|全栈】JS进阶知识(十)ES6(6)
目录
前言
开篇语
准备工作
同步和异步
概念
优缺点
案例
同步情况
异步情况
回调地域
Promise
概念
状态
成功状态
实例函数
案例
封装AJAX
优化回调地域
拓展——await
总结
异常
try-catch-finally
error
throw
结束语
前言
开篇语
本系列博客主要分享JavaScript的进阶语法知识,本期为第十期,依然围绕ES6的语法进行展开。
本期内容为:同步和异步、回调地域、Promise和异常。
与基础部分的语法相比,ES6的语法进行了一些更加严谨的约束和优化,因此,在之后使用原生JS时,我们应该尽量使用ES6的语法进行代码编写。
准备工作
软件:【参考版本】Visual Studio Code
插件(扩展包):Open in browser, Live Preview, Live Server, Tencent Cloud AI Code Assistant, htmltagwrap
提示:在不熟练的阶段建议关闭AI助手
浏览器版本:Chrome
系统版本: Win10/11/其他非Windows版本
同步和异步
概念
同步操作是指,程序由上至下顺序执行,后面的数据需要上一次执行的结果。
异步操作是指,多个功能并发(同时)执行,我们此前学过的计时器、AJAX都是支持异步操作的。
优缺点
同步操作的优点是数据安全,按顺序接收数据和执行。缺点是顺序执行的效率比较低。
异步操作的优点是执行速度快。缺点是存在安全性问题,明明需要先执行的步骤,却因为异步条件不符合而被推迟到之后了。
案例
现在有这么一个案例,将大象放入冰箱,总共需要几步?
其实,也就下面三个步骤——
1. 打开冰箱门
2. 大象放进去
3. 关闭冰箱门
我们为这三个步骤封装三个方法——
function open() {
console.log('打开冰箱门');
}
function putIn() {
console.log('大象放进去');
}
function close() {
console.log('关闭冰箱门');
}
同步情况
首先,我们来看看在同步情况下三个方法的执行情况。
我们封装一个执行函数exec(),它的参数为回调函数,由回调函数来决定执行哪一个功能,就像下面这样——
function exec(callback) {
callback();
}
那么,我们想要顺序执行上面的三个事件,就可以这么做——
exec(open);
exec(putIn);
exec(close);
异步情况
现在,我们为上面的三个步骤添加延时时间,即添加一个计时器。
我们假设第一个步骤需要延时3000ms完成,第二个步骤需要延时2000ms,最后一步需要延时1000ms,那么,我们的执行函数就需要更改成下面的形式——
function exec(callback, time) {
setTimeOut = (callback, time);
}
注意,这里的callback实际上就是计时器中的() => callback(),由于箭头函数本质上就是用于执行回调函数,所以这里直接写回调函数名即可(不要写括号,箭头函数没有在参数中调用)。
这是,如果我们再次像之前那样直接写三个exec()函数,由于三者异步执行,于是出现了下面的结果——
exec(open, 3000);
exec(putIn, 2000);
exec(close, 1000);
可以看到,明明需要先执行的打开冰箱门的步骤,却因为在并发操作中延时最久,在最后一步才执行,导致出现了上面的荒唐的结果。
所以,对于这类情况,我们需要将原本的并发执行的异步情况转化为顺序执行的同步情况,确保所有的步骤都能安全地执行,这也是本节的重点。
回调地域
为了解决异步操作之间的顺序问题,我们可以在exec()上做出一些小小的处理。
我们先执行完第一步,再执行第二步,之后的操作相同。
在callback的位置,我们知道callback和() => callback()是等价的,所以我们将该形参展开为箭头函数,而第二形参time保持不变——
exec(() => {
open();
},3000);
在open()执行完毕后,我们再次执行exec(),这一次执行putIn(),就像下面这样——
exec(() => {
open();
exec(() => {
putIn();
},2000);
},3000);
由此一来,我们就成功实现了先执行完第一步,再执行第二步的操作,第三步也是一样的。
exec(() => {
open();
exec(() => {
putIn();
exec(() => {
close();
}, 1000);
}, 2000);
}, 3000);
这样一种将异步操作转为同步操作的方式,就是回调地域。
但是,不难发现,对于这种转化的方式,整个程序在排列上是按照纵向排列的。
当我们执行的步骤很多时,就需要不断地进行这种回调的过程,最后导致整个程序的代码量将会非常庞大,不方便我们进行代码的调试工作。
Promise
概念
Promise就是一个构造函数,构造函数中有一个参数,该参数是一个函数。这个函数有两个参数resolved,rejected,分别代表两个回调函数。
于是,Promise就可以写成下面这样——
Promise((resolved, rejected) => {
// 调用的回调函数
})
状态
一般来说,Promise有三种状态——准备(pending)、成功(fulfilled/resolved)和失败(rejected)。
首先,我们为Promise创建一个实例化对象,以便查看这些状态——
const p = new Promise(() => { });
如果不调用任何回调函数,表示准备状态,控制台打印结果如下——
如果调用resolved()函数,表示成功状态,控制台打印的结果如下——
const p = new Promise((resolved, rejected) => {
resolved();
});
如果调用rejected()函数,表示失败状态,控制台打印的结果如下——
const p = new Promise((resolved, rejected) => {
rejected();
});
成功状态
这也是一道经典的面试题,一般来说,成功的状态有下面这些——
1. 初始化 resolved函数
2. 没有报错 但是没有返回值
3. 没有报错 但是有返回值 返回值作为下一个then的接收参数
4. Promise有一个静态函数 resolve
其中resolved()是直接调用的,静态函数是由Promise.resolve()调用的。
实例函数
Promise有三种常用的实例函数——then(),catch(),finally()。
then()有两个参数,分别是两个回调函数,第一个函数在接受成功的情况下执行,第二个函数在接受失败的情况下执行。只写一个参数,代表成功的情况。
catch()仅有一个参数,仅用于接收失败的情况,一般来说失败的情况都由该方法接受。
finally()没有参数,无论成功还是失败都需要接受参数。
案例
封装AJAX
假设我们现在有三个文本文件,请用ajax来按顺序接受他们的参数。
由于ajax为异步操作,所以我们可以使用Promise将这三次ajax转为同步操作。
在ajax返回success()时,调用resolved();返回error()时,调用rejected()——
function getAjax(url) {
return new Promise((resolved, rejected) => {
$.ajax({
url,
success(res) {
// ajax成功时调用resolved
resolved(res);
},
error(res) {
// ajax失败时调用rejected
rejected(res);
}
})
})
}
为简单起见,这里暂时只考虑成功的情况,通过resolved()的参数,可以获取success()接收的数据。
而then()可以拿到resolved()的参数或者上一次return的结果,于是,我们可以在then()中接收上一步ajax()获取的数据,并返回下一次getAjax()的结果。
getAjax(url1)
.then(data => {
// 将数据从ajax中通过resolved的参数拿出来
console.log(data);
// 进行下一次的ajax操作,返回的参数交给下一次的then
return getAjax(url2);
}).then(data => {
console.log(data);
return getAjax(url3);
}).then(data => console.log(data));
优化回调地域
对于之前的大象放进冰箱的案例,我们也可以利用Promise来优化。
思路是,让exec()方法返回一个Promise()对象,然后再将这个对象交给后续的then()接收,由此往复。
exec(open, 3000).then(() => exec(in1, 2000)).then(() => exec(close, 1000))
拓展——await
其实,上述使用then的过程,还可以使用另一种等价的方式来实现——
await exec(open, 3000);
await exec(putIn, 2000);
await exec(close, 1000);
await可以让当前的方法执行完毕之后,再执行后续的方法。
但await必须在使用async修饰的函数中使用,它用于将同步函数转为异步函数。
async function fn() {
await exec(open, 3000);
await exec(putIn, 2000);
await exec(close, 1000);
}
fn();
当同步函数返回了一个Promise对象后,由于then()的函数式异步,从而将异步转为同步。
实质上,await()返回的数据就是Promise的then()返回的数据。
总结
常见的异步操作汇总如下——
①计时器: setTimeout【经过多少毫秒执行一次】/ setInterval【每隔多少毫秒执行一次】;
②ajax:jquery的ajax方法的async参数为true时(默认状态)为异步;
③Promise的then() / catch()是异步。
④事件是异步的。
异常
try-catch-finally
当我们认为一段代码可能会出现错误时,就可以尝试(try)执行这段代码,并捕获(catch)其中可能出现的错误,为了不让这个错误阻断我们的程序,我们还需要让程序抛出错误之后还能最终执行(finally)。
error
如果我们捕获到了这个错误,并想要让这个错误在控制台输出出来,就可以catch(err),并使用console.error(err)将这个错误输出。形参err就是我们捕获的错误。
throw
对于异常而言,有一些常见的异常对象,由new + 构造函数获取,并由throw抛出。
由于常见的异常对象的父类都是Error()类型的,所以我们也常常使用下面的形式统一抛出错误——
throw new Error('自定义的错误提示语句');
比如,我们定义一个常量a,当常量a的值小于100时,我们抛出自定义的异常——
const a = 10;
if(a < 100) {
throw new Error("错误:a不足100");
}
结束语
本期内容到此结束。关于本系列的其他博客,可以查看我的JS进阶专栏。
在全栈领域,博主也只不过是一个普通的萌新而已。本系列的博客主要是记录一下自己学习的一些经历,然后把自己领悟到的一些东西总结一下,分享给大家。
文章全篇的操作过程都是笔者亲自操作完成的,一些定义性的文字加入了笔者自己的很多理解在里面,所以仅供参考。如果有说的不对的地方,还请谅解。
==期待与你在下一期博客中再次相遇==
——临期的【H2O2】