📖

对JavaScript闭包closure的理解

Tags
Published
Last Updated
修改:2024-11-11
内容构思:
  • 传统解释什么是JS中的闭包,MDN,YDKJS
  • 自己的话理解,满足什么条件是闭包
  • 闭包函数对变量的访问是基于变量不是值
  • 外层函数直接返回变量是基于值
  • 例子for loop let var
  • 内存分布:外部词法作用域中声明的变量受闭包影响后的内存分布,方法栈到堆?
  • 作用1:封装可见性,private效果, module模式
  • 作用2:React.js hooks实现底层原理,模拟的useState
  • 陷阱,闭包可能的内存泄漏,说一下自己认为闭包引用变量从函数栈转移到堆内存

对JavaScript闭包(Closure)的理解

什么是闭包?

对于程序员来说,不管学习使用的是哪一种编程语言,闭包Closure大都不是一个太陌生的概念。有时候我们难以用一句或几句话解释清楚对这一概念的理解,对于像本人一样不善表达的同学而言更是如此。因此对我们来说,最好的办法就是看别人尤其是比我们更聪明的人是如何对它进行解释的。
 
我们可以看MDN对JavaScript闭包的解释,一句话的总结:
A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment).
 
对我这种英文不好的人来说,看着这句话就比较懵,各种介词和定语(of, together with, references to, surrounding),不得不强行翻译理解一波,闭包Closure是一种组合(combination),什么的组合呢?函数(function)和对其周围状态(词法环境)(surrounding state (the lexical environment))的引用的组合。这是人话吗? 😂函数咱知道啊,啥叫周围状态或词法环境?其实我们更熟悉另一个概念,就是作用域(Scoping)或者叫词法作用域(Lexical Scoping),比如全局变量全局可访问,其作用域是全局;函数内部变量只在其内部可访问,其作用域是函数内部;逻辑块内部变量,如if else, for等形成的Block块,使用let, const关键字定义的变量其作用域是相应的Block内部。这又跟上述闭包概念中的函数周围状态有什么关系呢?当我们定义一个函数时,需要在代码中的某个地方对其函数体进行定义,被定义函数的函数体就可能访问属于某些不在该函数体内部声明的变量(如全局变量,外层函数变量,该函数的参数等)的作用域,这些变量就是当前被定义函数的周围状态。在函数体内使用相应的变量名可直接访问到其相应的周围状态变量,这个被定义的函数就对其所访问的周围状态变量具有了引用关系。
 
所以说闭包是一个函数和这个函数对其周围状态变量的引用的组合。当然,那些没有在函数体内访问的周围状态变量自然没有被该函数所引用,闭包也就没有包括对它们的引用。函数对其周围状态变量进行访问引用的这一动作,通常被叫作CaptureClose over
 
光知道这么个抽象的解释,我们还是无法清楚闭包能干什么,有什么功能,直bi接上代码:
function outer() { let a = 42; return function inner() { console.log(a); } } const func = outer(); func(); // output 42
上述代码定义了一个outer 函数,其内部声明定义了一个变量a ,并返回了一个函数inner ,而函数inner 内部访问了其外部函数outer 中的变量a 打印了其值,这里变量a 就是函数inner 的周围状态。当函数outer 被调用执行后,返回了内部定义函数inner 并将其赋值给变量func ,通过func() 对返回的函数inner 进行了调用,并发现函数outer中定义的变量a 的值42被打印了出来。这是比较反直觉的,因为变量a 在函数outer 被调用执行完毕后应该被销毁,不能被访问才对。当然我们可能会认为这是因为函数inner 其实是获取到了变量a 当时的值42 直接把语句console.log(a) 编译成console.log(42) 来执行的,这也很合理。
 
我们在上述代码的基础上加点料:
function outer() { let a = 42; return function inner(b) { a = b; console.log(a); } } const func = outer(); func(24); // output 24
上述代码打印输出了值24 而不是42 ,因为
 
