我是这么理解:js 执行上下文与闭包的

50 阅读6分钟

1 什么是执行上下文?

  • 执行上下文(Execution Context):是用来描述代码运行时的执行环境。

  • 执行栈:是用来存储函数的调用一种数据结构,用于管理执行上下文。当一个程序开始运行或者一个函数开始调用之前,首先会被加入到执行栈中,执行栈遵循先进后出的原则。当前函数执行完成后,当前函数的执行上下文会出栈,并等待垃圾回收。

  • 执行上下文分全局执行上下文和函数执行上下文;每个执行上下文都有三个生命周期,创建-执行-销毁

2.执行上下文干了什么?

执行上下文在创建阶段:
  1. 创建变量对象,变量对象的属性由 变量(variable) 和 函数声明(function declaration) 构成。在函数上下文情况下,参数列表(parameter list)也会被加入到变量对象(variable object)中作为属性。在全局上下文中变量对象很多文章说是 window 对象,但 let 和 const 申明的变量又不在 window 对象中,所以我的理解是全局上下文的变量对象包含了 window 对象以及const 和 let 申明的变量,在 chrome 浏览器中对于全局申明的 let 和 const 变量会放在 script 中 ,我们可以通过 debugger 下面代码在 chrome 浏览器看到。
debugger;
function an1() {}
an1();
const a = 1;
let b = 2;
var c=3;

下图是运行上面代码看到的截图: 'chrome截图'

  1. 建立作用域链,全局上下文的作用域是顶级的作用域,是作用域链的终点。当一个函数被执行器调用后,它会到当前执行上下文作用域中的变量对象中查找该函数,如果在当前作用域中没有找到,它会沿着作用域链往上层作用域查找,直到找到该函数。并创建该函数的作用域与保存该函数的执行上下文的作用域相连接,形成作用域链(这里的作用域链有点类型原型链,在当前作用域中没有找到该变量或者函数,会往上层作用域中查找,直到找到。)。作用域链的访问是单向的,是由当前作用域沿着作用域链往上层作用域去查找变量。换句话说js 作用域链是由函数定义的位置决定的,在哪里定义就将哪里的作用域与函数的作用域链接行程作用域链,也就是所说的词法作用域(也叫静态作用域)

  2. 确定 this 的指向

执行阶段:就是逐行的运行代码,此时变量对象会转化成活动对象进行赋值操作。
销毁阶段:当一个函数或者程序(指全局上下文)执行完毕后,就会从执行栈弹出,等待垃圾回收后,整个执行上下文变被销毁。

3.什么是作用域和作用域链?

作用域(Scope)是指变量的可见性和生命周期,决定了变量的访问权限。

作用域分全局作用域,局部作用域(函数作用域),还有块作用域(let 和 const 申明的变量在{}外就访问不了) 我的理解是:作用域是一个抽象的概念,具体的是两个大括号之间所有内容,类似一个大容器,包含变量对象。里面定义的变量和函数只能在这个容器内访问。当访问一个变量时就会先在当前的作用域中查找,如果没有找到就会到上级的作用域查找,直到全局作用域。

作用域链(Scope Chain)作用域链是由多个作用域组成的链表,其中包含了当前执行上下文的作用域和其他上层作用域。

接下来我们通过一个示例说明一下作用域链

var a=1;
let b=2;
function fn1(){
    var c=2;
    function fn2(){ 
      console.log(a)
    }
    fn2()
}
fn1()

程序运行后进入全局执行上下文进入执行栈并创建全局作用域(scope1)和全局变量对象 (gvo)={a:undefined,fn1: ƒ fn1(),b= < value unavailable >,...windom}(注意:变量 b 是 let 申明)。然后进入执行阶段从第一行到第九行全局变量对象 gvo 由变量对象变成活动对象={a:1,fn1:f fn1(),b:2}.当程序执行到第九行时,在当前执行上下文(全局执行上下文)的作用域 scope1 的变量对象 gvo 中找到函数 fn1将 fn1压入执行栈,并进入fn1 的执行上下文,然后创建 fn1 的作用域scope2 并将 scope2 =>scope1形成作用域链。创建fn1 的变量对象 fn1vo={c:undefined,fn2:f fn2()},从第四行到第七行变量对象 fn1vo变成活动对象={c:2,fn2:f fn2()}。最后执行到第八行通过作用域链在 scope2中的变量对象fn1vo中找到fn2并将 fn2 压入执行栈,进入fn2 的执行上下文,然后创建fn2 的作用域 scoped3和变量对象 fn2vo={},此时的作用链 socpe3=>scoped2=>scope1,然后执行第六行 console.log(a)时,获取变量 a 先从 scope3 中查找没有找到,继续到 socpe2 中查找还是没有找到,继续到 scope1 中查找并找到 a=1,然后输出 1。fn2 执行上下文销毁,fn2从执行栈弹出,接着fn1 执行上下文销毁,fn1 从执行栈弹出,最后全局上下文销毁,程序执行完毕。

最后来个动图展示上面函数的执行过程

'chrome截图'

什么闭包?

闭包是指那些引用了另一个函数作用域变量的函数

举例说明

function fn1(){
    var a=1;
    function fn2(){
        console.log(a)
    }//fn2 函数就是一个闭包
    console.dir(fn2)
}
fn1()

上面代码中的 fn2 就是一个闭包,fn2 函数引用了fn1 函数作用域中的变量 a,所以 fn2 就是一个闭包函数。

我们再看一个例子

function fn1(){
  var a=1;
  return function (){
    a++
    console.log(a)
  }
}
let fn2=fn1()
fn2()//2
fn2()//3

上面代码 fn1 函数执行后返回了一个匿名函数并赋值给了 fn2,当fn2 第一次调用打印 2 fn2第二次调用打印 3。 正常当一个函数执行完毕后,函数会被销毁函数中的变量也会随着函数销售被垃圾回收机制回收。但上面的代码中的 fn1 函数执行完毕后,变量 a 并没有被垃圾回收机制回收,而是被 fn2 函数保存在自己作用域的闭包中。这就是说闭包会导致内存泄露的原因。