js – 通过几个例子了解闭包

必包在于返回的函数拥有自己独立可访问的外部作用域。

作用域 & 执行上下文

js – 作用域链
js – 执行上下文

简单例子入门

function wrapperFunction() {
  let name = 1;
  
  return function () {
    console.log(name);
  }
}

let fuc = wrapperFunction()

fuc()
 

这里fuc的作用域形成了一个简单的必包环境。

这里最大的特点是当前func拥有对wrapperFunction变量环境的唯一访问权限。可能你会说wrapperFunction对他自己拥有访问权限的啊,但是当wrapperFunction在执行的时候是另外一个权限的环境了。因为这里func的环境变量里依靠内存存储了wrapperFunction的变量。

image.png

必包什么时候才会消失

变量的生命周期取决于闭包的生命周期。被闭包引用的外部作用域中的变量将一直存活直到闭包函数被销毁。如果一个变量被多个闭包所引用,那么直到所有的闭包被垃圾回收后,该变量才会被销毁。

所以必包的使用我们会在不再使用的时候,主动将其delete掉。

juejin.cn/post/684490…

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>

image.png
这里每一次的定时器方法执行都会存在前后两次方法执行的引用【通过t变量造成的变量引用】。造成每一次方法执行创建的数据仍然可以访问。

闭包本身不会造成内存泄漏,只是因为书写代码的不规范,造成闭包里本该不可达的作用域仍然可达,无法通过js垃圾回收回收掉,才会造成内存泄漏。

总结闭包定义:

红宝书定义:
匿名函数经常被人误认为是闭包(closure)。闭包指的是那些引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现的

匿名函数经常被人误认为是闭包(closure)。闭包指的是那些引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现的

MDN 对闭包的定义为:

闭包是指那些能够访问自由变量的函数。

那什么是自由变量呢?
自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量。
由此,我们可以看出闭包共有两部分组成:

闭包 = 函数 + 函数能够访问的自由变量

权威指南定义
所以在《JavaScript权威指南》中就讲到:从技术的角度讲,所有的JavaScript函数都是闭包。
咦,这怎么跟我们平时看到的讲到的闭包不一样呢!?
别着急,这是理论上的闭包,其实还有一个实践角度上的闭包,让我们看看汤姆大叔翻译的关于闭包的文章中的定义:

ECMAScript中,闭包指的是:
从理论角度:所有的函数。因为它们都在创建的时候就将上层上下文的数据保存起来了。哪怕是简单的全局变量
也是如此,因为函数中访问全局变量就相当于是在访问自由变量,这个时候使用最外层的作用域。

从实践角度:以下函数才算是闭包:
即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
在代码中引用了自由变量

原创文章,作者:我心飞翔,如若转载,请注明出处:https://www.pipipi.net/14810.html

发表评论

登录后才能评论