资讯专栏INFORMATION COLUMN

作用域、作用域链、执行环境和闭包知识点归纳

chnmagnus / 1965人阅读

摘要:全局作用域对应全局变量,局部作用域对应局部变量。通常理解的局部作用域是定义在函数内部的作用域。作用域链当我们定义一个全局变量的时候,它就在全局环境里,作用域链只有一条。

变量对象
js 解析器是如何找到我们定义的函数和变量的?
实际是通过VO(Varible Object)来存储执行上下文所需要存储的比如变量、函数声明、函数参数。进一步我们还需要区分全局变量对象和函数变量对象。
我们定义一个全局变量就会有一个全局变量对象,里面有刚定义的全局变量
定义函数,除了有全局变量对象外,还有函数变量对象。

作用域

简单说分为全局作用域、和局部作用域。全局作用域对应全局变量,局部作用域对应局部变量。通常理解的局部作用域是定义在函数内部的作用域。

作用域链

当我们定义一个全局变量的时候,它就在全局环境里,作用域链只有一条。当我们定义一个函数,首先在定义的时候,或者说在js流执行的时候,会将函数和变量申明提前,而且函数申明比变量更提前。在申明函数的时候,会生成一个scope属性。此时,[[scope]]里面只包含了全局对象【Global Object】。而如果, 我们在A的内部定义一个B函数,那B函数同样会创建一个[[scope]]属性,B的[[scope]]属性包含了两个对象,一个是A的活动对象【Activation Object】【对于函数来说】一个是全局对象,A的活动对象上面,全局对象在下面。以此类摧,每个函数的都在定义的时候创建自己的[[scope]],里面保存着一个类似于栈的格式的数据。

// 外部函数
function A(){
     // 内部函数
    function B(){
     }
}

执行环境

了解函数执行环境前先看下三个概念。

全局执行环境

我们通常可以在代码第一行使用类似String、Math等这样的全局函数,为何能直接使用到?
是因为我们在初始化代码以前,js引擎会把全局的一些东西,我理解成[[global]]会初始化到VO里面。
伪代码:

[[global]] = {
  Math : ...,
  String : ...,
  window : global, // 执行全局对象本身
}

String(10)  // [[global]].String(10)
window.a = 10 // [[global]].window.a = 10
this.b = 10  // [[global]].b = 10

激活对象(通常在函数中才有)

Active Object 缩写AO,该对象有函数的arguments类数组对象和this对象。
**慕课网这一章节说,函数里面 AO === VO
慕课网--Js深入浅出--闭包章节

变量初始化阶段

VO或者AO按照如下顺序初始化:

函数参数(如未传入,初始化该参数值为undefined)

函数声明(如发生命名冲突会覆盖)

变量声明(初始化变量值为undefined,若发生命名冲突会忽略)

通过一个实例来说明变量初始化阶段

function test (a, b) {
   var c = 10
   function d(){}
   var e = function _e(){}
   (function(){}) // 括号括起来的匿名函数,但并没执行,此时函数申明不会提前
   // 这里多说下,括起来的不管是匿名函数,还是正常申明的函数,不仅不会申明提前,还会
   // 形成一个闭包,只有通过自执行才能调用。在全局作用域或在申请的局部作用域调用都
   // 是undefined.这里一个简单粗暴的解释是为何没有提前,如果提前就留下一个括号,岂不
   // 是很奇怪。哈哈。
   b = 20
}

test(10)

AO(test) = {
  a: 10, // a 已传入,所以有值
  b: undefined, // b 未传入,undefined,并且局部变量b发生命名冲突,但被忽略
  c: undefined, // 局部变量
  d: , // 函数申明
  e: undefined // 命名函数_e,赋值给了局部变量e,undefined
}

// 分析发现,局部变量c和e两个局部变量都是undefined,因为开头就说了,变量申明被前置,所以在初始化的时候就是undefined,比如我们在var e = function _e() {} 前面调用e
//() 肯定会报错语法错误,e不是一个函数。

