【机制】js的闭包、执行上下文、做用域链

1.从闭包提及

什么是闭包html

一个函数和对其周围状态(词法环境)的引用捆绑在一块儿,这样的组合就是闭包。
也就是说,闭包让你能够在一个内层函数中访问到其外层函数的做用域。
在 JavaScript 中,每当建立一个函数,闭包就会在函数建立的同时被建立出来。git

上面是MDN对闭包的解释,这几句话可能不太好懂,不要紧,咱们先来看下能懂的:github

  • 闭包是和函数有关
  • 这个函数能够访问它外层函数的做用域
  • 从定义看,每一个函数均可以称为闭包

虽然从定义来看,全部函数均可以称为闭包,可是当咱们在讨论它的时候,通常是指这种状况:浏览器

//code-01
    function cat() {
      var name = "小猫";
      function say() {
        console.log(`my name is ${name}`);
      }
      return say;
    }
    var fun = cat();
    //---cat函数已经执行完,下面却还可以访问到 say函数的内部变量 name
    fun();
    //> my name is 小猫

当一个函数的返回值是一个内部函数时(cat函数返回say函数),在这个函数已经执行完毕后,这个返回的内部函数还能够访问到已经执行完毕的函数的内部变量,就像 code-01中fun能够访问到cat函数的name,通常咱们谈论的闭包就是指这种状况。
那么这是什么缘由呢?这就涉及到函数的做用域链执行上下文的概念了,咱们下面分别来讲。闭包

2.执行上下文

定义
什么是执行上下(Execution context )呢?简单来讲就是全局代码或函数代码执行的时候的环境,它包含三个部份内容:函数

  • 1.变量对象(Variable object,vo),
  • 2.做用域链(Scope chain,sc)
  • 3.this的指向(这篇先不谈)

咱们用一个对象来表示:oop

EC = {
      vo:{},
      sc:[],
      this
  }

而后代码或函数须要什么变量的时候,就会在这里面找。this

建立时间
执行上下文(EC)是何时建立的呢?这里分为两种状况:code

  • 全局代码:代码开始执行,可是尚未执行具体代码以前
  • 函数代码:函数要执行的时候,可是还没值执行具体代码以前

其实若是把全局的代码理解为一个大的函数,这二者就能够统一了。
每个函数都会建立本身的执行上下文,他们以栈的形式存储在一块儿,当函数执行完毕,则把它本身的执行上下文出栈,这就叫执行上下文栈(Execution context stack,ECS)
下面咱们经过一段代码实例来看一下htm

声明语句与变量提高
具体分析以前,咱们先来讲声明语句,什么是声明语句呢?

  • 声明语句是用来声明一个变量,函数,类的语句
  • 好比:var,let,const,function,class
  • 其中 var 和 function 会形成变量提高,其余不会,若是var和function同名的话,则函数声明优先
    那什么是变量提高呢?
// code-02
    console.log(varVal); // 输出undefined
    console.log(fun); // 输出  fun(){console.log('我是函数体') },
    //console.log(letVal) //报错 letVal is not defined

    var varVal = "var 声明的变量";
    let letVal = "let 声明的变量";

    function fun() {
      console.log("我是函数体");
    }
    var fun = "function"; //与函数同名,函数优先,可是能够从新赋值

    console.log(varVal); // >> "var 声明的变量"
    console.log(letVal); // >> "let 声明的变量"
    //fun(); // 报错,由于fun被赋值为'function'字符串了

    var name = "xiaoming";

在js执行代码的时候,会先扫一遍代码,把var,function的声明先执行,var声明的变量会先赋值为undefined,function声明的函数会直接就是函数体,这就叫变量提高,而其余的声明,好比let,则不会。
因此在变量赋值以前,console.log(varVal),console.log(fun)能够执行,而console.log(letVal)则会报错。
其中fun被从新声明为'function'字符串,可是在变量提高的时候,函数优先,因此console.log(fun)打印出来的是函数体,而代码执行赋值语句的时候,fun被赋值成了字符串,因此fun()会报错

代码执行过程分析--变量对象
咱们先上一段简单的代码,经过这段代码,来分析一下 执行上下文建立和做用的过程,对其内容咱们先只涉及变量对象vo:

//code-03
var name = 'xiaoming'

function user(name){
   var age = 27
   console.log(`我叫${name},今年${age}`)
}
user(name)
console.log(name)

咱们如今来分析一下这段代码执行过程当中,执行上下文的做用过程,会加入变量对象vo,做用域链scope会在下面讲,this的指向此次不讲,因此就不加上去了

1.代码执行以前,先建立 全局的执行上下文G_EC,并压入执行上下栈ECS

ECS = [
  G_EC : {
    vo:{
      name:undefined,
      user(name){ 
        var age = 27
        console.log(`我叫${name},今年${age}`)
      },
    },
    sc
  }
]

