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

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

作用域

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

作用域链

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

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

作用域、作用域链、执行环境和闭包知识点归纳_第1张图片

执行环境

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

  • 全局执行环境

我们通常可以在代码第一行使用类似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按照如下顺序初始化:

  1. 函数参数(如未传入,初始化该参数值为undefined)
  2. 函数声明(如发生命名冲突会覆盖)
  3. 变量声明(初始化变量值为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();

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

作用域、作用域链、执行环境和闭包知识点归纳_第3张图片

分析下:
当执行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();

作用域、作用域链、执行环境和闭包知识点归纳_第4张图片

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

作用域、作用域链、执行环境和闭包知识点归纳_第5张图片

如图,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解释比较透彻

你可能感兴趣的:(作用域链,执行环境,作用域,闭包,javascript)