每个函数运行时都会产生一个执行环境,而且同一个函数运行多次时,会多次产生不同的执行环境。js为每一个执行环境关联了一个变量对象。环境中定义的所有变量和函数都保存在这个对象中。 全局执行环境是最外围的执行环境,它所关联的对象就是我们熟知的window对象。js的执行顺序是根据函数的调用来决定的,当一个函数被调用时,该函数环境的变量对象就被压入一个环境栈中。而在函数执行之后,栈将该函数的变量对象弹出,把控制权交给之前的执行环境变量对象。

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


当函数被执行的时候,就是进入这个函数的执行环境,首先会创建一个它自己的活动对象【Activation Object】(这个对象中包含了this、参数(arguments)、局部变量(包括命名的参数)的定义,当然全局对象是没有arguments的)和一个变量对象的作用域链[[scope chain]],然后,把这个执行环境的[scope]按顺序复制到[[scope chain]]里(也有博客说把作用域[[scope chain]]链赋值给[[scope]]),最后把这个活动对象推入到[[scope chain]]的顶部。这样[[scope chain]]就是一个有序的栈,这样保了对执行环境有权访问的所有变量和对象的有序访问。

分析下:
当执行fn1的时候,VO中fn1会指向fn1的执行环境,因为函数申明前置,所以在VO中已经存在function。
可以看到fn1活动对象里并没有scope变量,于是沿着作用域链(scope chain)向后寻找,结果在全局变量对象里找到了scope,所以就返回全局变量对象里的scope值。

闭包

引入闭包代码

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

调用outer()
这里有点不解的是?如果outer()直接调用,然后再调用inner即outer()()不会造成闭包,但把outer赋值给一个全局变量,上面闭包代码的写法,就会成为一个闭包。

var fn = outer();
fn();

分析下:
执行outer函数,在VO对象中outer会指向它的执行环境,当因为变量申明前置,fn在初始化变量的时候是undefined。所以在outer函数执行中的时候,他依然是undefined.

如图,outer执行完后,执行环境不再被VO中的outer所指向,即执行环境已被销毁。
当第一次执行fn到底发生了什么?执行fn即执行inner函数,此时outer的执行环境已被销毁(而且只能有并且只有一个执行环境),而outer()执行后又赋值给了fn,所以可以这么理解,inner执行的时候执行环境也赋值给了fn。由于fn这个全局变量一直存在,除非你手动置为null或关闭浏览器,所以可以认为inner这个函数的执行环境就一直存在,那么inner函数执行环境的相关变量就一直存在。

通过上面加粗文字的理解,趁热打铁,再来看一个函数:

function outer() {
 var a = 1
 return function inner() {
   a ++
 }
}
var f = outer()
console.log(f()) // 2
console.log(f()) // 3

为何f()第二次执行的时候,会是3。结合上面加粗字体的理解,f()在执行完后,outer的AO并没有被释放,当第二次执行f()的时候,由于inner函数AO并没有变量a,所以沿着[[scope]]chain 中查找,结果在outer的AO中找到了变量a,但此时变量a已经是2了,所以再a ++ 后就是3了。除非我们关闭浏览器,或者将f = null,再次执行f(),就会回到初始化的状态。

理解了闭包再来看下面这个代码就很好理解了。

 function outer(){
       var result = new Array();
       for(var i = 0; i < 2; i++){
          //定义一个带参函数
          result[i] = function(num){
            // num 形参
            // 实际num这个形参拷贝了实参i这个变量的一个副本到arguments里。
            // i 的变化就不会影响到这个副本的变化。
             console.log("arguments", arguments)
             // innerarg 的执行环境里面引用了result[i]这个数组函数的活动对象
             // 当执行innerarg 的时候,作用域链会去找num,在result[i]这个数组活动对象的
             // arguments 找到了
             return function innerarg(){
                console.log("num", num)
                return num;
             }
          }(i);//预先执行函数写法
          //把i当成参数传进去  实参
       }
       console.log("result", result)
       return result;
  }

var fn = outer()
fn[0]()
fn[1]()

