JavaScript程序异常捕获的正确姿势

112 阅读10分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第16天,点击查看活动详情

“只要是人所编写的程序,就有可能会出现漏洞。我们所要做的就是尽可能的去降低程序中的漏洞所带来的损失或影响”,本篇我们来学习下,编写JavaScript时,如遇到了程序的异常,该如何正确的进行捕获。

这里我们先举一个简单的例子,有一个fun函数用来调用后台接口,其中我们定义一个data,中间的话经过一系列请求响应处理,最后将data返回。

 /* 程序的异常捕获 */
 const fun = function() {
     const data = null;
     // ajax doSomething  调用接口,获得数据
     return data
 }

假如说中间的处理过程中存在一些失误,导致data返回了一个null值,那么在去调用这个函数的时候,然后去通过data去获取对象属性,肯定是会抛出异常的,这里我们来执行一下:

 /* 程序的异常捕获 */
 const fun = function() {
     const data = null;
     // ajax doSomething  调用接口,获得数据
     return data
 };
 const data = fun();
 console.log(data.name); // => 异常:TypeError: Cannot read property 'name' of null
 ​
 console.log('after fun');  // 不会执行

这时我们会发现在打印data.name这行,会抛出一个TypeError异常,提示说无法获取到nul上的name属性,这也是前端开发的时最常见的一个TypeError的类型的异常。这段代码抛出异常之后,它后面的语句console.log(after fun),是不会执行的,因为程序已经被中断了。

假如说后面是有一些很重要的逻辑代码的话,我们就必须要对错误代码做异常的捕获,来防止它对后面的代码产生很大的影响。我们一般使用try...catch...语句来去做异常捕获,我们来试一下。

 /* 程序的异常捕获 */
 try {
     const fun = function() {
         const data = null;
         // ajax doSomething  调用接口,获得数据
         return data
     };
     const data = fun();
     console.log(data.name);
 } catch (e) {
     console.log(e.message); // Cannot read property 'name' of null
 }
 console.log('after fun'); // 正常执行 after fun

我们可以发现,使用try...catch...语句之后,异常之后会被catch捕获住,并把异常的信息打印出来 Cannot read property 'name' of null,然后后面的语句console.log('after fun'); // 正常执行 after fun是正常执行,就是使用try...catch...语句不会影响到后面的代码的执行。这就是我们今天所讲解的关于异常捕获中的主要语法,即try...catch...finally...和return。

接下来,我们会通过8段实例代码来看一下try...catch...finally...的一些重要的特性。

1 try...catch...的执行顺序

 try {
     console.log(1);
     throw new Error('Error1')
     console.log(2);
 } catch (e) {
     console.log(e.message);
 }
 console.log('outter1');

上述代码中,在try语句里,我们使用throw模拟抛出一个异常,那么这里的 console.log(2);会不会执行呢?最后后面的 console.log('outter1');会不会执行呢?我们来看一下执行结果:

 index.html:28 1
 index.html:32 Error1
 index.html:34 outter1

很明显,我们看到了28行先打印了数字1,接着throw异常时被catch捕获住,通过log在32行去把异常的信息e.message给打印出来,即'Error1'。但是 console.log(2);未被执行,说明一旦程序遇到throw语法抛出异常后,后续的代码是不会执行。但是不会影响try...catch...外层的语句,它依然会执行到 console.log('outter1');,这个的话就是try catch的执行顺序。接下来我们来看一下第二段代码。

2 try...catch...finally...的执行顺序

 try {
     console.log(4);
     throw new Error('Error2')
     console.log(5);
 } catch (e) {
     console.log(e.message);
 } finally {
     console.log(6);
 }
 console.log('outter2');

第2段代码增加了一个finally,我们执行下来看try...catch...finally...执行顺序:

 index.html:29 4
 index.html:32 Error2
 index.html:34 6
 index.html:36 outter2

首先,try中的第一条语句肯定执行,然后 throw一个Error2,被catch捕获后,打印了出来。紧接着执行了在finally里的 console.log(6);,最后执行外层外层的代码,打印出outter2。那么这段代码表示finally里的代码,始终会被执行,这里需要大家谨记这一点。

3 catch中的throw

 try {
     console.log(7);
     throw new Error('Error3')
     console.log(8);
 } catch (e) {
     console.log(e.message);
     throw new Error('Error4') // catch 中 抛出异常
     console.log(9);
 } finally {
     console.log(10);
 }
 console.log('outter3');

