必包在于返回的函数拥有自己独立可访问的外部作用域。
作用域 & 执行上下文
简单例子入门
function wrapperFunction() {
let name = 1;
return function () {
console.log(name);
}
}
let fuc = wrapperFunction()
fuc()
复制代码
这里fuc的作用域形成了一个简单的必包环境。
这里最大的特点是当前func拥有对wrapperFunction变量环境的唯一访问权限。可能你会说wrapperFunction对他自己拥有访问权限的啊,但是当wrapperFunction在执行的时候是另外一个权限的环境了。因为这里func的环境变量里依靠内存存储了wrapperFunction的变量。
必包什么时候才会消失
变量的生命周期取决于闭包的生命周期。被闭包引用的外部作用域中的变量将一直存活直到闭包函数被销毁。如果一个变量被多个闭包所引用,那么直到所有的闭包被垃圾回收后,该变量才会被销毁。
所以必包的使用我们会在不再使用的时候,主动将其delete掉。
Timer
(function autorun(){
let x = 1;
setTimeout(function log(){
console.log(x);
}, 10000);
})();
复制代码变量 x 将一直存活着直到定时器的回调执行或者 clearTimeout() 被调用。
如果这里使用的是 setInterval() ,那么变量 x 将一直存活到 clearInterval() 被调用。
译者注:原文中说变量 x 一直存活到 setTimeout() 或者 setInterval() 被调用是错误的。Event
(function autorun(){
let x = 1;
$(“#btn”).on(“click”, function log(){
console.log(x);
});
})();
复制代码当变量 x 在事件处理函数中被使用时,它将一直存活直到该事件处理函数被移除。
Ajax
(function autorun(){
let x = 1;
fetch(“http://”).then(function log(){
console.log(x);
});
})();
复制代码变量 x 将一直存活到接收到后端返回结果,回调函数被执行。
在已上几个示例中,我们可以看到,log() 函数在父函数执行完毕后还一直存活着,log() 函数就是一个闭包。除了 timer 定时器,事件处理,Ajax 请求等比较常见的异步任务,还有其他的一些异步 API 比如 HTML5 Geolocation,WebSockets , requestAnimationFrame()也将使用到闭包的这一特性。
变量的生命周期取决于闭包的生命周期。被闭包引用的外部作用域中的变量将一直存活直到闭包函数被销毁。如果一个变量被多个闭包所引用,那么直到所有的闭包被垃圾回收后,该变量才会被销毁。
经典面试题
书写代码:期待返回的结果是5,0,1,2,3,4
必包
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
console.log(i); // 5, 5, 5, 5, 5, 5
复制代码
定时器的执行机制在于,会讲当前函数压入事件队列,直到主流程执行完了之后,在执行。所以这里我们来看一下结果。
setTimeout执行的时候会讲第一次的callback函数压入事件队列,类比其他几次的setTimeout是相同的结果。然后因为他们的外部作用域都相同。都拥有对i变量的访问。因为i的值不具有块作用域。在循环的过程当中,这个变量已经累加到5。
立即执行表达式
for (var i = 0; i < 5; i++) {
(function(j) {
setTimeout(function() {
console.log(j);
}, 1000);
})(i);
}
console.log(i);
复制代码
这样处理的原因是:通过IIFE立即执行函数表达式。通过函数执行 + 参数复制的模式,给每一个setTimeout创建一个特有的父级作用域。
setTimeout api操作
for (var i = 0; i < 5; i++) {
setTimeout(function(j) {
console.log(j);
}, 1000, i);
}
console.log(i);
复制代码
这种做法是参数复制作为函数自身的变量对象的处理方法。
let
块级作用域处理
var j = 0
for (let i = 0; i < 5; i++, j++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
console.log(j);
复制代码
promise
0-1-2-3-4-5
const tasks = [];
for (var i = 0; i < 5; i++) {
((j) => {
tasks.push(new Promise((resolve) => {
setTimeout(() => {
console.log(j);
resolve();
}, 1000 * j);
}));
})(i);
}
Promise.all(tasks).then(() => {
setTimeout(() => {
console.log(i);
}, 1000);
});
// let 方式
const tasks = [];
for (let i = 0; i < 5; i++) {
tasks.push(new Promise((resolve) => {
setTimeout(() => {
console.log(i);
resolve(i);
}, 1000 * i);
}));
}
Promise.all(tasks).then((res) => {
setTimeout(() => {
console.log(res[res.length - 1] + 1);
}, 1000);
});
复制代码
工作当中的应用
具有内部状态或者内部处理的代码块。
- hoc高阶函数
- redux
- router
闭包造成的内存泄漏
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<style>
</style>
</head>
<body>
<div class="left">LEFT</div>
<div class="right">RIGHT</div>
<script type="text/javascript">
var t = null;
var replaceThing = function () {
var o = t;
var unused = function () {
if (o) {
console.log("hi");
}
}
t = {
longStr: new Array(100000).fill('*'),
someMethod: function () {
console.log(1)
}
}
}
setInterval(replaceThing, 1000)
</script>
</body>
</html>
复制代码
这里每一次的定时器方法执行都会存在前后两次方法执行的引用【通过t变量造成的变量引用】。造成每一次方法执行创建的数据仍然可以访问。
闭包本身不会造成内存泄漏,只是因为书写代码的不规范,造成闭包里本该不可达的作用域仍然可达,无法通过js垃圾回收回收掉,才会造成内存泄漏。
总结闭包定义:
红宝书定义:
匿名函数经常被人误认为是闭包(closure)。闭包指的是那些引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现的
匿名函数经常被人误认为是闭包(closure)。闭包指的是那些引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现的
MDN 对闭包的定义为:
闭包是指那些能够访问自由变量的函数。
那什么是自由变量呢?
自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量。
由此,我们可以看出闭包共有两部分组成:
闭包 = 函数 + 函数能够访问的自由变量
权威指南定义
所以在《JavaScript权威指南》中就讲到:从技术的角度讲,所有的JavaScript函数都是闭包。
咦,这怎么跟我们平时看到的讲到的闭包不一样呢!?
别着急,这是理论上的闭包,其实还有一个实践角度上的闭包,让我们看看汤姆大叔翻译的关于闭包的文章中的定义:
ECMAScript中,闭包指的是:
从理论角度:所有的函数。因为它们都在创建的时候就将上层上下文的数据保存起来了。哪怕是简单的全局变量
也是如此,因为函数中访问全局变量就相当于是在访问自由变量,这个时候使用最外层的作用域。
从实践角度:以下函数才算是闭包:
即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
在代码中引用了自由变量