JSBench
JSBench 是在线测试JS执行效率的网站。
其他工具还有 JSPerf ,但已停止维护。
使用 JSBench 进行测试的时候,建议只打开一个标签页,因为浏览器是支持多线程的,随着打开的标签页越来越多,其他标签页会抢占当前标签页的资源。
在运行测试的时候,最好保持在当前页面,或者说当前的进程不要关掉,因为操作系统是可以同时进行多件事情的,代码的执行是需要消耗时间的,如果将当前页面关闭(最小化)去做其他事情,它有可能会被挂起,等到再回来看的时候,计算的时间不一定是准确的。
另外做测试的时候,应该多执行几次脚本,不能用一次的结果作为最终结论。
注意,声明变量的时候使用 var
,否则 JSBench 识别不了。
堆栈中代码执行流程
示例代码
let a = 10
function foo(b) {
let a = 2
function baz(c) {
console.log(a+b+c)
}
return baz
}
let fn = foo(2)
fn(3)
流程图示
-
EC(Execution Context):执行上下文
- 代码执行所在的词法作用域
-
Stack:栈
-
ECStack(Execution Context Stack):栈内存(执行环境栈),用于存储执行上下文
-
Heap:堆内存,允许程序在运行时动态申请空间存储数据
-
VO/AO:存放变量的对象
- 在每一个上下文代码执行的时候,都可能会创建变量
- 每个上下文中都会有一个存储当前上下文中变量的空间
- 全局上下文称为VO(Variable Object),私有上下文中称为AO(Active Object,活动对象)
-
字面量的值是基本数据类型,存放在栈内存,因为执行上下文也在栈内存,所以访问字面量数据速度很快
-
函数属于引用数据类型,它需要在堆内存中申请一个空间去存储,然后将地址返回给上下文
- 这就是为什么在使用引用类型的时候,如果嵌套层级很深,查询速度会很慢,因为变量会依次去每个层级的引用地址中查找。
-
函数创建的执行上下文
- 首先确定 this 的指向
- 接着初始化作用域链
- 然后才会初始化 AO
- 初始化 arguments
- 为参数变量赋值
流程介绍
- JS 代码在开始执行后,首先会创建一个执行环境栈(栈内存),用于存放执行上下文
- 首先会创建全局上下文,存放到执行环境栈,称为入栈
- 首先初始化当前上下文的变量对象 VO
- 基本数据类型直接存放在栈内存中,例如 全局变量 a
- 引用数据类型要在堆内存申请空间去存储,例如 函数 foo
- 函数在堆内存中存储的内容包括:
- 函数的定义
- 函数的形参
- 函数的参数数量
- 等
- 上下文中给函数变量赋值的就是堆内存中访问这个数据的地址 例如 foo = AB1
- 函数在堆内存中存储的内容包括:
- 首先初始化当前上下文的变量对象 VO
- 当运行到函数调用的代码 foo(2),就会创建该函数的本地执行上下文,并入栈
- 函数执行上下文中
- 首先确定 this 的指向,foo 函数的 this 指向 window
- 接着初始化作用域链
- 然后初始化 AO
- 初始化参数 arguments
- 为参数变量赋值:b = 2
- 初始化 baz 的时候又在堆内存开辟了空间去存储数据
- 最终将 baz 的访问地址返回
- 当前函数执行完毕,就回去判断是否产生了闭包
- 由于外部调用的 fn 实际上就是函数内定义的 baz,baz 内部有使用了 foo 函数中定义的 a 变量,所以产生了闭包
- 所以此时 foo 的上下文还不能被销毁,它会被下移
- foo 中申请的堆内存 AB2 也不能被回收
- 函数执行上下文中
- 初始化 fn 的值是 baz 的访问地址,当 fn 被调用时,实际上就是 baz 被调用,于是继续创建 baz 的执行上下文并入栈
- baz 函数执行上下文中初始化的过程同 foo 一样
- 当执行到打印命令的时候,会从作用域链查找每个变量的值
- 当 baz(3) 被执行完毕,判断没有产生闭包就会销毁 baz 的上下文(出栈)
- 接着全局上下文出栈,AB2 被回收
- foo(2) 的执行上下文出栈,AB1 被回收
汇总
- JS 代码在开始执行后,首先会创建一个执行环境栈(栈内存),用于存放执行上下文
- 首先会创建存储一个全局执行上下文,然后每当函数被调用时都会创建存储这个函数的本地执行上下文。
- 上下文初始化变量时:
- 基本数据类型的值直接存放在栈内存,由 JS 主线程进行回收(出栈)。
- 引用类型的值存放在堆内存中,由 GC 进行回收。
- 每个上下文的代码执行完成以后,由是否产生闭包来决定
- 当前的上下文中引用的堆是否要释放掉
- 当前上下文是否要出栈,不出栈就会下移
减少判断层级
在编写代码的过程中,有可能出现 if else 多层嵌套的场景,可以通过提前 return 无效的条件,减少判断和嵌套层级,达到优化效果。
// 多层嵌套
function doSomething(name, age) {
const persons = ['张三', '李四', '王五', '赵六']
if (name) {
if (persons.includes(name)) {
console.log('是名单上的人')
if (age < 16) {
console.log('年龄不达标')
}
}
} else {
console.log('请确认姓名')
}
}
doSomething('李四', 6)
// 减少判断层级
function doSomething(name, age) {
const persons = ['张三', '李四', '王五', '赵六']
if (!name) {
console.log('请确认姓名')
return
}
if (!persons.includes(name)) return
console.log('是名单上的人')
if (age < 16) {
console.log('年龄不达标')
}
}
doSomething('李四', 6)
JSBench 测试:
减少判断层级本质上是解决问题的算法作了调整,与内存、作用域关系不大。
if else 更适合做区间性的条件判断,如果判断存在大量 else if 并且判断的值都是固定的,可以使用 switch,这样代码更简洁且便于维护。
减少作用域链查找层级
代码在访问变量的时候会根据作用域链一层一层去查找,减少作用域链的查找层级,可以提高代码运行的速度。
var name = 'Tom'
function foo() {
name = 'Jack' // 全局的 name
function baz() {
var age = 30
console.log(age)
console.log(name)
}
baz()
}
foo()
这段代码一共创建了三个作用域:全局作用域 -> foo 作用域 -> baz 作用域
尽管 foo 作用域中为 name 变量重新赋值,但是 name 还是属于全局作用域。
在打印 name 的时候会先在 baz 作用域查找变量 name,接着去 foo 作用域查找,最后去全局作用域查找。
如果在 foo 作用域重新声明一个 name 变量,则会减少查找的层级,提高运行速度:
var name = 'Tom'
function foo() {
var name = 'Jack' // 全局的 name
function baz() {
var age = 30
console.log(age)
console.log(name)
}
baz()
}
foo()
不过这种方式虽然提高了运行速度,但是又额外占用了内存,因为 foo 作用域的 name 变量会占用一部分内存。
如果 name 存放的数据量比较大,第一段代码相对会节省空间。
字面量与构造式
声明一个基本类型的数据,可以直接使用字面量,或使用构造函数创建。
使用构造函数,相当于增加一个函数调用的工作,所以速度相对慢很多。
并且构造函数创建的是一个对象类型,会占用更多的内存。
// 构造式
var str = new String('工作使我快乐')
console.log(str)
// 字面量
var str = '工作使我快乐'
console.log(str)
如果创建的是引用类型,效率也不会差很多。
var obj = {}
// or
var obj = new Object()
采用事件委托
事件委托的本质就是采用 JS 冒泡的机制把原本需要绑定在子元素的响应事件委托给了父元素,让父元素去完成事件的监听,这样可以大量减少内存的占用和事件的注册。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>遍历注册事件</title>
</head>
<body>
<ul id="ul">
<li>TOM</li>
<li>18</li>
<li>工作使我快乐</li>
</ul>
<script>
function showText(e) {
console.log(e.target.innerHTML)
}
var list = document.querySelectorAll('li')
for (var item of list) {
item.onclick = showText
}
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>采用事件委托</title>
</head>
<body>
<ul id="ul">
<li>TOM</li>
<li>18</li>
<li>工作使我快乐</li>
</ul>
<script>
function showText(e) {
if (e.target.nodeName.toLowerCase() === 'li') {
console.log(e.target.innerHTML)
}
}
var ul = document.querySelector('ul')
ul.addEventListener('click', showText, true)
</script>
</body>
</html>
PS:不要用 JSBench 调试,会卡死。
其它优化方法
以下优化方法在 JSBench 测试执行速度上并没有明显提升或更快。
减少数据读取次数
在 JS 中经常使用的数据表现形式主要包括:字面量,局部的变量,数组元素,对象属性。
其中字面量和局部的变量接存储在栈区,所以访问速度是最快的。
而数组元素和对象属性,访问速度相对较慢,这是因为访问它们需要按照引用关系,先找到它们在堆内存中的位置,而且对象属性的访问,往往还要考虑原型链上的查找。
与作用域链的道理一样,要减少查询时间的消耗,就应该尽量减少对象属性的查找次数和属性的嵌套层级。
常见的做法就是提前把对象的数据进行缓存,方便后续进行使用。
减少循环体中的活动
把每次循环都要操作的不变的数据,都放到循环外面完成。
减少声明及语句数
减少声明和语句数实际上是减少 JS 词法分析的工作。
JS 代码在运行之前会进行词法分析,将代码按照一定的规则拆分成多个词法单元,接着做语法分析,生成 AST 抽象语法树,最终将语法树转化成代码去执行。
选择
虽然有些方法可以提高JS的执行速度,但可能是建立在内存损失的前提下。
所以不但要考虑执行速度,也要考虑内存占用,是否易于维护和阅读等因素。