JavaScript函数执行、作用域链以及内存管理详解
作者:既白biu
前言
在我们平常编写JavaScript代码的时候,难免会用到函数,函数里面会有各种变量,这些变量的作用的范围,以及在使用内存存储这些变量时,内存管理的问题,在平时编程亦或者面试时,多多少少都会遇到,所以这篇文章针对这三个问题,进行了深入的探讨。
函数执行
首先说一下JavaScript执行代码的顺序,JavaScript在执行一段可执行代码的时候,会创建一个执行上下文栈(Execution Context Stack 简称ECStack),执行全局代码时创建的全局执行上下文(Global Execution Context 简称GEC),以及执行函数时创建的函数执行上下文(Function Execution Context 简称FEC),在运行时都会按顺序放入栈中,而不管是全局执行上下文还是函数执行上下文在创建时都会有一个变量对象(variable Object)。
全局执行上下文
在JavaScript执行全局代码时,会创建一个全局执行上下文,放入执行上下文栈,还有一个GlobalObject(GO),全局执行上下文中会有一个变量对象(variable Object),指向GO。
在编译阶段,GO会对在全局定义的变量初始化为undefined,当遇到函数时,便会以函数名作为GO的一个属性名,值为存储这个函数空间的内存地址,在这个函数空间中,会有函数的执行体(代码段),还会存储这个函数父级作用域。
编译完成后,代码开始执行,便会对这些变量赋值,当然里面除了这些,还有一些全局的对象和函数,比如setTimeout,Date,String等等,还有一个属性window赋值为this,当遇到函数时,便会创建一个函数执行上下文放入执行上下文栈中。
函数执行上下文
上文说到,代码执行时候,执行到函数时,会创建函数执行上下文,并且函数执行上下文放入执行上下文栈中,同样,在函数执行上下文里面,会有一个VO(Variable Object)变量对象,这里的VO其实指向AO(Activation Object),这里的AO类似于GO,只不过它不是全局的,而是函数特有的,在执行函数内部代码前,即编译阶段,也会将变量赋值为undefined,如果里面嵌套函数,类似GO,会以函数名作为GO的一个属性名,值为存储这个函数空间的内存地址,在这个函数空间中,会有函数的执行体(代码段),还会存储这个函数父级作用域,然后执行时,将变量赋值,如果里面嵌套的函数被执行,也会创建函数执行上下文,并且这个函数执行上下文放入执行上下文栈中。
作用域链
其实在创建VO对象时,也会在函数执行上下文中创建作用域链,这个作用域链包括,自身的变量对象(VO)和父级作用域,当我们查找一个变量时,真实的查找路径是沿着作用域链来查找
这段代码中,显然name会顺着作用域链查找到“why”,然后显然在foo 函数编译未执行阶段,m=undefined,然后执行,m输出的应该是undefined,如下图是代码的执行逻辑图。
这里的message输出的应该是Hello Global,foo函数在全局初始化时父级作用域已经为全局了,然后foo函数执行时,找不到message变量便会去父级作用域去寻找,也就是全局作用域,所以输出的是Hello Global,执行逻辑图如下。
上图的输出是undefined而不是100,就因为foo函数在解析时碰到return var a=100已经认为定义了一个a,赋值为undefined,但在执行时却不会执行到这一步。
这里要提一下,如果变量在定义时,未加任何约束。
比如通常来说定义一个变量是 var name=“anonymous”,let name="anonymous"如果直接写成name=”anonymous“,在其他语言中,这肯定会报错,但是在JavaScript中,允许这种写法,并且这种写法定义的变量会直接加到GO里面。
function foo(){ var a=b=10 } foo() console.log(a,b)
var a=b=10<==>等同于var a=10;b=10;
这样的话b放入GO中,在全局输出值为10;而a仅在函数中被定义,在全局输出显然会报错。
内存管理
不管什么样的编程语言,在代码的执行过程中都是需要给它分配内存的,不同的是某些编程语需要我们自己手动的管理内存,某些编程语言会可以自动帮助我们管理内存:
不管以什么样的方式来管理内存,内存的管理都会有如下的生命周期:
- 第一步:分配申请你需要的内存(申请)
- 第二步:使用分配的内存(存放一些东西,比如对象等) ;
- 第三步:不需要使用时,对其进行释放;
不同的编程语言对于第一步和第三步会有不同的实现:
手动管理内存:比如C、 C++ ,都是需要 手动来管理内存的申请和释放的 (malloc和free)
自动管理内存:比如Java、JavaScript. Python. Swift、 Dart等 ,它们有自动帮助我们管理内存;
引用计数
当一个对象有一个引用指向它时,那么这个对象的引用就+1 ,当一个对象的引用为0时,这个对象就可以被销毁掉;
这个算法有一个很大的弊端就是会产生循环引用:
var obj1={info:obj2}; var obj2={info:obj1}
标记清除
这个算法是设置一个根对象( root object) ,垃圾回收器会定期从这个根开始,找所有从根开始有引用到的对象,对于哪些没有引用到的对象,就认为是不可用的对象;
这个算法可以很好的解决循环弓|用的问题;
JS引擎比较广泛的采用的就是标记清除算法,当然类似于V8弓|擎为了进行更好的优化,它在算法的实现细节上也会结合一些其他的算法。
以上就是JavaScript函数执行、作用域链以及内存管理详解的详细内容,更多关于函数执行、作用域链以及内存管理的资料请关注脚本之家其它相关文章,希望大家以后多多支持脚本之家!