Skip to content

这一次,彻底理解闭包

闭包的概念

闭包指的是有权访问另一个函数作用域中的变量的函数。

::: MDN 定义:闭包是指那些可以访问自由变量的函数。 :::

何为自由变量,指的是在函数中使用,但既不是函数也不是函数局部变量的变量。

因此更权威的定义是:

闭包 = 函数 + 函数能访问的自由变量
闭包 = 函数 + 函数能访问的自由变量

从理论角度:

从本质上理解,所有的函数在定义的时候就将执行上下文数据保存起来了,它的外部作用域的变量就变成了自由变量。

从实践角度,以下的两种情况形成了闭包:

    1. 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
    1. 在代码中引用了自由变量
js
var scope = 'global scope'
function checkscope() {
  var scope = 'local scope'
  function f() {
    return scope
  }
  return f
}

var foo = checkscope()
foo()
var scope = 'global scope'
function checkscope() {
  var scope = 'local scope'
  function f() {
    return scope
  }
  return f
}

var foo = checkscope()
foo()

在这个例子中,f 的执行上下文维护了一个作用域链:[A0, checkscopeContext.AO, globalContext.VO],正因为这个作用域链的存在,f 读取到了 checkscope 中变量的值,即使 checkscopeContext 被销毁了,js 依然会让它的变量活跃在内存中,成为自由变量。

闭包的使用场景

  1. 封装工具函数,返回一个新函数(例如防抖和节流)

  2. 在模块化的开发中,所有的文件都是在函数中运行,模块导出了方法,被方法访问到的模块顶层的变量,都会形成闭包,可以用于模块的状态管理和通信

闭包的创建和销毁

在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。

在上述例子中,fn 函数调用完毕后,foo 函数会自动销毁,但是 foo 函数中的 name 不会被销毁。这是因为垃圾回收机制(GC)作用时,被另一个作用域引用的变量不会被回收,除非 bar 函数解除调用。如下所示

js
fn = null // 销毁fn防止内存泄露
fn = null // 销毁fn防止内存泄露

拓展:通过闭包访问的对象一定不能被修改吗

js
let foo = (function () {
  let obj = {
    a: 1,
    b: 2
  }
  return {
    get(k) {
      return obj[k]
    }
  }
})()

// 给obj添加一个key:c
let foo = (function () {
  let obj = {
    a: 1,
    b: 2
  }
  return {
    get(k) {
      return obj[k]
    }
  }
})()

// 给obj添加一个key:c

可以通过劫持原型链得到对象本身

js
// 给obj添加一个key:c
Object.defineProperty(Object.prototype, 'getThis', {
  get() {
    return this
  }
})

// foo.get('c')
let obj = foo.get('getThis')
obj.c = 3
console.log(foo.get('c'))
// 给obj添加一个key:c
Object.defineProperty(Object.prototype, 'getThis', {
  get() {
    return this
  }
})

// foo.get('c')
let obj = foo.get('getThis')
obj.c = 3
console.log(foo.get('c'))

防范方式:断掉 obj 的原型链

js
let foo = (function () {
  let obj = Object.create(null)
  obj.a = 1
  obj.b = 2
  // 或者采用下面这种方式断掉原型链
  // obj.__proto__ = null;
  return {
    get(k) {
      return obj[k]
    }
  }
})()
let foo = (function () {
  let obj = Object.create(null)
  obj.a = 1
  obj.b = 2
  // 或者采用下面这种方式断掉原型链
  // obj.__proto__ = null;
  return {
    get(k) {
      return obj[k]
    }
  }
})()

小结

闭包的本质是函数执行上下文的创建,函数在定义的时候就会确定执行上下文,函数使用到的外部变量也被标记引用。

只要函数没有被销毁,外部变量就将一直存在,而无法被垃圾回收。