起初我觉得上面那个代码实在是太复杂,为何要在result[i]这个函数数组里再返回一个函数。起初我是这么写的。结果发现我错了,但又对匿名函数自执行,又重新正确的认识了下。

  function outer(){
     var result = new Array();
     for(var i = 0; i < 2; i++){//注:i是outer()的局部变量
        result[i] = function(num){
           console.log(num)
           return i;
        }(i)
        // result[i]表面上是被一个函数所赋值,但这是一个自执行函数,自执行函数又
        // 返回了它的实参。所以如果我们调用fn[0]()就会提示fn[0]不是一个function.
        // 你修改成result[i] = (function(num){...}(i))也是一个自执行,只不过
        // 多了一层闭包。
     }
     console.log("result", result)
     return result;//返回一个函数对象数组
     //这个时候会初始化result.length个关于内部函数的作用域链
  }
  var fn = outer();
  fn[0]
  fn[1]
参考文献

csdn博文
一篇cn博客--2012年对scope解释比较透彻

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/107163.html

相关文章

  • 闭包函数

    摘要:当闭包函数外部包含了一个匿名函数指向全局匿名函数执行具有全局性指向原理每个函数在被调用时都会自动取得两个特殊变量和。过度使用闭包可能会导致内存占用过多闭包只能取得包含函数中任何变量的最后一个值所以要注意写法 面试的时候一直会被问到什么是闭包,以前也不是很在意,更没有去总结和归纳. 闭包就是能够读取其他函数内部变量的函数。所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。 ...

    pcChao 评论0 收藏0
  • 还是不明白JavaScript - 执行环境作用作用链、闭包吗?

    摘要:所以,全局执行环境的变量对象始终都是作用域链中的最后一个对象。讲到这里,可能你已经对执行环境执行环境对象变量对象作用域作用域链的理解已经他们之间的关系有了一个较清晰的认识。 JavaScript中的执行环境、作用域、作用域链、闭包一直是一个非常有意思的话题,很多博主和大神都分享过相关的文章。这些知识点不仅比较抽象,不易理解,更重要的是与这些知识点相关的问题在面试中高频出现。之前我也看过...

    jlanglang 评论0 收藏0
  • JS基础知识:变量对象、作用闭包

    摘要:前言这段时间一直在消化作用域链和闭包的相关知识。而作用域链则是这套规则这套规则的具体运行。是变量对象的缩写那这样放有什么好处呢我们知道作用域链保证了当前执行环境对符合访问权限的变量和函数的有序访问。 前言:这段时间一直在消化作用域链和闭包的相关知识。之前看《JS高程》和一些技术博客,对于这些概念的论述多多少少不太清楚或者不太完整,包括一些大神的技术文章。这也给我的学习上造成了一些困惑,...

    Keven 评论0 收藏0
  • JavaScript闭包(三)

    摘要:目录执行环境与作用域链立即执行函数闭包知识点什么是闭包使用闭包的意义与注意点闭包的具体应用小结这是基本语法的函数部分的第篇文章,主要讲述了中比较重要的知识点闭包在讲闭包之前,在上一篇函数二的基础上,进一步深化执行环境和作用域链的知识点,并补 目录 1.执行环境与作用域链 2. 立即执行函数 3. 闭包知识点 3.1 什么是闭包 3.2 使用闭包的意义与注意点 3.3 闭包的具体应用 4...

    Anonymous1 评论0 收藏0
  • 由两道题扩展的对作用,作用链,闭包,立即执行函数,匿名函数的认识总结

    摘要:前言最近在学前几天看到两道题刚开始看懵懵懂懂这几天通过各种查资料慢慢的理解顿悟了对匿名函数闭包立即执行函数的理解也更深了一点在此分享给大家我的理解与总结希望能帮助大家理解因为这篇文章是我用心总结的查阅了很多的资料所以总结的比较细篇幅较长如果 前言 最近在学JS,前几天看到两道题,刚开始看懵懵懂懂,这几天通过各种查资料,慢慢的理解,顿悟了,对匿名函数,闭包,立即执行函数的理解也更深了一点...

    piapia 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<