对。时隔10个月,回来填坑了......我就是鸽王
闭包这个话题,可能这样聊的话逻辑感好点(面试)。你们可以参考着来,不过这只是我的想法,你们也可以按自己的想法来。 注意区分哪些是我想拓展的内容,哪些是我主要的讲解内容。(分割线隔开了)
1. 给出定义,什么是闭包
内部函数总是可以访问其所在的外部函数中声明的参数和变量,即使在其外部函数被返回(寿命终结)了之后。
(注:这是我们比较容易接受的定义,可以直接在讲闭包问题的时候,开局就甩出来。)
(这里拓展一下,MDN上对闭包的定义,如果觉得这种好理解,也可以去查查相关资料)
闭包是指那些能够访问自由变量的函数。
举个例子:
var a = 1;
function foo() {
console.log(a);
}
foo()
foo 函数可以访问变量 a,但是 a 既不是 foo 函数的局部变量,也不是 foo 函数的参数,所以 a 就是自由变量。 那么,函数 foo + foo 函数访问的自由变量 a 不就是构成了一个闭包嘛……
我第一次看到这个观点,是在冴羽大佬的文章里,也可以看看。juejin.cn/post/684490…
2. 怎样产生闭包?
就如定义所言,当函数体内的函数被拿到了函数体外面使用,就形成了闭包。
关于闭包的产生,这个话题我更愿意用一个示例向您说明。
先定义一个函数a,a里面有name、age等属性并赋值,然后里面再定义一个方法sayName,用来打印name,再将这个方法return出来。然后我们再在外部进行调用这个暴露出来的方法,例如foo=a()并执行foo()。
这样就相当于形成了一个闭包。
function a(){
this.name = 'a'
var sayName = function (){
consule.log(this.name)
}
return sayName
}
var b = a()
b()
3. 闭包是怎样产生的?
依然就刚刚的例子,进行探讨。我们会想,闭包到底是怎么形成的?
(这里分两个版本,一个简单版本,一个进阶版本)
(简单版)
(简单版本的讲解我之前写过文章,也有讲过 ,之前写这个的时候语言还略显青涩,写的比较白话。) juejin.cn/post/696175…
(具体简单版可以看看文章,这里进行简述。)
在a函数执行完之后,垃圾回收机制会询问是否可以完全销毁a的内存,但是a不知道自己什么时候还会被调用,所以会形成一个closure对象,将可能被调用的变量保存起来放到堆内,这样就可以不用继续占用栈内空间。
当真正调用foo方法的时候,就直接到堆内查找相应的变量的储存地址。
这样就实现了闭包的功能。
(进阶版)
其实进阶版更像是具体讲解简单版的实现操作,就是每一个简单步骤,都是依据什么来实现的。
前置知识(放了一些大佬的链接,大家也可以看看,我也是从这些地方学的):
(先看看上面的文章,冴羽大佬讲的很好的。)
(这是从执行上下文的角度看闭包的过程。)
- 生成一个全局上下文,将全局上下文压入执行上下文栈
- 全局执行上下文初始化
- 然后进入全局,进行具体代码执行。执行到a函数,创建a的上下文环境并压入执行上下文栈中。
- a上下文初始化,创建变量对象、作用域链(a函数的作用域链属性维护操作,也就是对a.[[scope]]的操作)、this等。
- a函数执行完毕,将a函数的执行上下文环境弹出执行上下文栈。
- 执行b函数,创建b函数的执行上下文,b函数上下文被压入执行上下文栈
- b执行上下文初始化,创建变量对象、作用域链(对b.[[scope]]的操作,先复制外层作用域,再压入自身作用域,从而形成作用域链。根据这个作用域链,我们能够查询到a.AO的值,然后在AO中保存了对相关变量的引用)、this等。
- b函数执行完成,b函数上下文从执行上下文栈中弹出。
- 全局执行上下文从执行上下文栈弹出。
(然后是一些细节方面的补充,主要是变量如何在堆中储存)
我们会疑惑,本示例中name变量到底储存在哪?为什么会这样储存?
从垃圾回收机制的角度来看,在第五步的时候a上下文环境出栈,通过对其变量的引用进行查询,发现可能会被调用(闭包),因此可能存在引用关系。所以在垃圾回收时将name变量存入新生代储存,经历新生代中Scavenge算法识别后,很快进入到老生代中储存,而老生代是在堆内储存的。
那么接下来,只要这个变量存在引用关系,就能在老生代中被保存下来,也就一直存在于堆内。
而另一边,我们模拟了作用域链的实现,就可以发现,在调用b函数的时候,我们能找到a.AO的值,里面存了对变量name的引用,因此能够实现对变量的调用。
4. 闭包的应用
了解了闭包是什么,那么我们就自然而然会想到闭包的应用。
闭包能够实现一种保护机制,能够使得变量不被销毁。并且闭包将这些变量变成了私有变量(只能由抛出的内部方法调用)而不是一个全局变量。
注:闭包具有“保护”作用:保护私有变量不受外界的干扰(在真实项目,尤其是团队协作开发的时候,应该尽可能地减少全局变量的使用,防止相互之间的冲突(“全局变量污染”)),那么此时我们完全可以把自己这一部分内容封装到一个闭包中,让全局变量转换为私有变量。
单例应用
//js的单例模式 实现登入
var createLogin = function(a,b,c){
console.log(a,b,c); // 只打印一次1,2,3
var div = document.createElement('div')
div.innerHTML = '我是登入的弹窗'
div.style.display = 'none'
document.body.appendChild(div)
return div
}
var getSingle = function (fn) {
var result;
return function(){ // return一个函数里面形成闭包
// 如果result用的话只执行一次,否则会执行
return result || (result = fn.apply(this,arguments))
}
}
var create = getSingle(createLogin)
document.getElementById('loginBtn').onclick = function (){
var loginLay = create(1,2,3)
loginLay.style.display = 'block'
}
防抖函数
**效果:**当持续触发
事件 一定时间
内没有在触发事件,事件处理函数会执行一次,如果设定时间到来之前 又一次触发事件,就重新开始延时。
**思路:**触发事件 一段时间内,又一次触发了事件,重新开始延时,代表的就是重新开始定时器。在设定时间内,又一次触发了事件 重新开始延时,代表的就是重新开始定时器,那么意味着上一次
还没有结束定时器需要清除掉,重新开始。
//防抖
// 在一定时间内重复操作的话,只会刷新操作读条,等到最后一次操作后计时执行。
function mydebounce(fun, delay) {
let timer;
return function (args) {
let that = this;
clearTimeout(timer); //重复操作后刷新读条
// 我们应该存储timer变量
// timer变量需要一直保存到内存中
// 既然你不想打印之前已经输入的结果,清除以前的触发的定时器
// 一直在内存当中 内存的泄漏 闭包
timer = setTimeout(function () {
fun.call(that, args);
}, delay);
};
}
节流函数
当持续触发的时候 保证一段时间内 只调用一次事件处理函数.
//节流
//计算最近一次操作的时间与现在时间的间隔,再与标准比较
function mythrottle(fun, delay) {
let pre = 0;
return function (...args) {
//同样返回一个函数,并且使用了外部变量
//形成闭包
//一直存在对其的引用关系
let now = Date.now();
if (now - pre > delay) {
pre = now;
fn.apply(this, args);
}
};
}
5. 闭包后引导话题
从整个过程也能看出,简单讲完闭包后,能将话题引导到作用域、作用域链、执行上下文、预编译、V8引擎的回收机制、JS代码的编译执行、this指向等问题,后续就看自己的发挥了。
交个朋友(2022.04.01记)
目前正在疯狂学习前端知识,想要成为更优秀的前端工程师,因此喜欢记录并分享自己的学习笔记。奈何本人知识储备有限,只能做到输出一些些自己的观念。
不过!我真的有做很多很多笔记(确实很多笔记是摘抄而来,因此作为自己的帖子发出不厚道....),我也真的很希望能和志同道合的小伙伴们分享交流更多知识点!
因此我简单搭建了一个自己的博客,希望能结识到更多小伙伴,如果有兴趣,来我博客逛逛吧~ 阿敏的成长日记