深入理解JavaScript的执行环境、作用域与作用域链及闭包

执行环境(执行上下文EC)

来自JS高设--执行环境定义了变量或函数有权访问的其他数据,决定了它们各自的行为。每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个变量中。虽然我们编写的代码无法访问这个对象,但解析器在处理数据时会在后台使用它。每个函数都有自己的执行环境,当执行流进入一个函数时,函数的环境就会被推入一个环境栈中。而在函数执行后,栈将其环境弹出,把控制权返回给之前的执行环境。ECMAScript程序中的执行流正是由这个方便的机制控制着。

理解--执行环境始终是this关键字的值,它是拥有当前所执行代码的对象的引用。执行环境是基于对象来说的,比如 window对象在全局环境中。

执行环境可以分为执行和创建两个阶段。

1.创建阶段(当函数被调用,但未执行任何其内部代码之前),

  • 解析器首先会创建一个变量对象(VO),它由定义在执行环境中的变量,函数声明,参数组成。
  • 作用域链会被初始化
  • this的值也会被确定。

2.执行阶段

  • 初始化变量的值和函数的引用,代码被解释执行。

执行环境栈(ECS)

浏览器中的JS解析器被实现为单线程,这意味着同一时间只能发生一件事情,其他行为或事件将会被放在一个叫做执行栈的内存中排队。

单线程栈抽象视图

当浏览器首次载入脚本代码,它将默认进入全局执行环境。如果在全局代码中调用一个函数,程序的时序将进入被调用的函数,并创建一个新的执行环境并将其压入执行栈的顶部。浏览器总会首先执行位于栈顶的执行环境,运行结束后就从栈顶弹出,把控制权返回给之前的执行环境。类推,堆栈中的执行环境会被依次执行并弹出堆栈,知道回到全局执行环境。

<script type="text/javascript">
    function Fn1(){
        function Fn2(){
           
        }
        Fn2();
    }
    Fn1();
    
</script>