2.代码开始执行,name被赋值,执行user(name)
3.函数执行的时候,具体代码还没执行以前,建立函数执行上下文user_EC,并压入ECS

ECS = [
  user_EC : {
    vo:{
      name:undefined,
      age:undefined,
    },
    sc
  },
  G_EC : {
    vo:{
      name:'xiaoming',
      user(name){ 
        var age = 27
        console.log(`我叫${name},今年${age}`)
      }
    },
    sc
  }
]

4.开始执行函数代码,给形参name赋值,变量age赋值,执行console.log的时候须要变量nameage,因而从它本身的执行上下文user_EC中的变量对象vo里开始查找

ECS = [
  user_EC : {
    vo:{
      name:'xiaoming',
      age:27,
    },
    sc
  },
  G_EC : {
    vo:{
      name:'xiaoming',
      user(name){ 
        var age = 27
        console.log(`我叫${name},今年${age}`)
      }
    },
    sc
  }
]

5.发现找到了,因而打印 我叫xiaoming,今年27,至此函数user执行完毕了,因而把其对应的执行上下文user_EC出栈

ECS = [
  G_EC : {
    vo:{
      name:'xiaoming',
      user(name){ 
        var age = 27
        console.log(`我叫${name},今年${age}`)
      }
    },
    sc
  }
]

6.代码继续执行,console.log(name),发现须要变量那么,因而从它本身的执行上下文中的变量对象开始查找,也就是G_EC中的vo,顺利找到,因而打印"xiaoming"
7.至此代码执行结束,但全局的执行上下文好像要等到当前页面关闭才出栈(浏览器环境)

3.做用域链

上面咱们分析代码执行过程的时候,有说到若是要用到变量的时候,就从当前执行上下文中的变量对象vo里查找,咱们恰好是都有找到。
那么若是当前执行上下文中的变量对象中没有须要用的变量呢?根据咱们的经验,它会从父级的做用域来查找,那么这是根据什么来查找的呢?
全部接下来咱们继续来看 '做用域链'(scope chain,sc),它也是执行上下文得另外一个组成部分。
** 函数做用域 **
在说执行上下中的做用域链以前,咱们要先来看看函数做用域,那么这是个什么东西呢?

  • 每个函数都有一个内部属性【scope】
  • 它是函数建立的时候构建的
  • 它是一个列表,会把函数的全部父辈的执行上下中的变量对象存在其中
    举个例子:
//code-04
function fun_1(){
  function fun_2(){}
}

1.咱们看上面的代码,当fun_1函数建立的时候,它的父级执行上下文是全局执行上下文 G_EC,因此fun_1的函数做用域【scope】为:

fun_1.scope = [
  G_EC.vo
]

2.当fun_2函数建立的时候,它的全部父级执行上下文有两个,一个是全局执行上下文 G_EC, 还有一个是函数fun_1的执行上下文 fun_1_EC, 因此fun_2的函数做用域【scope】为:

fun_1.scope = [
  fun_1_EC.vo,
  G_EC.vo
]

执行上下文的做用域链
上面咱们说的是函数做用域,它包含了全部父级执行上下的变量对象,可是咱们发现它没有包含函数本身的变量对象,由于这个时候函数只是声明了,尚未执行,而函数的执行上下文是在函数执行的时候建立的。
当函数执行的时候,会建立函数的执行上下文,从上面咱们知道,这个时候会建立执行上下文变量对象vo,而赋值执行上下文做用域链sc的时候,会把vo加在scope前面,做为一个队列,赋值给做用域链
就是说:EC.sc = [EC.vo,...fun.scope],咱们下面举例说明,这段代码与code-03的区别只是不给函数传参,因此会用到父级做用域的变量。

//code-05
var name = 'xiaoming'

function user(){
   var age = 27
   console.log(`我叫${name},今年${age}`)
}
user()
console.log(name)

1.代码执行以前,先建立 全局的执行上下文G_EC,并压入执行上下栈ECS,同时赋值变量对象vo、做用域链sc,注意:当函数user被声明的时候,会带有函数做用域user.scope

ECS = [
  G_EC : {
    vo:{
      name:undefined,
      user // user.scope:[G_EC.vo]
    },
    sc:[G_EC.vo]
  }
]

2.代码开始执行,name被赋值,执行user()
3.函数执行的时候,具体代码还没执行以前,建立函数执行上下文user_EC,并压入ECS,同时赋值变量对象vo和做用域链sc:

ECS = [
  user_EC : {
    vo:{
      age:undefined,
    },
    sc:[user_EC.vo, ...user.scope]
  },
  G_EC : {
    vo:{
      name:'xiaoming',
      user // user.scope:[G_EC.vo]
    },
    sc:[G_EC.vo]
  }
]

4.开始执行函数代码,给变量age赋值,执行console.log的时候须要变量nameage,这里咱们上面说是从变量对象里找,这里更正一下,实际上是从做用域链中查找

