二、重新认识js作用域

108 阅读4分钟

之前讲到了js全局代码执行过程,里面只有变量的执行。我们都知道js中函数是一等功名,在遇到函数时代码会怎么执行呢? 🤔

  1. 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的函数执行上下文将会弹出栈。没有其他地方引用,则会被销毁

  1. 作用域链🍋

如果把上述代码改成这样呢?

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中找不到就会去全局作用域找,以此类推,形成作用域链

  1. 下面来看一道经典面试题😃
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

总结:

  • 函数的作用域链在定义的时候就已经确定了,与调用的位置没有关系
  1. js垃圾回收机制👧

js内部实内存回收的方式有以下几种:
🎬 引用计数

什么是引用计数呢?简单来说就是当前变量被其他变量引用的次数,通过记录当前变量被引用的次数,来判断是否是垃圾。如果被引用0次,则可以回收。

但是,此方法有一种弊端,就是存在互相引用的情况。如果不手动置为null,则两个变量互相引用,永远不会被收回,所以引出标记清楚法。

📺 标记清除

标记清除法的原理就是以全局对象为根节点,来往下寻找,如果不在根节点下面,说明没有代码中没有用到,可以清除。

下一篇简单了解一下闭包,敬请期待。。。