JavaScript 中的 try catch 语句的性能分析
由于我的上篇博客Array.prototype.forEach 方法详解提出了两种跳出 forEach 循环的方法。
但网上的博客,几乎都只提及第一种方法(try catch + throw 方法),而第二种方法(splice + return 方法)是自己在了解了 forEach 方法的原理之后想到的。
所以我想对比一下这两种方法的性能,所以需要对比 splice 方法与 try catch 语句的性能。
//方法一:try catch + throw 方法
const arr = [1, 2, 3, 4];
try {
arr.forEach((item) => {
if (item === 2) {
throw new Error(`值为${item}时跳出forEac循环`);
}
console.log(item); //只打印 1
});
} catch (e) {
console.log(e); //Error: 值为2时跳出forEac循环
}
console.log(arr); // [1, 2, 3, 4]
//方法二:splice + return 方法
//在迭代时 使用splice方法 删除数组中的元素
const arr = [1, 2, 3, 4];
let spliceArr = null;
arr.forEach((item, index) => {
if (item === 2) {
spliceArr = arr.splice(index); // 将 2 以后的元素全部删除 并赋值给spliceArr
return;
}
console.log(item); // 1
});
arr.splice(arr.length, spliceArr.length, ...spliceArr); //将删除的元素拼接回去
console.log(arr); // [1, 2, 3, 4]
1.splice 方法的基本介绍与原理
对于 splice 方法,大家可以参考下面两篇博客。
再看了 splice 方法的源码之后,发现 splice 方法内部是调用了一次循环的。
所以如果我们简单的用大 O 表示法来分析的话:
- 方法一:try catch + throw 的性能为 O(n)。
- 方法二:splice + return 的性能为 O(n²)。
但参阅了相关资料后又发现 try catch 语句也是一种非常消耗性能的语句,所以我们需要对 try catch 语句进行分析。
2.try catch 语句的性能分析
对try catch 语句不熟悉的朋友,可以参考下面三篇文章。
我所使用的性能分析方法主要参考JS 中用 try catch 对代码运行的性能影响分析博客中的方法,共分了五种情况来讨论。
- 空白组1. 无 try catch 的情况下,对数据取模1千万次耗时
- 参照组2:在 try 内部直接执行耗时代码
- 参照组3:在 try 内部调用拥有耗时代码的函数
- 参照组2:在 catch 内部直接执行耗时代码
- 参照组2:在 catch 内部调用拥有耗时代码的函数
该博客得到结果是:运行环境为 Chrome51 时,分别得到的结果为 98.2ms;1026.9ms;107.7; 1028.5ms; 105.9ms
。
但与我自己所做的实验结果不同,或许是版本与环境的问题,因为现代浏览器都会对我们的代码进行优化操作,我的测试结果请见下面的内容。
2.1 运行环境:Node.js v16.16.0
Node.js的测试代码如下:
//空白组1. 无 try catch 的情况下,对数据取模1千万次耗时
!(function () {
var t = new Date(); //耗时代码开始
for (var i = 0; i < 100000000; i++) {
var p = i % 2;
}
console.log("1:", new Date() - t); //耗时代码结束
})();
//参照组2:在 try 内部直接执行耗时代码
!(function () {
var t = new Date(); //耗时代码开始
try {
for (var i = 0; i < 100000000; i++) {
var p = i % 2;
}
throw new Error();
} catch (e) {}
console.log("2:", new Date() - t); //耗时代码结束
})();
//参照组3:在 try 内部调用拥有耗时代码的函数
!(function () {
var t = new Date(); //耗时代码开始
function run() {
for (var i = 0; i < 100000000; i++) {
var p = i % 2;
}
}
try {
run();
throw new Error();
} catch (e) {}
console.log("3:", new Date() - t); //耗时代码结束
})();
//参照组4:在 catch 内部直接执行耗时代码
!(function () {
var t = new Date(); //耗时代码开始
try {
throw new Error();
} catch (e) {
for (var i = 0; i < 100000000; i++) {
var p = i % 2;
}
}
console.log("4:", new Date() - t); //耗时代码结束
})();
//参照组5:在 catch 内部调用拥有耗时代码的函数
!(function () {
function run() {
for (var i = 0; i < 100000000; i++) {
var p = i % 2;
}
}
var t = new Date(); //耗时代码开始
try {
throw new Error();
} catch (e) {
run();
}
console.log("5:", new Date() - t); //耗时代码结束
})();
五种方法的结果:均在 60-80ms 之内,所以我推断应该是现代浏览器会对我们的代码进行优化操作而造成的,大家可以自己在本地环境运行看看。
2.2 运行环境:Chrome 版本(105.0.5195.127) 或 Microsoft Edge 版本(105.0.1343.42)
测试代码如下:
- 无 try catch 的情况下,对数据取模1千万次耗时**[空白组]**
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>1. 无 try catch 的情况下,对数据取模1千万次耗时[空白组]</title>
</head>
<body>
<h1 id="h1"></h1>
<script>
//空白组1:[无 try catch 的情况下对数据取模1千万次耗时]
!(function () {
const h1 = document.querySelector("#h1");
var t = new Date(); //耗时代码开始
for (var i = 0; i < 100000000; i++) {
var p = i % 2;
}
h1.innerText = `情况1:无 try catch 消耗性能为${new Date() - t}ms`; //耗时代码结束
})();
</script>
</body>
</html>
- 在 try 内部直接执行耗时代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>2.将耗时代码用 try 包围,内联耗时代码</title>
</head>
<body>
<h1 id="h1"></h1>
<script>
//参照组2:在 try 内部直接执行耗时代码
!(function () {
const h1 = document.querySelector("#h1");
var t = new Date(); //耗时代码开始
try {
for (var i = 0; i < 100000000; i++) {
var p = i % 2;
}
throw new Error();
} catch (e) {}
h1.innerText = `情况2:try 内部执行消耗性能为${new Date() - t}ms`; //耗时代码结束
})();
</script>
</body>
</html>
- 在 try 内部调用拥有耗时代码的函数
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>3.将耗时代码用 try 包围,try调用耗时代码的函数</title>
</head>
<body>
<h1 id="h1"></h1>
<script>
//参照组3:在 try 内部调用拥有耗时代码的函数
!(function () {
const h1 = document.querySelector("#h1");
var t = new Date(); //耗时代码开始
function run() {
for (var i = 0; i < 100000000; i++) {
var p = i % 2;
}
}
try {
run();
throw new Error();
} catch (e) {}
h1.innerText = `情况3:try 内部调用消耗性能为${new Date() - t}ms`; //耗时代码结束
})();
</script>
</body>
</html>
- 在 catch 内部直接执行耗时代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>4.在 catch 内部直接执行耗时代码</title>
</head>
<body>
<h1 id="h1"></h1>
<script>
//参照组4:在 catch 内部直接执行耗时代码
!(function () {
const h1 = document.querySelector("#h1");
var t = new Date(); //耗时代码开始
try {
throw new Error();
} catch (e) {
for (var i = 0; i < 100000000; i++) {
var p = i % 2;
}
}
h1.innerText = `情况4:catch 内部执行消耗性能为${new Date() - t}ms`; //耗时代码结束
})();
</script>
</body>
</html>
- 在 catch 内部调用拥有耗时代码的函数
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>5.在 catch 内部调用耗时代码的函数</title>
</head>
<body>
<h1 id="h1"></h1>
<script>
//参照组5:在 catch 内部调用拥有耗时代码的函数
!(function () {
const h1 = document.querySelector("#h1");
var t = new Date(); //耗时代码开始
function run() {
for (var i = 0; i < 100000000; i++) {
var p = i % 2;
}
}
try {
throw new Error();
} catch (e) {
run();
}
h1.innerText = `情况5:catch 内部调用消耗性能为${new Date() - t}ms`; //耗时代码结束
})();
</script>
</body>
</html>
结果:这五种方法的结果也都差不多,首次运行时会比较耗费时间都在 100ms 以上,但重新刷新页面时耗费时间都在 60-80ms 左右,与 node.js 环境结果差不多。
3.跳出 forEach 循环的两种方法的性能对比
性能分析的代码如下:
- 方法一:try catch + throw 方法
//方法一:try catch + throw 方法
!(function () {
const arr = new Array(100000).fill(0).map((item, index) => index + 1);
console.time("testTime1"); //开始计时
// const start = Date.now();
try {
arr.forEach((item) => {
if (item === 50000) {
throw 1;
}
});
} catch (e) {}
console.timeEnd("testTime1"); //结束计时
// console.log("runtime:", Date.now() - start);
})();
2.方法二:splice + return 方法
//方法二:splice + return 方法
!(function () {
const arr = new Array(100000).fill(0).map((item, index) => index + 1);
console.time("testTime2"); //开始计时
// const start = Date.now();
//当数组元素过多时 会报错 RangeError: Maximum call stack size exceeded
let spliceArr = null;
arr.forEach((item, index) => {
if (item === 50000) {
spliceArr = arr.splice(index); // 将 2 以后的元素全部删除 并赋值给spliceArr
return;
}
});
arr.splice(arr.length, spliceArr.length, ...spliceArr); //将删除的元素拼接回去
console.timeEnd("testTime2"); //结束计时
// console.log("runtime:", Date.now() - start);
})();
结果如下:第一种方法明显优于第二种方法,但差距大小受跳出循环条件与数组大小影响,但第一种方法总体上都优于第二种。
所以大家开发时:推荐使用方法一进行跳出forEac循环,但其实我们完全可以不使用forEach方法,因为遍历的方法有很多种,且大部分性能比forEach方法好,比如:for循环。但大家还是得按照实际应用场景来选择最合适的方法,抛开场景谈性能等于耍流氓。
但是如果是面试时被问到:如何跳出forEac循环?
你要是能答出个大概,相信肯定会给面试官眼前一亮的感觉。
结语
这是我目前所了解的知识面中最好的解答,当然也有可能存在一点的误区。
所以如果对本文存在疑惑,可以去评论区进行留言,欢迎大家指出文中的错误观点。
码字不易,觉得有帮助的朋友点赞,关注走一波。