My Understanding of Closures in JavaScript
- Published on
- Authors
- Name
- Jerry
Many people feel confused when learning about the concept of closures in JavaScript. Closures are a very important concept in JavaScript and are often a topic in interviews. After reading numerous introductions to closures, in this article, I will explain closures from my understanding and how to use them in JavaScript.
What is a Closure?
I don't want to start with a complex definition, as this might confuse you even more. So, let's dive into this topic with a piece of code.
function getInfo() {
var info = {
name: 'John',
age: 30,
}
function getName() {
return info['name']
}
return getName
}
var getNameFunction = getInfo()
console.log(getNameFunction()) // John
In this code, we define a function getInfo
that returns another function named getName
. The returned function could also be an anonymous function returned directly via return
, but here, for clarity, we give it a name.
In the subsequent code, the getInfo
function is called and returns the getName
function. We then assign the getName
function to the variable getNameFunction
and call it, printing the result to the console. Let's think about what happens internally when we call getNameFunction
.
Here, the getNameFunction
needs to access the name
property from the info
object. However, the info
object does not exist within this function, so it tries to look for it in the outer scope. In this case, it finds the info
object, retrieves its name
property, and returns it.
When a function can't find a variable internally, it will attempt to look in the next outer scope. If it still can't find it, it will continue searching outward until it reaches the global scope. This chain-like search is called the Scope Chain. A crucial concept to remember is that the scope chain is determined when a function is defined, not when it is called.
So, what are the "nodes" on the scope chain? How does JavaScript know if a variable is on a "node" in the current scope? This brings up another concept: the Variable Object (VO)
, which contains function parameters, local variables, and function declarations. It can be thought of as an object, but the variable object is an abstract concept and cannot be directly accessed via code; the usable object is something else.
In JavaScript, whenever a function is called, an execution context is created, along with an actual usable variable object called the Activation Object (AO)
, which is added to the front of the scope chain. When a variable needs to be found, the search starts from the front of the scope chain. The activation object can be thought of as a concrete implementation of the variable object.
Once a function completes execution, its execution context is destroyed. In the absence of closures, the local activation object is also destroyed. When all functions have finished executing, the only remaining variable object in memory is the global scope; all others will be garbage collected.
However, due to closures, the activation object inside a function cannot be destroyed because it is still referenced by another function defined inside it. Unless this other function is also destroyed, the activation object will persist. This is the essence of closures.
Returning to the initial code, how do we determine if there is a closure present? A simple method is to add a debugger statement inside the function, then use the browser's developer tools (I'm using Chrome 127.0.6533.120) to inspect the Scope tab, where you'll see a Closure entry—this is the closure.
function getInfo() {
var info = {
name: 'John',
age: 30,
}
function getName() {
debugger // Add a breakpoint here
return info['name']
}
return getName
}
var getNameFunction = getInfo()
console.log(getNameFunction()) // John
We can see which scope the closure references and which variable it points to.
Common Misconceptions
A common misconception is that immediately-invoked function expressions (IIFEs) always create closures. However, if an IIFE does not reference any variables from an outer scope, it does not create a closure.
;(function () {
var elements = document.getElementsByTagName('li')
for (var i = 0; i < elements.length; i++) {
elements[i].style.color = 'red'
}
})()
You can see that there is no closure here.
Questioning Definitions
The book "Professional JavaScript for Web Developer" is the gold-standard in intermediate-to-advanced JavaScript programming development books. Regarding P376 (4th edition) defines closures as:
Closures are functions that have access to variables from another function's scope. This is often accomplished by creating a function inside a function.
According to the book, closures are essentially functions. However, I have doubts about this definition. Let's modify the earlier example by wrapping it in another function:
function initialInfo(info) {
return function getInfo(keyName) {
return function getName() {
debugger
return info[keyName]
}
}
}
var info = initialInfo({
name: 'John',
age: 30,
})
console.log(info('name')())
Here, the getName
function needs to access the keyName
property from the info
object. Through the scope chain, it finds keyName
in the next outer scope and then finds info
in the next outer scope, thus referencing two different activation objects from two different scopes. Let's look at the browser developer tools:
Two closures appear here.
Here, the same getName
function references two different activation objects from different scopes, resulting in two closures. However, the book's definition claims that closures are functions, which doesn't quite explain this scenario. Moreover, the definition only mentions "variables in another function's scope," but as seen in the example above, the closure can reference another function's parameters. Thus, I believe the book offers an easily understandable explanation rather than a strict definition. Some front-end interview "cheat sheets" even treat it as a standard answer, which I find inaccurate.
In my understanding, a closure is more of a phenomenon — it's when a function references another function's activation object, preventing it from being destroyed after the function completes, possibly causing memory to be unreleased.
Functional Programming
Closures are frequently used in JavaScript's functional programming paradigm. In this paradigm, functions often return other functions or define other functions internally, and these internal functions form closures. The example above is actually a functional programming example.
Closures can also be used to implement currying and other features.
function add(a) {
return function (b) {
debugger
return a + b
}
}
var add5 = add(5)
var add10 = add(10)
console.log(add5(3)) // 8
console.log(add10(add5(3))) // 18
Memory Leaks
A memory leak occurs when memory that has been allocated is not properly released or collected.
Closures are a powerful feature in JavaScript but can also lead to memory leaks. Because closures prevent the activation object inside a function from being destroyed and the referenced variables from being garbage-collected, they consume more memory. Although modern browsers no longer use reference counting for garbage collection and engines like V8 use mark-and-sweep algorithms to reclaim inactive memory, we should still minimize memory leaks when possible.
很多人在学习 JavaScript 时,都会对闭包这个概念感到困惑。闭包(Closures)是 JavaScript 中一个非常强大的特性,也是面试时常考的题目。在阅读了一些关于闭包的介绍后,在这篇文章中,我将从我的理解出发介绍闭包,以及如何在 JavaScript 中使用它。
什么是闭包?
我不想一开始就给出一个很复杂的定义,因为这可能会让你更加困惑。所以,我将尝试从一段代码开始进入这个话题。
function getInfo() {
var info = {
name: 'John',
age: 30,
}
function getName() {
return info['name']
}
return getName
}
var getNameFunction = getInfo()
console.log(getNameFunction()) // John
在这段代码中,我们定义了一个函数 getInfo
,它返回了一个名为getName
的函数。实际上返回的函数也可以是匿名函数,直接通过return
返回,但我们这里为了便于理解,所以也给它命名了。
在随后的代码中,getInfo
函数被调用并返回了 getName
函数。然后我们将 getName
函数赋值给了变量 getNameFunction
,并调用了它,将结果打印到控制台。让我们想一想,当我们调用 getNameFunction
时,它的内部发生了什么?
在这里,getNameFunction
函数需要得到 info
对象里的 name
属性。但是,info
对象在该函数里并不存在,因此它尝试向外面一层的作用域中查找,在这里它找到了 info
对象,然后它获取到了当中的 name
属性并返回它。
函数在内部找不到一个变量时,总会尝试往外面一层的作用域查找,如果仍然找不到,就会继续往外面的作用域查找,直到全局作用域为止。这种链条式的查找,被称为作用域链(Scope Chain)。这里有一个很重要的概念需要记住,作用域链是在函数定义时就确定的,而不是在函数调用时。
那么在作用域链上的“节点”又是什么呢?JavaScript 如何得知某个变量是不是在当前作用域的“节点”上呢?这就要提到另一个概念:变量对象
,它包含函数的参数、内部变量和函数声明。它可以被理解为是一个对象,但变量对象是一个抽象的概念,不可以直接通过代码访问,而真正能被使用的对象另有它者。
在 JavaScript 中每一个函数被调用时,都会创建一个执行上下文,同时会创建一个可以被实际使用的变量对象,被称为活动对象
,并被添加到作用域链的最前端,当需要查找变量时,就会从作用域链的最前端开始查找。活动对象可以被理解为是变量对象的一个具体实现。
当函数执行完毕后,执行上下文就会被销毁,在不存在闭包的情况下,局部的活动对象也会被销毁,当所有函数都被执行完毕后,内存里就只剩下全局作用域这一个变量对象,其他都会被垃圾回收机制回收。
但是,由于闭包的存在,函数内部的活动对象不能被销毁,因为它还被内部定义的另一个函数引用着,除非这个函数也被销毁,否则活动对象就会一直存在,这就是闭包的本质。
我们回到一开始的代码,我们如何确定这段代码里存在闭包呢?一个简单的方法就是在函数内部添加一个debugger,然后借助浏览器的开发者工具(chrome 127.0.6533.120),可以看到在Scope标签里有一个Closure
,这就是闭包。
function getInfo() {
var info = {
name: 'John',
age: 30,
}
function getName() {
debugger // 在这里添加一个断点
return info['name']
}
return getName
}
var getNameFunction = getInfo()
console.log(getNameFunction()) // John
我们可以看到闭包引用的是哪一个作用域里的哪一个变量
常见误解
有一个比较常见的迷思就是,立即执行函数会产生闭包,但实际上,如果立即执行函数内部没有引用外部作用域的变量,那么它并不会产生闭包。
;(function () {
var elements = document.getElementsByTagName('li')
for (var i = 0; i < elements.length; i++) {
elements[i].style.color = 'red'
}
})()
可以看到这里并不存在闭包
存疑
关于闭包,被称为“JavaScript红宝书”的《JavaScript 高级程序设计(第四版)》309页给出的定义是:
闭包指的是那些引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现的。
因此书中认为闭包实际上是函数,但是我对这个定义存疑。我们把上面的例子改造一下,再包一层函数:
function initialInfo(info) {
return function getInfo(keyName) {
return function getName() {
debugger
return info[keyName]
}
}
}
var info = initialInfo({
name: 'John',
age: 30,
})
console.log(info('name')())
这里的 getName
函数需要得到 info
对象里的 keyName
属性,通过作用域链,往外一层找到了 keyName
,再往外一层找到了 info
,因此是对两个不同的作用域中的活动对象进行了引用。我们看看浏览器的开发者工具:
这里出现了两个闭包
这里的 getName
同一个函数引用了两个不同的作用域中的活动对象,因此产生了两个闭包,但是书中的定义认为闭包是函数,这就解释不通了,而且定义中只说了“引用了另一个函数作用域中变量
”,在以上例子可以看到,闭包引用的是另一个函数的参数。因此我认为书中给出的更多的是一种让人容易理解的方式,而不是严格的定义。有一些前端面试“八股文”中甚至把它视为标准答案,我认为这是不准确的。
我的理解是,闭包应该是一种现象,是一个函数对另一个函数活动对象的引用,导致该活动对象不能在函数执行完毕后被销毁,内存有可能无法被回收,这样的现象。
函数式编程
在 JavaScript 的函数式编程(Functional Programming)中,会经常使用闭包。在这种编程范式中,函数往往会返回另一个函数,或者在函数内部定义其他函数,这些内部函数就会形成闭包。我们上面的这个例子,实际上也是一个函数式编程的例子。
另外,闭包也可以用来实现柯里化(Currying)等功能。
function add(a) {
return function (b) {
debugger
return a + b
}
}
var add5 = add(5)
var add10 = add(10)
console.log(add5(3)) // 8
console.log(add10(add5(3))) // 18
内存泄漏
内存泄漏(Memory Leak)是指在计算机程序中,内存被分配但没有被正确释放或回收的情况。
闭包是 JavaScript 中一个非常强大的特性,但也有可能导致内存泄漏。因为闭包会导致函数内部的活动对象无法被正常销毁,引用的变量也无法被回收,因此它比更占内存。虽然目前主流的浏览器已经不再使用引用计数法作为垃圾回收机制,像V8引擎等也会通过基于标记清除的方式,回收不活跃的内存,但是我们还是应该尽量避免内存泄漏。