JavaScript性能优化3——浏览器执行JavaScript时底层的堆栈操作

目录

一、堆栈准备

二、堆栈机制

1.基本数据类型

2.对象类型

修改同属性

新赋值

复杂样例分析

3.函数堆栈处理

函数创建

函数执行

步骤

4.闭包堆栈处理


一、堆栈准备

  • JS执行环境(比如现在常见的就是V8):代码最终是会被转为能够运行的机器码
  • 执行环境栈(ECStack,execution context stack):在这里执行机器码。浏览器在渲染过程中,会在我们的内存当中去开辟一片内存空间,专门用来执行代码,这个栈内存说的就是执行环境栈。
  • 执行上下文:管理代码执行,让不同代码之间保持独立,不能相互影响。
  • VO(G),全局变量对象:所有变量声明都是存放在这个对象占据的空间当中。

最初的时候,浏览器从我们计算机的内存当中申请或者开辟一个空间,我们把这个空间称之为执行环境栈。但是我们不能把所有代码内容全部放在整体的执行环境栈当中,不同区域的代码是需要隔离开的。因此我们需要执行上下文管理不同的区,之后每个执行上下文中的代码在需要执行的时候进栈操作,例如我们连续调用多个函数。但是无论我们如何操作,全局的执行上下文肯定是存在的。因此栈底永远有一个ECG,也就是全局执行上下文。

而代码的执行步骤,对于全局来说:先要做编译,这块包含我们之前提及的词法分析、语法分析、预解析等等这些过程。接下来就是代码执行了,不过为了便于分析,我们在这个过程中人为添加一个变量提升。

二、堆栈机制

  1. 基本数据类型是按照值进行操作,它是存放在栈区当中的
  2. 对于我们的引用类型来说,我们有个空间叫堆区,然后他会把地址放在栈区里面,直接通过一个变量对它进行引用
  3. ECStack是执行环境栈,里面会存放执行上下文,不同的执行上下文用EC表示,栈底永远放着一个全局执行上下文EC(G)。
  4. 当前执行上下文代码执行完毕会有一个出栈的操作,出栈之后存放的变量和值是会被释放的。他引用的对象会不会就释放就看垃圾回收机制了。
  5. 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},它会变成

  1. obj1.y = { x: 200 }
  2. obj1.y = obj1
  3. 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

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Assignment#%E7%AE%80%E5%8D%95%E8%B5%8B%E5%80%BC%E5%92%8C%E9%93%BE%E5%BC%8F%E8%B5%8B%E5%80%BC

看到链式赋值部分,有这么一个例子

// 假设已经存在以下变量
//  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连续运算

http://c.biancheng.net/view/5490.html

嗯,这基本就确认了我的猜想。 

回到上面这个例子

将obj1.y = obj1 = {x:200}拆解为

  1. obj1.y = { x: 200 }
  2. 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,用于管理这个上下文中的变量

步骤

  1. 确定作用域链<当前执行上下文,上级作用域所在的执行上下文>
  2. 确定this
  3. 初始化arguments
  4. 形参赋值:相当于变量声明,将声明的变量放置于AO
  5. 变量提升
  6. 代码执行

还是前面这段代码,在这里我们把形参赋值的步骤给注释掉

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++的那份代码。

关于闭包

闭包是一种机制

  1. 保护:当前上下文当中的变量和其他的上下文中变量互不干扰
  2. 保存:当前上下文中的数据(堆内存)被当前上下文以外的上下文中变量引用,这个数据就会被保存下来了
  3. 函数调用形成了一个全新的私有上下文,在函数调用之后,当前上下文不被释放的就是闭包(临时不被释放)

因为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

参考资料

1.拉勾网 《大前端训练营》课程

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值