代码执行环境栈表示图 (图片来自笨蛋的座右铭

变量对象(VO)& 活动对象(AO)

  • JS的执行环境中都有个对象用来存放执行环境中可被访问但是不能被delete的函数标识符、形参、变量声明等。它们会被挂载这个叫做”变量对象“的对象上,对象的属性对应它们的名字,属性的值对应它们的值。这个对象是在规范上的不可在JS环境中访问到的活动对象。
  • 有了变量对象存每个执行环境中的东西,但是它什么时候能被访问到呢?就是每进入一个执行环境,这个执行环境中的变量就被激活,也就是该执行环境中的函数标识符,形参,变量声明等就可以被访问到了。

    活动对象是被激活的变量对象

 

作用域

作用域是什么

  • 简单来说,作用域就是变量与函数的可访问范围。即作用域控制着变量与函数的可见性和声明周期。

作用域分为哪几种

  • 全局作用域:在代码中任何地方都能访问到的对象拥有全局作用域
  1. 最外层函数和在最外层函数外面定义的变量拥有全局作用域
  2. 所有未定义直接赋值的变量自动声明为拥有全局作用域
  3. 所有window对象的属性拥有全局作用域。
  • 局部作用域(函数作用域):一般只在固定的代码片段内可访问到,而对于函数外部是无法访问的。
  1. 在函数内部声明的变量及函数。

作用域链

来自JS高设--当代码在一个环境中执行的时候,会创建变量对象的一个作用域链。作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。

作用域链的前端,始终都是当前执行的代码所在环境的变量对象。如果这个环境是函数,则将其活动对象作为变量对象。活动对象最开始时只包含一个变量,即arguments对象(这个对象在全局环境中是不存在的),而作用域链中的下一个变量对象来自包含(外部)环境,再下一个变量对象则来自下一个包含环境。这样一直延续到全局执行环境;全局执行环境的变量对象始终都是作用域链中的最后一个对象。

理解--根据内部函数可以访问外部函数变量的这种机制,所有可被内部函数访问的变量在被访问时会根据一种链式机制来进行访问,而这种包含且由有权访问的变量组成的链式机制,就称为作用域链。

作用域链的本质是一个指向变量对象的指针列表,它只引用但不实际包含变量对象。

图解作用域链(此处图片来自JavaScript关于作用域、作用域链和闭包的理解

由执行环境知道,当某个函数被调用的时候,就会创建一个执行环境以及相应的作用域链,并把作用域链赋值给一个特殊的内部属性[scope],然后使用this,argumens和其他命名参数的值来初始化函数的活动对象。当前执行环境的变量始终在作用域链的第0位。

1.

      var scope = "global"; 
      function fn1(){
         return scope; 
      }
      function fn2(){
         return scope;
      }
      fn1();
      fn2();

上述代码中,当运行fn1( )时的作用域链图解:

由图可知,fn1( )的作用域里并没有scope变量,于是沿着作用域链向上一次查找,结果在全局作用域里找到了变量scope,返回此值,结束查找。

到现在,我们可以很清楚的知道,作用域的一个作用就是完成标识符解析。标识符解析是沿着作用域链一级一级的搜素标识符的过程。搜索过程始终从作用域链的前端开始,然后逐级向后回溯,直到找到标识符未知。如果找不到标识符,通常会导致错误。

2.

      function outer(){
         var scope = "outer";
         function inner(){
            return scope;
         }
         return inner;
      }
      var fn = outer();
      fn();

在outer( )函数内部返回了inner( )函数。所以在调用outer( )函数时,inner( )函数的作用域链就已经被初始化了,即复制父函数的作用域链,再在作用域链最前端插入自己的活动对象,具体如下图

一般来说,当某个执行环境中的所有代码执行完毕后,该执行环境会被弹出环境栈,保存在其中的所有变量和函数也随之销毁。全局执行环境变量会到应用程序推出后销毁,如关闭网页。但针对上面所说的这种情况,当outer( )函数执行后,其执行环境被销毁,但是其关联的活动对象并没有随之销毁,而是一直存在于内存中,因为该活动对象被其内部函数的作用域链所引用。

  • outer( )执行结束后,内部函数开始被调用。
  • outer( )执行环境等待被回收,其作用域链对全局变量对象和对自身内活动对象的引用断了。

像上面这种内部函数的作用域链仍然保持着对父级函数活动对象的引用,就是闭包。

闭包

闭包,即 有权访问其他函数作用域内变量的一个函数,本质上是将函数内部和外部连接起来的一座桥梁。

先看一段典型的代码

<script>
    function aArray() {
        var result=new Array;
        for(var i=0;i<10;i++){
            result[i]= function () {
                return i;
            }
        }
        return result;
    }
    var f=aArray();
    console.log(f[0]()); //10
    console.log(f[1]()); //10
    console.log(f[2]()); //10
</script>

上面的函数会返回一个函数数组。表面上,似乎每个函数都应该返回自己的索引值,但实际上每个函数都返回10.这是因为每个函数的作用域链中都保存着aArray( )函数的活动对象,所有它们引用的都是同一个变量i。当aArray( )函数返回后,变量i的值是10,此时每个函数都引用着保存变量i的同一个变量对象,所以在每个函数内部i的值都是10。

由此也可以知道,js函数内的变量值不是在编译的时候就确定的,而是等在运行时期才去寻找的。且由于作用域链的本质是一个指向变量对象的指针列表,所以有一个副作用,就是闭包只能取得包含函数中任何变量的最后一个值

如何让代码达到预期呢?看下面的代码

    function aArray() {
        var result=new Array;
        for(var i=0;i<10;i++){
            result[i]= function (num) {
                return function (){
                    return num;
                }
            }(i);
        }
        return result;
    }
    var f=aArray();
    console.log(f[0]()); //0
    console.log(f[1]()); //1
    console.log(f[2]()); //2

在这段代码中,我们没有直接把闭包赋给数组,而是定义了一个匿名函数,并将立即执行该匿名函数的结果赋给数组。这里的匿名函数有一个num,也就是最终函数要返回的值。在调用每个匿名函数时,我们传入了变量i,由于函数参数是按值传递的,所以就会将变量i的当前值复制给参数num。而在这个匿名函数内部,又创建并返回了一个访问num的闭包。这样一来,result数组中的每个函数都有自己num变量的一个副本,因此就可以返回各自不同的数值了。

总结--闭包的两个作用

  • 可以在函数内部读取函数外部的变量
  • 可以让这些变量一直保存在内存中
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值