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

几个概念

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

1.1. 作用域

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

1.2. 变量对象

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

1.3. 作用域链

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

1.4. 执行环境

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

JavaScript的作用域控制机制

2.1. JS作用域的内存模型

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

先来看如下代码:

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。 来看如下代码:

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语句块原理一样,请自行脑补吧。

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

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏落花落雨不落叶

canvas画简单电路图

59611
来自专栏飞扬的花生

jsencrypt参数前端加密c#解密

      写程序时一般是通过form表单或者ajax方式将参数提交到服务器进行验证,如何防止提交的请求不被抓包后串改,虽然无法说绝对安全却给非法提交提高了难度...

3859
来自专栏菩提树下的杨过

Flash/Flex学习笔记(23):运动学原理

先写一个公用的小球类Ball: package{ import flash.display.Sprite; //小球 类 public class B...

25210
来自专栏张善友的专栏

Mix 10 上的asp.net mvc 2的相关Session

Beyond File | New Company: From Cheesy Sample to Social Platform Scott Hansel...

2527
来自专栏Ceph对象存储方案

Luminous版本PG 分布调优

Luminous版本开始新增的balancer模块在PG分布优化方面效果非常明显,操作也非常简便,强烈推荐各位在集群上线之前进行这一操作,能够极大的提升整个集群...

3095
来自专栏我和未来有约会

Kit 3D 更新

Kit3D is a 3D graphics engine written for Microsoft Silverlight. Kit3D was inita...

2506
来自专栏张善友的专栏

Miguel de Icaza 细说 Mix 07大会上的Silverlight和DLR

Mono之父Miguel de Icaza 详细报道微软Mix 07大会上的Silverlight和DLR ,上面还谈到了Mono and Silverligh...

2677
来自专栏转载gongluck的CSDN博客

cocos2dx 打灰机

#include "GamePlane.h" #include "PlaneSprite.h" #include "BulletNode.h" #include...

5346
来自专栏一个会写诗的程序员的博客

Spring Reactor 项目核心库Reactor Core

Non-Blocking Reactive Streams Foundation for the JVM both implementing a Reactiv...

2132
来自专栏闻道于事

js登录滑动验证,不滑动无法登陆

js的判断这里是根据滑块的位置进行判断,应该是用一个flag判断 <%@ page language="java" contentType="text/html...

6658

扫码关注云+社区