之前讲到了js全局代码执行过程,里面只有变量的执行。我们都知道js中函数是一等功名,在遇到函数时代码会怎么执行呢? 🤔
- js函数执行过程🍊
var name = "wkb";
foo(123);
function foo(num) {
console.log(m) // undefined
var m = 20;
var n = 10;
console.log("foo");
}
图看着有点复杂,我们来解释一下:
之前我们说过了变量的执行过程,这里我们遇到了函数。
(1).在编译阶段遇到函数时,如果是在全局执行上下文中,首先会创建一个地址用来存放函数。存放的内容有:
- parent scope 父级作用域
- 函数执行体,也就是代码块
然后在go对象中新foo,并指向刚刚创建的内存地址。
这时编译阶段就完成了,下面进入函数执行阶段。
(2).在执行foo函数时,会去内存中找到对应的地址的函数,然后函数放到执行栈中。函数会创建自己的函数执行上下文(FEC)。这里的VO也是Activation Object,一开始就是定义的变量,初始化为undefined。然后开始执行函数里的代码,给变量赋值。
(3). 当函数执行完成之后,foo的函数执行上下文将会弹出栈。没有其他地方引用,则会被销毁
- 作用域链🍋
如果把上述代码改成这样呢?
var age = 18;
foo(123);
function foo(num) {
console.log(m) // undefined
var m = 20;
var n = 10;
function bar () {
console.log(age) // 18
}
bar()
}
我们都知道结果,age打印出18。我们来看一下原理。
其实在编译函数创建VO时还会创建一个scope chain(作用域链: 当前作用域加上父级作用域)
当bar中的VO找不到age时就会去父级作用域找,也就是foo。foo中找不到就会去全局作用域找,以此类推,形成作用域链
- 下面来看一道经典面试题😃
var message = "global";
function foo() {
console.log(message);
}
function bar() {
var message = "bar";
foo();
}
bar();
有了以上的知识,我们很容易就知道结果打印的是global,我们来分析一下代码执行过程。
编译阶段:
- 先创建一个全局对象global data,存放内置属性
- 然后往后编译到message,把message放到go中,并初始化undefined
- 遇到foo函数,在内存中开辟一个地址。假设内存地址是0xa00,该地址中存放目前来说两个东西:父级作用域parent scope,和函数代码。foo是在全局中定义的函数,则foo的父级作用域就是go。go中存储foo的内存地址。
- 同理bar函数
代码执行阶段:
- 首先会创建一个代码执行栈ECStack,为代码执行提供环境。全局代码执行时,会创建一个全局执行上下文GECS,其中包括VO对象,此时的VO对象就是GO,还有全局的代码体。注意:代码还没开始执行
- 接着执行代码,也就是message的赋值,message = 'global'
- 后面执行bar() 。 从go中找到bar函数的内存地址,从内存中取出函数,创建一个 函数执行栈放入ECStack中。此时会创建一个Activation Object(AO)存储当前函数内部定义的变量。则AO中message初始化为undefined。
- 之后执行bar函数里的代码。message被赋值为bar,后面开始执行foo函数。
- 同理先创建foo函数执行上下文,初始化AO,然后执行foo函数内的代码。
- 执行console.log(message),会从当前的作用域链scope chain中查找。当前的scope chain由VO和parent scope组成,则从父级作用域中查到message为global
总结:
- 函数的作用域链在定义的时候就已经确定了,与调用的位置没有关系
- js垃圾回收机制👧
js内部实内存回收的方式有以下几种:
🎬 引用计数
什么是引用计数呢?简单来说就是当前变量被其他变量引用的次数,通过记录当前变量被引用的次数,来判断是否是垃圾。如果被引用0次,则可以回收。
但是,此方法有一种弊端,就是存在互相引用的情况。如果不手动置为null,则两个变量互相引用,永远不会被收回,所以引出标记清楚法。
📺 标记清除
标记清除法的原理就是以全局对象为根节点,来往下寻找,如果不在根节点下面,说明没有代码中没有用到,可以清除。
下一篇简单了解一下闭包,敬请期待。。。