这段代码比较经典,在 catch中去抛出了一个异常,最后会怎么去执行呢?我们来执行一下:

 index.html:29 7
 index.html:32 Error3
 index.html:36 10
 index.html:33 Uncaught Error: Error4 at index.html:33

第一行打印数字7肯定会被执行,然后Error3被捕获,并输出。那么我们在catch抛出的异常 throw new Error('Error4')执行的话,因为外层没有再包一层catch捕获,所以执行到这里,就会报错Uncaught Error: Error4 at index.html:33,导致程序终止。然而程序终止状态并不会影响到finally里的console.log(10);执行,但是会影响外层的语句console.log('outter3');的执行。

这就是我们对于 throw的一个理解,假如说我们在catch中同样去抛出一个异常,我们在当前的catch里面是无法捕获的,只有在外面再包一层try...catch...的话,才能捕获住里层catch中的异常 ,例如:

 try {
     try {
         console.log(7);
         throw new Error('Error3')
         console.log(8);
     } catch (e) {
         console.log(e.message);
         throw new Error('Error4') // catch 中 抛出异常
         console.log(9);
     } finally {
         console.log(10);
     }
 } catch (err) {
     console.log(err.message);
 }
 console.log('outter3');

在try...catch...外面再包上一层try...catch...,这个时候程序才能把这个里层catch中throw new Error('Error4') 抛出的异常给捕获住,程序顺序执行,并输出最后面的outter3:

 index.html:29 7
 index.html:33 Error3
 index.html:37 10
 index.html:40 Error4
 index.html:44 outter3

我们可以简单理解,try...catch...只能捕获当前try代码块里的异常,而在catch中的异常,自身catch是无法捕获的,需要再外面再包一层try...catch...。

4 try中包含retrun语句

接下来,我们看下面代码,在try代码块中包含了return语句会怎么执行?

 function fun1() {
     try {
         console.log(11);
         return 'try中的return'
     } catch (e) {
         console.log(e.message);
     } finally {
         console.log(12);
     }
     console.log('outter4');
     return 'try外层的return'
 }
 console.log(fun1());

这里,我们定义了一个fun1的函数,然后在函数中,添加了try...catch...finally...,分别在try代码块里和外层return了两段文字,最后打印fun1()的执行结果:

 index.html:30 11
 index.html:35 12
 index.html:40 try中的return

我们会发现,首先执行了打印数字11,这个是毋庸置疑。紧接着try里的return语句并未顺序执行,而是先把finally里12打印出来了,这个告诉我们什么?就是说如果我们在try...catch...finally...一旦遇到了return语句,程序是会先把finally中的逻辑先执行完,再去执行try中的return。同时如果在函数中一旦遇到了return语句,整个程序就会终止执行,跳出函数,所以外层outter4和return语句 return 'try外层的return',将不会被执行。

5 catch中包含return语句

上面的例子是在try中包含return,下面我们看一下如果在catch中存在return会是怎样的结果:

 function fun2() {
     try {
         console.log(13);
         throw new Error('Error5')
     } catch (e) {
         console.log(e.message);
         return 'catch中的return'
     } finally {
         console.log(14);
     }
     console.log('outter5');
     return 'try外层的return'
 }
 console.log(fun2());

这里我们稍微调整了下,在try中抛出了异常Error5,同时在catch中添加了一个return,来看一下执行结果:

 index.html:31 13
 index.html:34 Error5
 index.html:37 14
 index.html:42 catch中的return

我们会发现,依然是先打印try中的13,然后是捕获异常打印error5,接下来执行finally里的打印14,最后执行catch中的return语句,和上例try中的return一样,如果函数中遇到第一个return的话,后面的代码,即打印outter5和return语句将不会执行了。

6 finally中包含return语句

最后我们看一下,如果在finally里存在return的话,程序的执行顺序。

 function fun3() {
     try {
         console.log(15);
         throw new Error('Error5')
         return 'try中的return'
     } catch (e) {
         console.log(e.message);
         return 'catch中的return'
     } finally {
         console.log(16);
         return 'finally中的return'
     }
     console.log('outter5');
     return 'try外层的return'
 }
 console.log(fun3());

在上面定义的fun3函数中,分别在try、catch、finally和外层均存在return语句,我们执行一下:

 index.html:31 15
 index.html:35 Error5
 index.html:38 16
 index.html:44 finally中的return

通过执行结果,我们可以得出结论:如果在try、catch、finally和外层均存在return语句,程序执行时会忽略try和catch中的return语句,同时外层的return也不会执行,最后执行的是finally中的return语句。

