面试官:怎样理解模块模式的一个核心机制--“闭包”

300 阅读7分钟

一、 一个问题与一个闭包

question:

给你这样的一段代码,你可以尝试着感觉会输出什么结果?

var arr = []
for(var i = 0; i < 10; i++){
    arr[i] = function(){
        console.log(i)
    }
}
arr.forEach(function(item){
    item()
})

很显然,我们编写这段代码的预期是,输出0到9,但事实会是如此吗?咱们一起来看输出结果

image.png

竟然输出了十个10,不可思议,相信有一些小伙伴们感觉出来的结果和这样,说明你对编程有很好的码感,可以感觉到这里的不对劲。当然,没有得到正确答案的小伙伴也不要着急,跟我一起解开闭包的奥秘。

闭包:

接下来,我将展现一段形成闭包的代码,它其实并不复杂。

function makeCounter() {
    let count = 0;
    function foo() {
        count++;
        return count;
    };
    return foo
}
const counter = makeCounter();

console.log(counter()); 
console.log(counter()); 
console.log(counter()); 

const anotherCounter = makeCounter();
console.log(anotherCounter());

我们定义了一个makeCounter()函数,返回一个在函数内部定义的函数foo(),这个函数记住了它外部的count变量,每次调用counter()时,count都会递增。

二、闭包

1、定义:

闭包(Closure)是一个在计算机科学,特别是在函数式编程和一些支持匿名函数的编程语言中非常重要的概念。

它描述的是一个函数能够访问并操作其自身作用域以外的变量的能力,这些变量来自于包含它的外部函数或者全局作用域

2、核心特性:

(1)作用域链:当在函数体内定义另一个函数时,内部函数往往都可以访问外部函数的变量,这是因为在内部函数形成了一个对其外部作用域的引用,这些引用称为作用域链。更形象的说,变量环境中有一个内定的outer属性用于指明该函数的外层作用域是谁,outer的指向是根据词法作用域(函数声明的位置)来定的。如图:

image.png (2)变量存活期延迟:正常情况下,当一个函数执行完毕,其内部的局部变量会被销毁。但是,如果这些变量被内部函数(即闭包)所引用,那么它们会继续存在,直到没有任何引用指向它们为止。

(3)主要用途:

  1. 实现变量私有化:Javascript没有类的概念,闭包提供了一个实现封装的方法,让某些变量成为只有特定函数才可以访问的“私有”成员。
  2. 状态保留:闭包可以用来保存函数的局部状态,让每次调用时,都可以基于之前的状态进行操作,这对于创建迭代器或生成等模式非常有用。
  3. 函数工厂:通过闭包,可以创建出一系列行为相似但状态各异的函数,每个函数都绑定到其创建时的特定环境。

(4)注意事项:

  1. 内存管理:由于闭包可以延长变量的生命周期,如果不恰当使用,可能会导致内存泄漏,尤其是在循环或大量创建闭包的场景下。
  2. 性能:过多或不当的闭包使用可能会影响程序的性能,尤其是当闭包持有大量数据或造成作用域链过长时。

个人理解: 根据Javascript中词法作用域的规则,内部的函数总是可以访问外部函数中的变量,当我们通过调用一个外部函数返回一个内部函数后,即使外部函数已经执行结束了,但是内部函数引用了外部函数的变量,也仍然需要被保存在内存中,我们把这些变量的集合称为闭包。

三、解决问题

让我们回到文章开始提出的问题,为什么会输出十个10呢? image.png 首先我们定义了一个arr数组,然后让数组中每一个元素都赋值为function(){},这个函数中能输出i,然后arr.forEach()遍历数组。

i=10时for循环结束,因为let + {}才会形成块级作用域,所有此时的var i是声明在全局的,当for循环结束,i = 10,遍历数组时,每个元素中的console.log(i),就会去全局中找 i,此时i = 10,输出十个10。

如何解决呢?这就要提到闭包的用途——状态保留。

var arr = []
for (var i = 0;i < 10; i++){
(function(j){  //外部函数
    arr[j] = function(){//内部函数
    console.log(j)
    } 
})(i)
}
arr.forEach(function(item){
    item()
})
image.png 我们让i作为实参,传入在for循环中定义的function函数中,在函数中让arr的每一个元素都赋值为function函数,当每进行一次循环,编译器能够检测到内部函数在外部函数外调用,所以,即使外部函数执行完毕,它的执行上下文都不会被完全清理,而是会留下一个“包”,用来存放内部函数所需要的数据,每进行一个for循环,就会形成一个新的闭包,这样就能记录下每次循环的i,有的人也形象的把闭包称为“背包”。下面来看一下简单的图示:

image.png 到这个里,我相信大家对闭包的了解已经很深刻,如果“作用域”、“词法作用域”、“执行上下文”等不理解的概念,可以移步我的上一篇文章,相信你可以更进一步的理解闭包。Javascript中的“域”、“预”、“译”,你真的掌握了吗? - 掘金 (juejin.cn)

4、闭包与模块模式的联系

闭包和模块模式在JavaScript中有着密切的联系,它们都是基于作用域和私有性的核心概念来实现特定的功能和设计模式。

模块模式:

是一种常用的JavaScript编程模式,它利用了闭包的特性来创建独立的、封装良好的对象或模块。模块模式的主要目的是为了提高代码的可复用性、可维护性,并实现私有变量和方法。一个典型的模块模式实现会有一个立即执行的匿名函数(IIFE),在这个函数内部定义私有变量和函数,然后返回一个对象,该对象暴露需要公开的属性和方法。通过这种方式,模块内部的状态对外部世界是隐藏的,只暴露必要的接口。

联系:

  1. 私有性:两者都利用了JavaScript的词法作用域来实现变量和函数的私有化。在模块模式中,通过闭包来确保模块内的私有成员不被外界直接访问,只暴露公共接口。
  2. 封装:模块模式是闭包概念的一个扩展应用,它利用闭包来封装实现细节,只暴露一个清晰的API给使用者,这是模块化编程的基础。
  3. 数据隐藏和共享:闭包帮助维持函数内部状态,而模块模式则利用这一特性来隐藏模块内部的复杂性,并可以通过暴露的方法来控制对内部状态的访问和修改,实现更细粒度的权限控制。
  4. 总的来说,闭包是实现模块模式的技术基础,而模块模式是闭包在实际开发中的一种高级应用,旨在增强代码的组织结构和可维护性。

总结

在《你不知道的Javascript 上卷》中,它是这样描述闭包的:非常重要但又难以掌握,近乎神话的概念--闭包。其实,只要我们一步一步将这些概念之间的逻辑和联系理清,相信有更加难懂的概念,我们可以做到能够把它理解,甚至掌握。