前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >稳扎稳打JavaScript(一)——作用域链内存模型

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

作者头像
大闲人柴毛毛
发布2018-03-09 17:30:42
1.1K0
发布2018-03-09 17:30:42
举报
文章被收录于专栏:大闲人柴毛毛大闲人柴毛毛

几个概念

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

1.1. 作用域

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

1.2. 变量对象

  • 变量对象用于存储函数各自的局部变量;
  • 每个函数都有各自的变量对象,并且在函数执行时被创建;
  • 上文提到:“每个函数都有各自的作用域,用于存储函数的局部变量”,其实这句话并不严谨。作用域中存储的其实是变量对象的引用,而变量对象才是存储函数局部变量的地方,就像这样:
这里写图片描述
这里写图片描述

1.3. 作用域链

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

1.4. 执行环境

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

JavaScript的作用域控制机制

2.1. JS作用域的内存模型

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

先来看如下代码:

代码语言:javascript
复制
var 全局变量 = "柴毛毛";

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

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

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

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

  1. 首先初始化全局执行环境
这里写图片描述
这里写图片描述

当初始化全局执行环境时,会进行如下操作:

  • 创建全局变量对象。其中包含所有的全局变量,上述代码中分别是“全局变量”和“外层函数”。
  • 创建全局作用域链。该作用域链中只包含一个全局变量对象。
  • 创建外层函数的作用域链。我们知道,函数一旦被初始化后就会创建它的作用域链,只不过这个作用域链中不包含函数本身的作用域,只包含其父级函数的作用域链。这里就是全局作用域。
  • 创建全局执行环境。全局环境通过一个指针指向它的作用域链,作用域链中又通过指针指向它的变量对象。

  1. 调用外层函数时
这里写图片描述
这里写图片描述

当调用“外层函数”时,会进行如下操作:

  • 创建外层函数的变量对象。变量对象中包含外层函数的全部局部变量,这里分别是“局部变量1”和那个匿名函数。
  • 将当前函数的作用域添加到当前函数作用域链的顶部。也就是把先前创建的“外部函数作用域链”中第一个作用域的指针指向“外部函数变量对象”,第二个指针指向“全局变量对象”。
  • 将“外层函数的执行环境”压入执行环境栈的顶部。PS:执行环境栈顶表示当前正在执行的环境。

  1. 调用闭包时
这里写图片描述
这里写图片描述

调用闭包时,会进行如下操作:

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

2.2. 变量查找

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

2.3. JS没有块级作用域

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

2.4. JS作用域类型

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

代码语言:javascript
复制
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语句块原理一样,请自行脑补吧。

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

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2016年12月10日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 几个概念
    • 1.1. 作用域
      • 1.2. 变量对象
        • 1.3. 作用域链
          • 1.4. 执行环境
          • JavaScript的作用域控制机制
            • 2.1. JS作用域的内存模型
              • 2.2. 变量查找
                • 2.3. JS没有块级作用域
                  • 2.4. JS作用域类型
                  领券
                  问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档