7 finally中的语句会不会影响return的值?

结合上面的几个例子,我们可以发现finally的语句,是肯定会执行的,那么finally语句会不会影响到try或catch中的return的值?我们看下面这个例子:

 function fun4() {
     let a = 1;
     try {
         console.log(17);
         a = 2;
         return a; 
     } catch (e) {
         console.log(e.message);
     } finally {
         console.log(18);
         a = 3;
         console.log('finally a:', a);
     }
     console.log('outter6');
     return 'try外层的return'
 }
 console.log(fun4());

在fun4函数中,定义了一个变量a值为1,在try中修改a的值为2,在finally里修改a的值为3,我们起那面说过,finally的里的语句肯定会执行,那么最后这个a的值是多少呢?

 index.html:32 17
 index.html:39 18
 index.html:41 finally a: 3
 index.html:46 2

从执行结果来看,finally里的语句执行了,并输出了finally a: 3,这里a值确实发生变化了,但是最终a的值却是因try中a = 2所影响,并不会因finally里的语句a = 3所改变,这说明了,在finally中的语句执行是不会影响到我们在try或者catch中return的a值,大家一定要深刻的去理解。

接着,我们深入研究下,上面这个例子的a的值,是一个简单数值类型,如果我们把a变为引用类型的话,在代码结构不变的条件下,这段结论是否生效?

 function fun5() {
     let a = {
         name: '掘金'
     };
     try {
         console.log(18);
         a.name = '掘银';
         return a;
     } catch (e) {
         console.log(e.message);
         return 'catch中的return'
     } finally {
         console.log(19);
         a.name = '掘铜';
         console.log('finally a:', a);
     }
 }
 console.log(fun5());

现在我们将a变成一个对象,其有一个name属性,初始叫做掘金,在try中修改为掘银,在catch中修改为掘铜,我们看一下执行结果:

 index.html:34 18
 index.html:41 19
 index.html:43 finally a: {name: "掘铜"}
 index.html:48 {name: "掘铜"}

我们可以看到43行代码打印的a,已经把name属性改成掘铜了,最终结果也是掘铜,这就说明因为a是引用类型,对于finally中的a与 try中的a,它们指向的是同一份内存引用,如果说把内存引用中的值进行改变的话,它们是会相互影响的。

关于finally中的修改数据,a是引用类型和a是简单类型的执行结果不一样。因为简单类型是值传递,值传递的话,如果说我们对值进行修改的话,是不会影响到原有的值返回,但我们对引用的对象的值进行改变的话,它是会有影响的,这点需要我们谨记。

8 finally中return是否影响try或catch的return结果?

最后,我们再深入研究下,如果在finally语句中return a的值,结果是什么?首先,我们以简单类型举例:

 function fun6() {
     let a = 1;
     try {
         console.log(17);
         a = 2;
         return a;
     } catch (e) {
         console.log(e.message);
         return 'catch中的return'
     } finally {
         console.log(18);
         a = 3;
         console.log('finally a:', a);
         return a
     }
 }
 console.log(fun6()); // 3

我们在finally里修改了a的值为3,然后return a,最后执行结果是3,如果是引用类型呢?

 function fun7() {
     let a = {
         name: '掘金'
     };
     try {
         console.log(17);
         a.name = '掘银';
         return a;
     } catch (e) {
         console.log(e.message);
         return 'catch中的return'
     } finally {
         console.log(18);
         a.name = '掘铜';
         console.log('finally a:', a);
         return a
     }
 }
 console.log(fun7());  // => {name: "掘铜"}

我们可以看到,最后也是执行的finally的a的name值,这就说明如果说我们在finally中有一个执行的return语句,它会提前将程序终止,而不再执行到try中进行return。那么,提醒大家一定要在平时写代码的时候注意,尽量不要在finally中使用return语句,否则很可能会拿不到try中去返回的结果,这个是需要大家严格注意。

总结

最后,我们将今天所讲的知识点进行总结如下:

  1. catch用于捕获当前try中的异常,如果catch中抛出的异常,则需要再外层嵌套catch中捕获;

  2. 无论try或catch是否执行或执行怎样,finally中的语句都会执行,即使其中包含return;

  3. try或catch中的return值,如果是简单类型,finally中的语句不会改变其值。如果是引用类型,则会会改变其值。

  4. 如果finally中存在return并对try或catch中的返回值修改的话,程序会提前退出,其返回值是finally中修改后的值,而不是try或catch中的值。