稳扎稳打JavaScript(一)——作用域链内存模型

这里写图片描述

几个概念

在开始之前,先了解几个概念。

1.1. 作用域

  • 作用域是指当前正在执行的代码能够访问到变量的范围;
  • 每个函数都有各自的作用域,存储函数所有的局部变量;

1.2. 变量对象

  • 变量对象用于存储函数各自的局部变量;
  • 每个函数都有各自的变量对象,并且在函数执行时被创建;
  • 上文提到:“每个函数都有各自的作用域,用于存储函数的局部变量”,其实这句话并不严谨。作用域中存储的其实是变量对象的引用,而变量对象才是存储函数局部变量的地方,就像这样:
    稳扎稳打JavaScript(一)——作用域链内存模型_第1张图片

1.3. 作用域链

  • 把多个作用域串起来便形成了作用域链;
  • 每个函数在初始化完成之后就拥有了各自的作用域链,但此时的作用域链中并不包含自己的作用域;只有当函数执行时,才会创建自己的作用域,并加入到作用域链的开头;
  • 作用域链中不仅存储了函数本身的作用域,还存储了该函数能够访问的其他函数的作用域;

1.4. 执行环境

每个正在执行的函数都有一个执行环境,记录了函数执行过程中的各项信息。
除了全局执行环境外,其余函数的执行环境都会随着函数的执行而被创建,函数的执行结束而被销毁。
所有的执行环境会存放在执行环境栈中,只有栈顶的执行环境才有执行权。

JavaScript的作用域控制机制

2.1. JS作用域的内存模型

每个函数都有各自的作用域、作用域链、变量对象、执行环境。
其中,作用域链在函数初始化完成后便存在,而作用域、变量对象、执行环境只有在函数被执行时才创建。
执行结束后,函数的作用域、作用域链、执行环境被销毁;而变量对象仍有可能留在内存中(如果函数内部有闭包,则函数执行结束后变量对象仍然留在内存,直到闭包执行结束,该变量对象才会被销毁)。

先来看如下代码:

var 全局变量 = "柴毛毛";

function 外层函数(){
    var 局部变量1 = "大闲人";

    return function(){
        var 局部变量2 = "是傻逼";
        return 全局变量+局部变量1+局部变量2;
    };
}

var 函数 = 外层函数();
函数();

上述代码对应的内存模型如下:

  1. 首先初始化全局执行环境
    稳扎稳打JavaScript(一)——作用域链内存模型_第2张图片
    当初始化全局执行环境时,会进行如下操作:

    • 创建全局变量对象。其中包含所有的全局变量,上述代码中分别是“全局变量”和“外层函数”。
    • 创建全局作用域链。该作用域链中只包含一个全局变量对象。
    • 创建外层函数的作用域链。我们知道,函数一旦被初始化后就会创建它的作用域链,只不过这个作用域链中不包含函数本身的作用域,只包含其父级函数的作用域链。这里就是全局作用域。
    • 创建全局执行环境。全局环境通过一个指针指向它的作用域链,作用域链中又通过指针指向它的变量对象。
  2. 调用外层函数时
    稳扎稳打JavaScript(一)——作用域链内存模型_第3张图片
    当调用“外层函数”时,会进行如下操作:

    • 创建外层函数的变量对象。变量对象中包含外层函数的全部局部变量,这里分别是“局部变量1”和那个匿名函数。
    • 将当前函数的作用域添加到当前函数作用域链的顶部。也就是把先前创建的“外部函数作用域链”中第一个作用域的指针指向“外部函数变量对象”,第二个指针指向“全局变量对象”。
    • 将“外层函数的执行环境”压入执行环境栈的顶部。PS:执行环境栈顶表示当前正在执行的环境。
  3. 调用闭包时
    稳扎稳打JavaScript(一)——作用域链内存模型_第4张图片
    调用闭包时,会进行如下操作:

    • 销毁“外层函数”的作用域链和执行环境。
    • 创建闭包的变量对象。
    • 创建闭包的作用域,并压入闭包作用域链的头部。
    • 创建闭包的执行环境,并指向闭包的作用域链。
      只有当闭包执行结束后,“外层函数的变量对象”才会被释放,否则它将一直驻留内存,因此闭包会比普通函数占用更多的内存,因此要慎用!

2.2. 变量查找

当上述代码执行到“return 全局变量+局部变量1+局部变量2;”时,此时执行环境栈的栈顶是闭包的执行环境,因此通过闭包的作用域链寻找这三个变量的值。
查找过程首先从作用域链的顶部开始,首先在闭包变量对象中寻找“全局变量”的值,若没有,则去外层函数的变量对象中查找;若仍未找到,则去全局变量对象中查找,直到找到为止;若在全局变量对象中仍未找到,则查找失败。
若在某一个变量对象中找到该值,则立即停止查找。
PS:查找过程必须从作用域链的头部开始,依次向后查找。

2.3. JS没有块级作用域

JS没有块级作用域。因此,在if-else、while、switch-case语句中通过var定义的变量都属于当前所在的函数。

2.4. JS作用域类型

严格来说,JS中只有两种作用域:全局作用域 和 函数作用域。
但还有两种特殊的作用域:catch、with。
来看如下代码:

function fn(person) {
    with(person){
        var personInfo = name+age+location;
    }
}

上述代码在函数中使用with语句,with后需要有一个对象,从而在with语句中使用该对象中的属性就不需要通过person.xxx访问,直接访问其属性即可。
并且,JS没有块级作用域,因此在with、catch中创建的变量将属于离它们最近的函数!
那么,这种功能JS是如何实现的呢?

当执行到一个with语句时,会JS会为其创建一个变量对象,这个变量对象中包含with语句后传入的那个对象的全部属性。
紧接着,为with语句创建一个指向该变量对象的作用域,并添加到当前函数/全局作用域链的头部。
当with语句块结束,该变量对象就会被销毁,作用域也会被弹出。
因此,with语句能临时性延长当前函数/全局作用域链的长度,在with语句块中就可以不带前缀访问对象的属性,因为with中传入的对象已经作为一个变量对象被添加到当前作用域链的头部,通过作用域链的查找规则就能找到该变量对象中的属性。

那么with语句块有何用呢?
如果你要大量用到一个对象的属性,重复写person.xxx太繁琐了,这种情况下你可以使用with语句。
但在严格模式下是禁止使用with语句的,只要了解原理就好,平时尽量避免使用。

catch语句块原理一样,请自行脑补吧。

我花了很长时间画这几张图,各位且看且珍惜。

这里写图片描述

你可能感兴趣的:(稳扎稳打JavaScript)