loading...
Javascript执行上下文与闭包
Published in:2023-06-22 | category: Typescript
Words: 1.7k | Reading time: 6min | reading:

Javascript执行上下文与闭包

执行上下文

说闭包之前先解释一下JavaScript的执行上下文,即作用域。

  1. 全局作用域:默认环境,当我们第一次启动程序时,默认创建的全局作用域。
  2. 函数作用域:当执行流进入函数体时就会创建一个本地函数执行上下文,并压入栈中。
  3. 块级作用域:这是es6中新出的作用域,比如for循环、while循环、{}等都属于是块级作用域。

闭包

知道了作用域之后,我们来看一下闭包的定义、作用以及使用场景:

闭包定义:指的是那些引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现的。———-JS红宝书上面的解释

个人理解:该作用域存在对上级作用域当中变量的引用即形成闭包。上级作用域内的变量,因为被下级作用域内引用,而没有被释放。就导致上级作用域内的变量,等到下级作用域执行完以后才正常得到释放(也就是说当解除了对该闭包的所有引用的时候,这个闭包才会被JS的垃圾回收机制回收)。

下面我先给出一个看似闭包但不是闭包的例子,以助后面更好的认识以及理解闭包:

function outerFunction() :void {
    let outerVariable: number = 1;
    function innerFunction(): void {
      let innerVariable: number = 20;
      console.log(outerVariable + innerVariable);
    }
    innerFunction();
  }
  outerFunction();
  outerFunction();
  outerFunction();

在这个例子中,我们定义了一个外部函数 outerFunction,其中包含一个内部函数 innerFunction。在 outerFunction 中,我们直接调用了 innerFunction,而不是将其作为返回值或赋值给变量。
尽管 innerFunction 访问了 outerFunction 中的变量 outerVariable,但由于 innerFunction 在 outerFunction 内部被直接调用,而不是在外部作用域外被调用,它不满足闭包的条件。
在这个例子中,innerFunction 没有形成闭包,因为它没有在外部函数作用域外被调用,也没有捕获外部函数作用域的引用。
看一下运行效果:
运行结果(伪闭包)

可以看到,并没有实现闭包的效果,原因就是三次调用outerFunction函数,之间没有任何关系,每次执行完之后都会将该函数内部的变量回收。
下面我们改造一下这个函数,让其变成闭包:

1 function outerFunction() :Function {
2    let outerVariable :number = 1;
3    function innerFunction() :number{
4      let innerVariable :number = 20;
5       outerVariable ++
6      return (outerVariable + innerVariable);
7    }
8    return innerFunction;
9 }
10 const closure :Function = outerFunction();
11 console.log(closure())
12 console.log(closure())
13 console.log(closure())

先看一下执行的结果:
修改为闭包之后的执行结果

修改之处在于,我们将 innerFunction 返回并赋值给 closure 变量,并在外部调用 closure()。
现在,closure 变量中保存的是 innerFunction,它实际上是一个闭包。在调用 closure() 时,innerFunction 可以访问其定义时所在的外部函数 outerFunction 的作用域,即可以访问 outerVariable。这样,我们就创建了一个闭包,使得 innerFunction 在外部函数作用域外仍然能够访问和操作其引用的变量。
通过这种方式,我们确保了 innerFunction 在外部函数作用域外被调用时仍然可以访问外部变量,满足了闭包的条件。

剖析执行过程

  • 第 1-9行。我们在全局执行上下文中创建了一个新的变量outerFunction,并赋值了一个的函数定义。
  • 第10行。我们在全局执行上下文中声明了一个名为closure的新变量。
  • 第11行。我们需要调用outerVariable 函数并将其返回值赋给closure变量。
  • 第 1-9行。调用函数,创建新的本地执行上下文。
  • 第2行。在本地执行上下文中,声明一个名为outerVariable 的新变量并赋值为 1;
  • 第 3-7行。声明一个名为myFunction的新变量,变量在本地执行上下文中声明,变量的内容是为第4-6行所定义。
  • 第8行。返回innerFunction变量的内容,删除本地执行上下文。变量innerFunction和outerVariable 不再存在。
  • 第10行。在调用上下文(全局执行上下文)中,outerFunction返回的值赋给了closure,变量closure现在包含一个函数定义内容为outerFunction返回的函数。它不再标记为innerFunction,但它的定义是相同的。这个closure就相当于是闭包的引用,如果该变量一直引用着,则这个闭包会一直存在于内存当中,不会被垃圾回收机制标记为垃圾。
  • 第11行。查找closure变量,它是一个函数并调用它。它包含前面返回的函数定义,除此之外它还有一个带有变量的闭包
  • 创建一个新的执行上下文。没有参数,开始执行函数。
  • 第四行,声明了一个innerVariable变量并赋值20
  • 第五行,寻找变量outerVariable,在查找当前作用域和上级作用域之前,会先检查一下闭包当中有没有,找到了,然后执行+1操作,然后将其再次存储到闭包当中,待下次使用。
  • 第六行,返回outerVariable + innerVariable的值,然后销毁当前上下文。
  • 回到第11行,打印出22.
  • 接下来就是第12行和第13行就是重复步骤执行第11行代码的执行步骤,实现递增的效果。分别打印出23,24.

闭包的作用:保护函数的私有变量不受外界干扰;将上级作用域的引用保存下来,实现方法或者属性的私有化;实现模块化

闭包的缺点:使用不当,会造成内存泄漏。

下面是一个使用闭包不当造成内存泄漏的例子:

function outerFunction() {
  let outerVariable = '1';
  function innerFunction() {
    let innerVariable = '1';
    setInterval(function() {
    outerVariable++
    innerVariable++
      console.log(outerVariable + innerVariable);
    }, 1000);
  }
  innerFunction();
}
outerFunction();

在这个例子中,我们定义了一个外部函数 outerFunction,其中包含一个内部函数 innerFunction。在 innerFunction 中,我们使用 setInterval 创建了一个定时器,每秒钟输出 outerVariable 和 innerVariable 的值。
由于定时器的回调函数形成了闭包,它可以访问 outerVariable 和 innerVariable,即使 innerFunction 已经执行完毕。这导致闭包中引用的变量无法被垃圾回收,因为定时器仍然持有对闭包的引用。
如果我们多次调用 outerFunction,每次调用都会创建一个新的定时器,并且每个定时器都会保留对其自己的闭包的引用。这会导致内存泄漏,因为闭包中引用的变量无法被释放。
为了避免内存泄漏,要记得清除定时器。
执行结果

闭包的使用场景:节抖或防流;自执行函数以及回调函数等

Prev:
块级格式化上下文BFC(Block Formatting Context)
Next:
数据结构八大排序之快速排序算法