ECS = [
  user_EC : {
    vo:{
      age:27,
    },
    sc:[user_EC.vo, ...user.scope]
  },
  G_EC : {
    vo:{
      name:'xiaoming',
      user, // user.scope:[G_EC.vo]
    },
    sc:[G_EC.vo]
  }
]

5.咱们发如今做用域链的第一个对象中(user_EC.vo)找到了age,可是没有name,因而开始查找做用域链的第二个对象,依次往下找,若是都没找到,则会报错。
这里的话,咱们发现做用域链的第二个元素user.scope析构出来的,也就是G_EC.vo,这个里面有找到name='xiaoming'
因而打印 我叫xiaoming,今年27,至此函数user执行完毕了,因而把其对应的执行上下文user_EC出栈

ECS = [
  G_EC : {
    vo:{
      name:'xiaoming',
      user, // user.scope:[G_EC.vo]
    },
    sc:[G_EC.vo]
  }
]

6.代码继续执行,console.log(name),发现须要变量那么,因而从它本身的执行上下文中的做用域链开始查找,在第一个元素G_EC.vo就顺利找到,因而打印"xiaoming"
7.至此代码执行结束,

4.回归到闭包的问题

到此为止咱们介绍完了执行上下文,那么如今咱们回归到刚开始的闭包为何能访问到已经执行完毕了的函数的内部变量问题。咱们再来回顾一下代码:

//code-06
    function cat() {
      var name = "小猫";
      function say() {
        console.log(`my name is ${name}`);
      }
      return say;
    }
    var fun = cat();
    fun();

咱们来照上面的步骤来分析下代码:
1.代码执行以前,先建立 全局的执行上下文G_EC,并压入执行上下栈ECS,同时赋值变量对象vo、做用域链sc

ECS = [
  G_EC : {
    vo:{
      fun:undefined,
      cat, // cat.scope:[G_EC.vo]
    },
    sc:[G_EC.vo]
  }
]

2.代码开始执行,执行cat()函数
3.函数执行的时候,具体代码还没执行以前,建立函数执行上下文cat_EC,并压入ECS,同时赋值变量对象vo和做用域链sc:

ECS = [
  cat_EC : {
    vo:{
      name:undefined,
      say, // say.scope:[cat_EC.vo,G_EC.vo]
    },
    sc:[cat_EC.vo, ...cat.scope]
  },
  G_EC : {
    vo:{
      fun:undefined,
      cat, // cat.scope:[G_EC.vo]
    },
    sc:[G_EC.vo]
  }
]

4.开始执行函数代码,给变量name赋值,而后返回say函数,这个时候函数执行完毕,它的值被付给变量fun,它的执行上下文出栈

ECS = [
  G_EC : {
    vo:{
      fun:say, // say.scope:[cat_EC.vo,G_EC.vo]
      cat // cat.scope:[G_EC.vo]
    },
    sc:[G_EC.vo]
  }
]

5.代码继续执行,到了fun(),
6.当函数要执行,还没执行具体代码以前,建立函数执行上下文fun_EC,并压入ECS,同时赋值变量对象vo和做用域链sc:

ECS = [
  fun_EC : {
    vo:{},
    sc:[fun_EC.vo, ...fun.scope]//fun==cat,因此fun.scope = say.scope = [cat_EC.vo,G_EC.vo]
  },
  G_EC : {
    vo:{
      fun:say, // say.scope:[cat_EC.vo,G_EC.vo]
      cat // cat.scope:[G_EC.vo]
    },
    sc:[G_EC.vo]
  }
]

7.函数fun开始执行具体代码:console.log(my name is ${name}),发现须要变量name,因而从他的fun_EC.sc中开始查找,第一个fun_EC.vo没有,因而找第二个cat_EC.vo,发现这里有name="小猫",
因而打印 my name is 小猫,至此函数fun执行完毕了,因而把其对应的执行上下文fun_EC出栈

ECS = [
  G_EC : {
    vo:{
      fun:say, // say.scope:[cat_EC.vo,G_EC.vo]
      cat // cat.scope:[G_EC.vo]
    },
    sc:[G_EC.vo]
  }
]

8.至此代码执行结束

到这里咱们知道闭包为何能够访问到已经执行完毕的函数的内部变量,是由于在的执行上下文中的做用域链中保存了变量的引用,而保存的引用的变量不会被垃圾回收机制所销毁。

闭包的优缺点
优势:

  1. 能够建立拥有私有变量的函数,使函数具备封装性
  2. 避免全局变量污染

缺点:

  1. 增大内存消耗

参考
1.JavaScript深刻之词法做用域和动态做用域
2.JavaScript深刻之执行上下文栈
3.setTimeout和setImmediate到底谁先执行,本文让你完全理解Event Loop

相关文章
相关标签/搜索