稳扎稳打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 条评论
登录 后参与评论

相关文章

来自专栏Java开发者杂谈

关于多态

  多态是面向对象的核心思想之一,多态的实现有三要素: 1、 存在继承 2、子类对父类的方法进行了重写 3、父类引用指向子类对象。   前面说的还是有点虚,下面...

3057
来自专栏cnblogs

记Javascript一道题的理解

代码如下: function Foo(){ getName = function(){ console.log("1"); } return t...

2048
来自专栏Albert陈凯

scala 隐式详解(implicit关键字)

掌握implicit的用法是阅读Spark源码的基础,也是学习Scala其它的开源框架的关键,implicit 可分为: 隐式参数 隐式转换类型 隐式调用...

2759
来自专栏我爱编程

Day7函数式编程3/3

装饰器 由于函数也是一个对象,而且函数对象可以被赋值给变量,所以,通过变量也能调用该函数。 >>> def now(): ... print('2018...

3387
来自专栏微信公众号:Java团长

详解equals()方法和hashCode()方法

Java的基类Object提供了一些方法,其中equals()方法用于判断两个对象是否相等,hashCode()方法用于计算对象的哈希码。equals()和ha...

431
来自专栏lzj_learn_note

3-序列、列表、元组

序列就是一堆数据元素的集合,并对每个元素进行编号。在Python中,字符串、列表、元组都属于序列,他们都具有一些特定的操作,如索引、切片、相加、相乘、in、长度...

753
来自专栏小小挖掘机

由问题入手,步步爬出Python中赋值与拷贝的坑

1、问题的引出 之前遇到了一个求数组中出现次数最多的k个元素的题,我们参照如下的思路进行求解,首先利用一个dict记录所有元素出现的次数,key:value中的...

36210
来自专栏吴伟祥

Java类初始化顺序 转

关于构造函数,以下几点要注意: 1.对象一建立,就会调用与之相应的构造函数,也就是说,不建立对象,构造函数时不会运行的。 2.构造函数的作用是用于给对象进行...

713
来自专栏行者常至

004.golang 常量与运算符

Go虽然保留了指针,但与其它编程语言不同的是,在Go当中不 支持指针运算以及”->”运算符,而直接采用”.”选择符来操作指针 目标对象的成员

781
来自专栏猿人谷

const用法小结

常类型是指使用类型修饰符const说明的类型,常类型的变量或对象的值是不能被更新的。因此,定义或说明常类型时必须进行初始化。 概述 1. const有什么...

1677

扫描关注云+社区