先说观点
1. 什么是闭包?
现象说: 如果在一个函数内定义了另一个函数,在外层函数执行完毕后,再执行内部函数,这个内部函数也能访问外层函数执行时的变量
函数说: 如果在一个函数内定义了另一个函数,且内部函数中访问了来自外层函数的变量,就把这个内部函数 称为闭包函数
2. 产生闭包的条件
从上面两种定义中,我们至少可以找出产生闭包的三个关键要素:
- 要素1:两个函数有嵌套关系,即在 一个函数中,定义另一个函数
- 要素2:内部函数是在外部函数执行结束后才执行。 表现形式可以是,内部函数做为返回值返回
- 要素3:内部函数内访问了外部函数中的变量。 可以是实参或内部声明的变量
那我们来举个栗子🌰
function createFunc(a, b) {
let a2 = a*a;
let b2 = b*b;
return function () {
return a2+b2; // 访问外层函数的局部变量
}
}
let fn = createFunc(3, 4); // 我们执行了外层函数
fn() // 输出 25
这个例子,满足了,我们刚才提到的三个要素 ,再看看这个!
function sumsqu(a, b) {
let fn = function() {
return a*a + b*b;
}
return fn();
}
sumsqu(3, 4) // 25
虽然这段代码有两个嵌套的函数 ,但内部函数 fn 定义 后就立即执行了,所以并不会形成闭包。
再深入一点
那有的小伙伴说,你说的这三个要素 我早就知道了!闭包了解这么多应该够了吧!曾经我也是这么理解的,直到有一天,面试官问我闭包的原理😂 !
要了解闭包的原理,我们要引入一些概念, 作用域, 静态作用域,执行上下文,有了这些概念,我们再分析代码的执行流程,就更加清晰了
1. 作用域
一句描述作用域:作用域是标识符的查找范围。 什么是标识符:变量名和函数名。 在JS中支持三种作用域 分别是 全局作用域, 函数作用域 和 块级作用域
举个栗子🌰:
let a = 100; // 1
function log() { // 2
{
let b = 10; // 3
}
let c = 12; // 4
console.log(a+b+c); // 5
}
log();
我在代码中加上 数字的注释,方便说明执行的流程:
- 这段代码中我们 定义了a 和 函数 log 他们是位于全局作用域中的,可以把全局作用域 想象成一个大的盒子
- 最后一行我们调用了 log 函数 ,会进行一个标识符查找 ,因为这个log()位于代码最外层, 所以他的查找的范围是 全局作用域,可以很快的找到 函数 log
- 下面执行流程进入 log 函数内部 ,在 3 的地方 我们定义了一个 b = 10 ,它位于一对大括号中,这样会形成一个新的作用域(块级作用域),就是就是一个新的小盒子,这个盒子中,只有一个 b , 这个 b 对盒子外的代码 是不可见的
- 在 4 的地方, 我们定义了一个变量 c ,很明显,他定义在 函数log 作用域内, c的可访问范围就是这个函数
- 在 5 的地方,稍微有一点复杂,首先这是一个函数调用语句 ,js引擎要先查找 console 标识符,在 函数 log 中并没有出现过,于是 js 引擎会向上查找,进入全局作用域, 在全局作用域 console 是内置对象,直接返回,接着继续查找 log 属性,也成功找到。 a 的 查找过程与console 类似
- 注意 b 是无法找到的,这段代码最终是会报错,因为标识符查找,只会向上级查找
2. 静态作用域与作用域链
通过上面的分析,我们不难发现:通过分析代码中的函数和变量定义的位置,就可以清晰地知道代码中的各个作用域。不论我们何时调用log()
, 也是遵循这些确定好的规则 ,所以: JS中作用域只和代码结构有关与何时运行没有关系 ,比如lodash这类库,这个工具函数是由使用者执行的。 且在代码中各个作用域是可以相互嵌套, 标识符的查找正是基于这些嵌套的作用域进行由内向外进行的,最后到达全局作用域, 这个全局作用域,一般是是由JS的运行环境 提供的,比如浏览器中的全局作用域中有, window, atob(), Promise(), fetch() 等!
关于作用域的嵌套请看下图:
来个小结:
- 我们把这种基于代码结构分析 得到作用域的机制,叫 静态作用域也叫 词法作用域, 它是由JS引擎,按ECMAScript-262 标准来实现的
- 我们把 作用域嵌套 和 标识符由内向外的查找规则 ,叫 作用域链, 也就是标识符的每次查找都会由内向外在作用域链上查找
3. 静态作用域与闭包的关系
那你讲了半天的作用域和我闭包有什么关系🙄!
在我看来:闭包,是用来实现静态作用域的一种手段! JavaScript 的标准TC39委员会那波人制定的,而具体实现是写 JavaScript 引擎的那波人! 我仿佛听到他们的对话,“标准我们已经制定出来了,怎么实现就看你们的了!”
我们回到文章开头,闭包的例子!
function createFunc(a, b) {
let a2 = a*a;
let b2 = b*b;
function getSum() {
return a2+b2; // 访问外层函数的局部变量
}
console.dir(getSum) // 打印 函数 getSum
return getSum;
}
let fn = createFunc(3, 4); // 我们执行了外层函数
fn() // 输出 25
基于我们刚提到的静态作用域的知识 ,简单分析一下
- 这块代码中有两个作用域
createFunc
函数 作用域,和getSum
函数作用域,两个作用域相互嵌套 - getSum 函数中访问了 外层作用域中的
a2
和b2
两个变量 - 依据 作用域链的规则 ,在函数getSum 中 就应该可以访问
a2
和b2
- 当
createFunc
执行结束后,getSum
并没有执行 - 一般来讲,函数中的变量,会在函数执行完成后,释放掉!但有一种情况例外
- 为了实现第三点,我们必需找一个地方把
a2
和b2
存在起来,在函数getSum
调用时使用,只有这样才符合 JavaScript 静态作用域的规则
那到底存在哪儿呢?其实在函数 getSum
存在一个内部属性 [[Scopes]]
,在Chrome 中运行运行上面这段代码 瞅瞅:
可以看到图片的 Scopes 是一个类似数组的东西, 第一项是 Closure(createFunc) 里边两个值 a2=9
b2=16
, 看这名字难道是由 createFunc()
函数创建的闭包,挂在了 getSum
函数上。
对没错!是就是这样
感觉越来越接近原理了呢!
那我们给闭包重新 下一个定义 吧:
闭包是实际上是 挂在函数上的一组没有释放的变量(或内存区域),在这个函数执行时使用!
还有几点要说:
- 闭包的是在对 函数进行 词法分析 时创建的,为了节约内存,闭包中只保留必要的值,也就是在这个函数执行时要用到的变量!
闭包的运用
1. 防抖和节流
贴一代码 ,大家感受一下
// 防抖
function debounce(fn, delay = 300) {
let timer;
return function () {
const args = arguments;
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
// 节流
function throttle(fn, delay) {
let flag = true;
return function () {
let args = arguments;
if (!flag) return;
flag = false;
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
2. 生成连续ID
let uniqueId = function() {
let start = 0;
return function (prefix = 'id_') {
let id = ++start;
if(prefix === 'id_') {
return `${id}`;
}
return `${prefix}${id}`
}
}();
闭包的使用场景 还有很多,要注意的是如一个闭包函数 ,就用不了就及时释放掉,以免过多消耗内存, 将闭包函数 赋值为 null 可以释放!
总结
- 闭包的本质是一组没有释放的变量(或内存区域),并且在函数被执行时,加到入执行上下文中!
- 闭包产生原因:因为
JavaScript
遵循静态作用域规则, 为了保证函数在执行时可以访问外层作用域的变量,而形成的一种实现机制
参考
- 《你不知道的 JavaScript 上》
- Closures - JavaScript | MDN
- 面试官:说说作用域和闭包吧
- JavaScript 的静态作用域链与“动态”闭包链
最后
感谢你读到这里, 希望你读完这篇文章对闭包有更进一步的了解 !
如果觉得有所收获 ,点个赞,再走吧~~