个人认为这里面比较关键的词就是“引用”,我们知道JavaScript语言使用垃圾回收机制对变量进行内存管理(复杂对象还是primitive变量?),当一个变量不再被引用后,它会被垃圾回收器回收并释放其所占用的内存,反之,当对变量的引用依然存在时,垃圾回收器不会回收该变量,该变量也就可以被访问。闭包
 
什么叫维持了?JavaScript中闭包具有比较反直觉的特点,如果函数内部访问了其周围状态变量,那么在无法访问这些变量的作用域中执行该函数,该函数依然能正常执行且其内部依然能访问到相应的变量,这就是函数维持了对其周围状态(词法环境)变量的引用(我们通常看到一些文章中使用capture一词来描述)。

我对闭包的理解

在我看来,闭包是当一个函数能够记住并访问其词法作用域,即使当该函数在其原始作用域之外执行时,也能保持这种能力。要形成闭包,通常需要满足以下条件:
  • 有一个外部函数
  • 外部函数中定义了一个内部函数
  • 内部函数引用了外部函数的变量
  • 内部函数在其定义的词法作用域之外被调用

闭包的特性

基于变量而非值的访问

闭包函数对变量的访问是基于变量本身,而不是变量的值。这意味着如果外部函数的变量发生变化,闭包函数访问到的也是变化后的值。
function outer() { let count = 0; return function() { return ++count; } } const counter = outer(); console.log(counter()); // 1 console.log(counter()); // 2

外层函数直接返回变量

相比之下,如果外层函数直接返回变量,那么返回的是该变量的值,而不是对变量的引用。
function outer() { let count = 0; return count; } const value = outer(); console.log(value); // 0

闭包的实际应用

例子:for循环中的let和var

闭包在处理异步操作时特别有用,尤其是在循环中。考虑以下例子:
// 使用var for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 1000); } // 输出: 3, 3, 3 // 使用let for (let i = 0; i < 3; i++) { setTimeout(() => console.log(i), 1000); } // 输出: 0, 1, 2
使用let创建了块级作用域,每次迭代都会创建一个新的闭包,从而捕获当前的i值。

作用1:封装和私有性

闭包可以用来创建私有变量和方法,实现类似于其他语言中的私有成员的效果。这就是所谓的模块模式。
const counter = (function() { let count = 0; return { increment: function() { return ++count; }, decrement: function() { return --count; }, getCount: function() { return count; } }; })(); console.log(counter.getCount()); // 0 counter.increment(); console.log(counter.getCount()); // 1

作用2:React Hooks的实现原理

React Hooks的底层实现就依赖于闭包。例如,我们可以用闭包来模拟一个简单的useState hook:
function createUseState() { let state; return function(initialState) { state = state || initialState; function setState(newState) { state = newState; // 触发重新渲染 } return [state, setState]; } } const useState = createUseState(); // 在组件中使用 function Counter() { const [count, setCount] = useState(0); // ... }

闭包的潜在问题

虽然闭包强大,但使用不当可能导致内存泄漏。当闭包持有对大型对象的引用时,这些对象将不会被垃圾回收。
我的理解是,闭包引用的变量实际上从函数的栈内存转移到了堆内存中。这意味着即使外部函数执行完毕,这些变量也不会被立即回收,而是会一直存在于内存中,直到闭包本身被垃圾回收。

结论

闭包是JavaScript中一个强大的特性,它允许我们创建更灵活、更强大的函数。然而,理解和正确使用闭包需要深入理解JavaScript的作用域和执行上下文。通过合理使用闭包,我们可以编写出更加优雅和高效的代码。

参考链接

  1. Closures - JavaScript https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures
  1. You Don't Know JS Yet: Scope & Closures - 2nd Edition Chapter 7: Using Closures https://github.com/getify/You-Dont-Know-JS/blob/2nd-ed/scope-closures/ch7.md
  1. Deep dive: How do React hooks really work? https://www.netlify.com/blog/2019/03/11/deep-dive-how-do-react-hooks-really-work/