闭包是什么?从为什么会有闭包讲起!

3,330 阅读6分钟

闭包是什么?从为什么会有闭包讲起。

闭包的起源

维基百科:闭包的概念是在1960年代为采用lambda演算的表达式的机器求值而开发的,它首次在1970年于PAL编程语言中完全实现,用来支持词法作用域的头等函数。

关键词:

词法作用域:函数的作用域在函数定义(就是代码书写的位置)的时候就决定了。

头等函数函数是一等公民):函数可以作为函数的参数、函数的返回值,赋值给变量或存储在数据结构中。

举例

写一个JS函数,来说明

function parent() {
  let n = 1
  function son() {
    // 词法作用域: 可以访问到n
    console.log(n);
  }
  return son // 函数是一等公民: 可以被返回
}

let son = parent()
son()// 函数是一等公民: 返回的函数可以执行。
  • 函数是一等公民 决定了:函数 son 可以返回,且任意时刻执行。
  • 词法作用域 决定了:函数 son 可以访问到函数 parent 的 变量 n

现在不要想其它概念。

我们直观的认为:函数 son 执行,访问函数 parent 作用域的 n 执行 console.log

是对的吗?对的。

那闭包这个概念呢?

发现了吗?我们不知道闭包这个概念,但是完全不影响,我们认识代码的执行!

为什么有闭包这个概念?

先讲一个概念:函数调用栈(Function Call Stack)

简单讲一下函数在函数调用栈上的执行流程。

  • 函数在栈上运行,且会使用栈内存
  • 函数在栈内存上,保存局部变量,**基本类型数据、**函数执行信息等数据。
  • 函数执行完后,出栈。

出栈后,栈上保存的局部变量,基本类型数据,都无法访问了,数据不存在了。

举例说明

如果没有闭包,执行是什么情况。

// 在没有闭包的调用栈中执行
// 下面有图,看图理解
function parent() {
  let n = 1 // 局部变量n,基本数据类型1
  function son() { // 局部变量son

    // son执行时,parent已经出栈,变量n访问不到了
    // 报错!
    console.log(n);
  }
  return son  
  // parent出栈
}

let son = parent() // parent入栈
son()// son入栈,

Untitled Diagram.drawio (5).png 要让 son 可以在 parent 出栈后依然能访问到 n

需要个新技术来解决这个问题。

闭包诞生

为了解决上述问题( 词法作用域 + 函数是一等公民 + 函数调用栈 正常运行)。诞生了闭包这个概念。

闭包其实是一种技术:在函数调用栈的基础上,同时实现函数是一等公民、词法作用域。

闭包的具体实现

核心原理:将闭包所需的数据,都存储到堆(Heap)上。

堆(Heap)的数据,是不会随着函数调用结束而回收的。

Untitled Diagram.drawio (4).png

上面的举例函数,在 chrome 中的闭包信息:

Untitled.png

v8将函数闭包所需的数据,构成一个Closure对象放在堆上,然后函数引用这个对象。

再次调用时,函数会去访问Closure对象里的数据。

(深入原理请看一文颠覆大众对闭包的认知

闭包的特性:保存、保护变量

根据闭包的实现,我们可以看出,Closure 是一个私有的、不影响全局的对象。

从而有了以下特性:

  • 保存:闭包将局部变量的生命周期拉长,不在随着函数调用结束而回收。
  • 保护:不会成为全局变量、也不会收到外部影响。
    • (我认为是因为词法作用域的作用,也算闭包的一部分)

私有变量,很重要。

闭包的应用

模块

我们要提供一个模块供别人使用,要解决命名空间污染的问题。

命名空间污染:模块要用多个变量,我们希望变量不影响全局,全局也不影响我们的变量。

IIFE + 闭包 即可实现。


// 闭包实现模块化。
var moduleA = (function (global, doc) {
  var methodA = function() {};
  var dataA = {};
  return {
    methodA: methodA,
    dataA: dataA
  };
})(this, document);

现代JS提供ESM,原生支持模块。

模拟私有属性

// 模拟私有属性
function Test(name) {
  let _name = name;
  return {
    getName: function () {return _name;},
  };
}
let obj = new Test('z');
obj.getName(); // z
obj._name // undefined

现代JS,class支持私有属性(操作符#)。

高阶函数、有状态的函数

例如:高阶函数、科里化、节流防抖(等要判断状态的函数)

// 节流
function throttle(fn, timeout) {
    let timer = null
    return function (...arg) {
        if(timer) return
        timer = setTimeout(() => {
            fn.apply(this, arg)
            timer = null
        }, timeout)
    }
}

其实都应该算函数式编程的高阶函数。

闭包的缺点

内存泄漏

数据被闭包对象 Closure 引用,无法被释放回收。

闭包函数又在多个地方被引用,导致数据引用复杂,容易发生内存泄漏问题。

什么是闭包?

闭包(多种说法):

  • MDN:一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被用包围),这样的组合就是闭包closure)。
    • 闭包让你可以在一个内层函数中访问到其外层函数的作用域。
    • 在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。
  • Rust语言:闭包允许捕获调用者作用域中的值。
  • 维基百科:闭包在实现上是一个结构体,它存储了一个函数(通常是其入口地址)和一个关联的环境(相当于一个符号查找表)。
  • 闭包其实是一种技术:在函数调用栈的基础上,同时实现函数是一等公民、词法作用域。

闭包的实现有多种方式,所以解释就有差异。

面试就回答MDN的说法就好。

总结

  • 闭包解决了什么问题?
    • 函数调用栈的基础上,同时实现函数是一等公民、词法作用域。
  • JS中的闭包是什么?(MDN的解释)
    • 一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被用包围),这样的组合就是闭包closure)。
    • 闭包让你可以在一个内层函数中访问到其外层函数的作用域。
    • 在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。
  • 闭包的实现:
    • 原理:将闭包所需的数据,都存储到堆(Heap)上。
    • V8会将闭包所需的数据,存在函数的[[Scope]]Closure 对象上,这个对象在堆(Heap)上。
  • 闭包的特性
    • 保存:闭包将局部变量的生命周期拉长,不在随着函数调用结束而回收。
    • 保护:不会成为全局变量、也不会收到外部影响。
    • 可以构建私有变量
  • 闭包的应用
    • 模块
    • 私有属性
    • 高阶函数、有状态的函数
  • 闭包的缺点
    • 内存泄漏

参考文献

  1. MDN
  2. 维基百科
  3. JavaScript深入之词法作用域和动态作用域
  4. 深入理解JavaScript闭包(closure)
  5. 一文颠覆大众对闭包的认知(深讲)