记得有次面试的时候,面试官问我,什么是闭包,我巴拉巴拉说了一堆,然后面试官看了我一眼,淡淡的说,你工作快两年了,连闭包都不清楚,我脑袋一蒙,心里想,明明复习了好几次,也查了很多资料,这就是闭包啊,面试完毕,回来继续差,终于,经历九九八十一难,我把闭包给安排的明明白白了。
很多时候我们会粗浅的认为闭包就是引用一个局部作用域下的变量,通过函数 return 出去的方式行程,通过和同事交流,发现这种想法很多,甚至一些同事跳槽的面试官在面试时也是这样理解的,下面我们就来梳理一下闭包。
定义
先来看 MDN 关于闭包的定义:一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。
是不是有点看不大懂,没关系,我们慢慢来!
关键点
JavaScript 闭包的本质来源于两点,词法作用域 和 函数当作值传递。
闭包就是,外部能访问到一个函数内的词法作用域(就是访问到函数内部定义的变量)
函数当作值传递
什么是把函数当作值传递呢,举个栗子:
function fun() {
return fun1() {
console.log("a")
}
}
词法作用域
词法作用域是定义表达式并能被访问的区间。换言之,一个声明(定义变量、函数等)的词法作用域就是它被定义时所在的作用域。
举个小栗子
function getName() {
const myName = "Oluwatobi";
return myName;
}
console.log(getName()) // 'Oluwatobi'
我们在getName()函数内定义并调用了myName变量。因此,myName的词法作用域是getName()的局部作用域,因为getName()是myName定义时所在的作用域。非常容易理解。
因此
因为作用域的缘故,我们访问不到函数内部的变量,所以我们通过函数将值传递出来。内部函数对外部作用域变量的引用,这样就造成了处于被引用状态的堆内存数据,导致不会被垃圾回收清除。
function fn () {
let a = 1;
let b = 2;
return function () {
console.log(a);
}
}
let foo = fn();
foo();
如果闭包函数被外部变量接收,那么这个存在于堆内存中的闭包函数对象就一直存在着(因为它被外部引用着,只要外部作用域不退出,就不会被垃圾回收清除)。那么这个函数对象上的 [[Scope]] 属性(即作用域链)自然就不会被清除,那么作用域链引用的所有层级包含函数的活动对象就不会被清除。
这也就意味着,所有返回函数的函数被接收之后,都有占用内存的闭包问题。