JS回归基础之闭包

问题:

  1. 什么是闭包?
  2. 闭包什么时候创建?
  3. 闭包什么时候销毁?
  4. 闭包的作用

闭包

闭包是一个可以访问外部作用域的内部函数,即使这个外部作用域已经执行结束。

作用域 (Lexical scope)

JavaScript 采用词法作用域(lexical scoping),也就是静态作用域。函数的作用域在函数定义的时候就决定了。var 创建的变量只有函数作用域,let 和 const 创建的变量既有函数作用域,也有块作用域
在JavaScript中,作用域对象是在中被创建的(至少表现出来的行为是这样的),所以在函数返回后它们也还是能够被访问到而不被销毁

作用域链(Scope Chain)

当JavaScript在运行的时候,它需要一些空间让它来存储本地变量(local variables)。我们将这些空间称为作用域对象(Scope object),有时候也称作词法环境LexicalEnvironment
当访问一个变量的时候会先在当前的作用域对象中查找这个属性,如果不存在就在父作用域中寻找,这个查找的过程称为作用域链;
在作用域链中查找变量的过程和原型继承(prototypal inheritance)有着非常相似之处。但是在原型链中找不到一个属性,返回undefined,在作用域中找不到会直接报错ReferenceError

返回函数的函数

 1: function createCounter() {
    
    
 2:   let counter = 0
 3:   const myFunction = function() {
    
    
 4:     counter = counter + 1
 5:     return counter
 6:   }
 7:   return myFunction
 8: }
 9: const increment = createCounter()
10: const c1 = increment()
11: const c2 = increment()
12: const c3 = increment()
13: console.log('example increment', c1, c2, c3) 

执行结果

'example increment'123

执行顺序

  • 行1 - 8。我们在全局执行上下文中创建了一个新的变量createCounter,它得到了指定的函数定义。
  • 第9行。我们在全局执行上下文中声明了一个名为increment的新变量。
  • 第9行。我们需要调用createCounter函数并将其返回值赋给increment变量。
  • 行1 - 8。调用函数,创建新的本地执行上下文。
  • 第2行。在本地执行上下文中,声明一个名为counter的新变量并赋值为 0 。
  • 行3 - 6。声明一个名为myFunction的新变量,变量在本地执行上下文中声明,变量的内容是另一个函数定义。如第4行和第5行所定义,现在我们还创建了一个闭包,并将其作为函数定义的一部分。闭包包含作用域中的变量,在本例中是变量counter(值为0)。
  • 第7行。返回myFunction变量的内容,删除本地执行上下文。myFunctioncounter不再存在。控制权交给了调用上下文,我们返回函数定义和它的闭包,闭包中包含了创建它时在作用域内的变量。
  • 第9行。在调用上下文(全局执行上下文)中,createCounter返回的值被指定为increment,变量increment现在包含一个函数定义(和闭包),由createCounter返回的函数定义,它不再标记为myFunction,但它的定义是相同的,在全局上下文中,称为increment
  • 第10行。声明一个新变量(c1)。
  • 继续第10行。查找变量increment,它是一个函数,调用它。它包含前面返回的函数定义,如第4-5行所定义的。(它还有一个带有变量的闭包)。
  • 创建一个新的执行上下文,没有参数,开始执行函数。
  • 第4行。counter = counter + 1,寻找变量 counter,在查找本地或全局执行上下文之前,让我们检查一下闭包,瞧,闭包包含一个名为counter的变量,其值为0。在第4行表达式之后,它的值被设置为1。它再次被储存在闭包里,闭包现在包含值为1的变量 counter
  • 第5行。我们返回counter的值,销毁本地执行上下文。
  • 回到第10行。返回值(1)被赋给变量c1。
  • 第11行。我们重复步骤10-14。这一次,在闭包中此时变量counter的值是1。它在第12步设置的,它的值被递增并以2的形式存储在递增函数的闭包中,c2被赋值为2。
  • 第12行。重复步骤10-14,c3被赋值为3。
  • 第13行。我们打印变量c1 c2和c3的值。

在全局作用域中创建的函数创建闭包,但是由于这些函数是在全局作用域中创建的,所以它们可以访问全局作用域中的所有变量,闭包的概念并不重要。

当函数返回函数时,闭包的概念就变得更加重要了。返回的函数可以访问不属于全局作用域的变量,但它们仅存在于其闭包中。

垃圾回收

在 Javascript 中,局部变量会随着函数的执行完毕而被销毁,除非还有指向他们的引用。当闭包本身也被垃圾回收之后,这些闭包中的私有状态随后也会被垃圾回收。

  1. 标记清除(mark and sweep)
    常用的方式,大部分浏览器以此方式进行垃圾回收,垃圾收集器给内存中的所有变量都加上标记,然后去掉环境中的变量以及被环境中的变量引用的变量的标记。在此之后再被加上的标记的变量即为需要回收的变量,因为环境中的变量已经无法访问到这些变量。
  2. 引用计数(reference counting)
    容易引起内存泄漏,低版本的IE使用这种方式。

面试题

问题1

for( var i = 0; i < 5; i++ ) {
    
    
    setTimeout(() => {
    
    
        console.log( i );
    }, 1000)
}

问题2

function fun(n, o) {
    
    
    console.log(o);
    return {
    
    
        fun: function(m) {
    
    
            return fun(m, n);
        }
    };
}

var a = fun(0); // ?
a.fun(1); // ?
a.fun(2); // ?
a.fun(3); // ?
var b = fun(0).fun(1).fun(2).fun(3); // ?
var c = fun(0).fun(1); // ?
c.fun(2); // ?
c.fun(3); // ?

问题3

 function createCounter() {
    
    
   let counter = 0
   const myFunction = function() {
    
    
     counter = counter + 1
     return counter
   }
   return myFunction
 }
 const increment = createCounter()
 const c1 = increment()
 const c2 = increment()
 const c3 = increment()
 console.log('example increment', c1, c2, c3) 

总结

  1. 什么是闭包?
    闭包就是同时含有对函数对象以及作用域对象引用的最想。实际上,所有JavaScript对象都是闭包。
  2. 闭包什么时候创建?
    因为所有JavaScript对象都是闭包,因此,当你定义一个函数的时候,你就定义了一个闭包。
  3. 闭包什么时候销毁?
    闭包是什么时候被销毁的?当它不被任何其他的对象引用的时候。
  4. 闭包的作用
    正常函数执行完毕后,里面声明的变量被垃圾回收处理掉,但是闭包可以让作用域里的 变量,在函数执行完之后依旧保持没有被垃圾回收处理掉

参考:
发现 JavaScript 中闭包的强大威力
JavaScript闭包的底层运行机制
我从来不理解JavaScript闭包,直到有人这样向我解释它…
破解前端面试(80% 应聘者不及格系列):从闭包说起

猜你喜欢

转载自blog.csdn.net/qq_42816550/article/details/111474618
今日推荐