JavaScript性能优化3——浏览器执行JavaScript时底层的堆栈操作
Posted JIZQAQ
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JavaScript性能优化3——浏览器执行JavaScript时底层的堆栈操作相关的知识,希望对你有一定的参考价值。
目录
一、堆栈准备
- JS执行环境(比如现在常见的就是V8):代码最终是会被转为能够运行的机器码
- 执行环境栈(ECStack,execution context stack):在这里执行机器码。浏览器在渲染过程中,会在我们的内存当中去开辟一片内存空间,专门用来执行代码,这个栈内存说的就是执行环境栈。
- 执行上下文:管理代码执行,让不同代码之间保持独立,不能相互影响。
- VO(G),全局变量对象:所有变量声明都是存放在这个对象占据的空间当中。
最初的时候,浏览器从我们计算机的内存当中申请或者开辟一个空间,我们把这个空间称之为执行环境栈。但是我们不能把所有代码内容全部放在整体的执行环境栈当中,不同区域的代码是需要隔离开的。因此我们需要执行上下文管理不同的区,之后每个执行上下文中的代码在需要执行的时候进栈操作,例如我们连续调用多个函数。但是无论我们如何操作,全局的执行上下文肯定是存在的。因此栈底永远有一个ECG,也就是全局执行上下文。
而代码的执行步骤,对于全局来说:先要做编译,这块包含我们之前提及的词法分析、语法分析、预解析等等这些过程。接下来就是代码执行了,不过为了便于分析,我们在这个过程中人为添加一个变量提升。
二、堆栈机制
- 基本数据类型是按照值进行操作,它是存放在栈区当中的
- 对于我们的引用类型来说,我们有个空间叫堆区,然后他会把地址放在栈区里面,直接通过一个变量对它进行引用
- ECStack是执行环境栈,里面会存放执行上下文,不同的执行上下文用EC表示,栈底永远放着一个全局执行上下文EC(G)。
- 当前执行上下文代码执行完毕会有一个出栈的操作,出栈之后存放的变量和值是会被释放的。他引用的对象会不会就释放就看垃圾回收机制了。
- GO(全局对象) ,它并不是VO(G),但是它也是一个对象。这个全局对象,相当于是我们JS或者浏览器为我们准备好的,对我们感知上讲就是window,里面存放的很多东西我们可以直接对它进行调用。
1.基本数据类型
代码:
var x = 100
var y = x
y = 200
console.log(x)
输出结果:
基本数据类型是按照值进行操作,它是存放在栈区当中的,并没有引用关系。所以x和y是两个不同的值,修改y也就不会对x发生任何变动。
2.对象类型
修改同属性
代码:
var obj1 = { x:100 }
var obj2 = obj1
obj2['x'] = 200
console.log(obj1.x)
输出结果:
像上面所说,对于我们的引用类型来说,我们有个空间叫堆区(heap),然后他会把地址放在栈区里面,直接通过一个变量对它进行引用。这个例子里面我们修改obj2['x'] = 200的时候,直接把heap里面的数值变成了200、因为obj1和obj2用了引用的是同一个地址,所以obj1.x也变成了200
新赋值
代码:
var obj1 = { x:100 }
var obj2 = obj1
//obj2['x'] = 200
obj2 = {name: 'ali'}
console.log(obj1.x)
输出结果:
即使写了obj2 = obj1,但是下一行的obj2 = { name: 'ali' }直接给obj2新的赋值,完全和obj1无关了,在heap中新辟了一片内存空间来存放内容,obj2指向的是新的内存空间0x001,而obj1指向的仍然是0x000,之后我们对obj2进行任何操作都不会对obj1发生影响。
看下我对obj2进行别的操作也不会影响obj1的例子:
var obj1 = { x:100 }
var obj2 = obj1
//obj2['x'] = 200
obj2 = {name: 'ali'}
console.log(obj1.x)
console.log(obj2.x)
obj2['x'] = 200
console.log(obj1.x)
console.log(obj2.x)
输出结果:
执行obj2 = { name: 'ali' }后,如上面线框图所示,直接开辟了一个新的内存空间,obj2指向新的这片空间。所以第一次打印obj2.x是undefined,因为我们压根没有声明过这个属性。执行obj2['x'] = 200的时候,是在0x001当中进行创建的,和0x000毫无关系,所以最后打印出来obj1.x 为100, obj2.x为200也是符合预期的。
复杂样例分析
代码:
var obj1 = { x: 100 }
var obj2 = obj1
obj1.y = obj1 = {x:200}
console.log(obj1.y)
console.log(obj2)
输出结果:
我们可以看看右边黄色框框里面,老师写了类似var a = b = 1这种类型的语法,实际上的执行方式。它可以拆解为var a = 2,a = b,b = 1三步。因为运算优先级的问题,obj1.y是先执行的,无论obj1.y放在了前面还是后面。不过,好在这和老师列的三步也不冲突。
于是对照老师的分析的obj1.y = obj1 = {x:200},它会变成
- obj1.y = { x: 200 }
- obj1.y = obj1
- obj1 = { x: 200 }
1.的时候我们在0x000中新增一个y,指向一块新的内存空间0x001,里面放上x: 200。
2.的时候,我理解下来应该要将0x000空间里面的y原本指向的0x001改为0x000本身。按照这个分析的话,应该obj1和obj1.y会变成下面这这样一个玩意。
3.这步骤,直接把obj1指向的0x000空间,改成了0x001空间。
这也是为什么我们打印obj1.y会显示undefined的原因,因为obj1指向的空间里面,现在压根不存在obj1.y了。而obj2指向的还是我们的0x000空间。但是又不太一样,按照前面我理解的,应该obj2也会输出个玩意,但实际上obj2输出的是
.
我这下有点懵了,于是直接把三步代码替换成原本的代码运行了一次
var obj1 = { x: 100 }
var obj2 = obj1
obj1.y = obj1 = {x:200}
console.log(obj2)
var obj1 = { x: 100 }
var obj2 = obj1
obj1.y = { x: 200 }
obj1.y = obj1
obj1 = { x: 200 }
console.log(obj2)
输出结果:
……到这里,我火气都要上来了。这说明我推理过程完全没有出错,问题是老师给了却没有展开说明白的那个拆解的三步出了问题。
行吧,怪我JS基础语法不扎实,错了也发现不了。这也给我一个教训,千万不要轻易相信别人…
那还是自己动手丰衣足食,打开MDN
赋值运算符(=) - javascript | MDN
看到链式赋值部分,有这么一个例子
// 假设已经存在以下变量
// x = 5
// y = 10
// z = 25
x = y // x 为 10
x = y = z // x, y 都为 25
MDN上也没有做过多解释,不过我中文阅读理解还过得去的话,x = y = z应该是等价于x = z和y = z
顺便搜了下其他网站的介绍
JS连续运算
嗯,这基本就确认了我的猜想。
回到上面这个例子
将obj1.y = obj1 = {x:200}拆解为
- obj1.y = { x: 200 }
- obj1 = { x: 200 }
1.的时候我们在0x000中新增一个y,指向一块新的内存空间0x001,里面放上x: 200。
2.直接把obj1指向的0x000空间,改成了0x001空间。
同样obj1.y会显示undefined的原因也得到解释,因为obj1指向的空间里面,不存在obj1.y。
而obj2指向的还是我们的0x000空间。0x000空间里面,x:100,y呢指向的是0x001里面是x:200。和我们预期的结果终于匹配上了…感动的落泪
3.函数堆栈处理
代码:
var arr = ['zce', 'alishi']
function foo (obj) {
obj[0] = 'zoe'
obj = ['拉钩教育'] //这是在foo的作用域里面重新赋值声明的obj,并且指向一个新的内存空间,所以从这之后,我们对obj操作其实是不影响外面的arr的。
obj[1] = '大前端'
console.log('obj:',obj)
}
foo(arr)
console.log('arr:',arr)
输出结果:
函数创建
函数创建可以将函数名称看作是变量,存放在VO中,同时它的值就是当前函数对应的内存地址。函数本身也是一个对象,创建时候会有一个内存地址,空间内存放的就是函数体代码(字符串形式的)
函数执行
函数执行时会行程一个全新私有上下文,它里面有一个AO,用于管理这个上下文中的变量
步骤
- 确定作用域链<当前执行上下文,上级作用域所在的执行上下文>
- 确定this
- 初始化arguments
- 形参赋值:相当于变量声明,将声明的变量放置于AO
- 变量提升
- 代码执行
还是前面这段代码,在这里我们把形参赋值的步骤给注释掉
var arr = ['zce', 'alishi']
function foo (obj) {
obj[0] = 'zoe'
// obj = ['拉钩教育'] //形参赋值,这是在foo的作用域里面重新赋值声明的obj,所以从这之后,我们对obj操作其实是不影响外面的arr的。
obj[1] = '大前端'
console.log('obj:',obj)
}
foo(arr)
console.log('arr:',arr)
输出结果:
由于我们没有给obj重新声明赋值,所以这个时候对obj进行任何操作,直接改变的是obj指向的内存地址的,外面的arr指向的和obj指向的是同一个内存地址,所以外面的arr的值也会发生改变。
4.闭包堆栈处理
代码:
var a = 1
function foo() {
var b = 2
return function (c) {
console.log(c + b++)
}
}
var f = foo()//因为f一直引用着foo(),所以foo()调用时候创建的执行上下文不能被释放,所以每次我们修改的b其实都是同一个内存空间里面的b。
f(5)
f(10)
输出结果:
首先先复习一下JS基本的语法,b++和++b的区别。
++b 被称为前自加,其后面的变量执行自加操作,其运算为,先执行自加操作,再引用b值。
b++ 被称为后自加,其前面的变量执行自加操作,其运算为,先引用b值,再进行自加操作。
所以如果我们把代码里的b++改为++b的话,输出结果会变成
好了,复习完了,回到最开始b++的那份代码。
关于闭包
闭包是一种机制
- 保护:当前上下文当中的变量和其他的上下文中变量互不干扰
- 保存:当前上下文中的数据(堆内存)被当前上下文以外的上下文中变量引用,这个数据就会被保存下来了
- 函数调用形成了一个全新的私有上下文,在函数调用之后,当前上下文不被释放的就是闭包(临时不被释放)
因为f一直引用着foo(),所以foo()调用时候创建的执行上下文不能被释放,所以每次我们修改的b其实都是同一个内存空间里面的b。
我把代码进行改造,不再用f引用着foo()
var a = 1
function foo() {
var b = 2
return function (c) {
console.log(c + b++ )
}
}
foo()(5)
foo()(10)
那么这个时候,每次调用foo()其实都是创建了一个新的执行上下文,开辟了一块新的内存空间。所以每次b都是新的b。我们能看到下面输出结果里面,foo()(10)就不再是13了而是12.
当然这个情况下也不形成闭包了。
优化
了解闭包在堆栈中执行的机制之后,我们知道假如像前面的代码,f = foo()如果一直引用着的话,我们会有一片堆内存无法释放,即使我们后续不再使用f了。所以,当我们确定我们后续不再使用的时候,需要手动把f设置为null来释放内存。
var a = 1
function foo() {
var b = 2
return function (c) {
console.log(c + b++)
}
}
var f = foo()//因为f一直引用着foo(),所以foo()调用时候创建的执行上下文不能被释放,所以每次我们修改的b其实都是同一个内存空间里面的b。
f(5)
f(10)
f = null
以上是关于JavaScript性能优化3——浏览器执行JavaScript时底层的堆栈操作的主要内容,如果未能解决你的问题,请参考以下文章