个人笔记
闭包机制
闭包:函数运行的一种机制(不是某种代码形式)
函数执行会形成一个私有上下文,如果上下文中的某些内容(一般指的是堆内存地址,常见函数或对象产生的堆)被上下文以外的一些事物(例如:变量/事件绑定等)所占用(引用),则当前函数私有上下文不能被出栈释放「浏览器的垃圾回收机制GC所决定的」 =>这就是闭包的机制:形成一个不被释放的上下文
闭包机制会有以下的影响:
- 保护:保护私有上下文中的“私有变量”和外界互不影响
- 保存:上下文不被释放,那么上下文中的“私有变量”和“值”都会被保存起来,可以供其下级上下文中使用
弊端:如果大量使用闭包,会导致栈内存太大,页面渲染变慢,性能受到影响,所以真实项目中需要“合理应用闭包”;某些代码会导致栈溢出或者内存泄漏
闭包的具体分析可以看我的上一篇文章
栈溢出
递归:函数执行中再次调用自己执行
递归函数执行的时候,无限制得生成很多私有上下文入栈,会导致栈内存太大溢出
下面案例是“死递归” ,会出现Uncaught RangeError: Maximum call stack size exceeded的错误 (内存溢出)
function fn(x) {
// console.log(x);
fn(x + 1);
}
fn(1);
如果使用递归一定要有一个结束条件,否则就会内存溢出
闭包应用(循环中事件绑定的几种解决办法)
<button >我是第1个按钮</button>
<button >我是第2个按钮</button>
<button >我是第3个按钮</button>
var buttons = document.querySelectorAll('button'); //=>NodeList“类数组”集合
for (var i = 0; i < buttons.length; i++) {
console.log(buttons[i])
buttons[i].onclick = function () {
console.log(`当前点击按钮的索引:${i}`);
};
}
不管点哪个按钮,每次都是i的最后一个值3。
原因:循环结束,此时全局变量i=3
每次点击,触发onclick函数,打印的变量i都不是函数中的私有变量,都会去上级上下文中(全局上下文)寻找,所以永远打印3
方案一:使用闭包解决
基于“闭包”的机制完成
「每一轮循环都产生一个闭包,“存储对应的索引”;点击事件触发,执行对应的函数,让其上级上下文是闭包即可」
var buttons = document.querySelectorAll('button');
for (var i = 0; i < buttons.length; i++) {
// 每一轮循环都会形成一个闭包,存储私有变量i的值(当前循环传递的i的值)
// + 自执行函数执行,产生一个上下文EC(A) 私有形参变量i=0/1/2
// + EC(A)上下文中创建一个小函数,并且让全局buttons中的某一项占用创建的函数
(function (i) {
buttons[i].onclick = function () {
console.log(`当前点击按钮的索引:${i}`);
};
})(i);
}
(function (i) {
buttons[i].onclick = function () {
console.log(`当前点击按钮的索引:${i}`);
};
})(i)
这段代码是如何应用闭包机制的?
创建函数function (i) {...}并立即执行,函数执行,形成私有上下文,这块栈内存不会释放,因为函数其中的这段代码
buttons[i].onclick = function () {
console.log(`当前点击按钮的索引:${i}`);
}
打印当前索引的这段函数代码(这块堆内存)被上下文以外的一些事物(buttons[i].onclick)所占用(应指向),则当前私有上下文不能被出栈释放(闭包机制),其中的私有变量i也不会释放,所以每次点击按钮,打印的就是立即执行的这块函数的私有上下文中的i。
另一种方式
一个例子
var obj = {
fn:(function(){
console.log('大函数立即执行')
return function (){
console.log('小函数被返回')
}
})()
}
//打印出'大函数立即执行'
obj.fn()
//打印出'小函数被返回'
自执行函数在给fn赋值的时候就执行了,把小函数的地址返回给fn
所以上面这段也可以这样写
var buttons = document.querySelectorAll('button');
for (var i = 0; i < buttons.length; i++) {
buttons[i].onclick = (function (i) {
return function () {
console.log(`当前点击按钮的索引:${i}`);
};
})(i);
}
另一种方式
基于let这种方式也是“闭包”方案
let buttons = document.querySelectorAll('button');
for (let i = 0; i < buttons.length; i++) {
buttons[i].onclick = function () {
console.log(`当前点击按钮的索引:${i}`);
};
}
方案一不太好,循环多少次就产生多少次闭包,内存不会被释放,真实项目中应该少用
方案二 :自定义属性(性能强于闭包)
因为每个button DOM 都是对象,我可以在每一个button DOM对象中自定义一个myindex存在DOM对象里
给当前元素的某个事件绑定方法,方法中的this就代表当前元素的DOM对象
var buttons = document.querySelectorAll('button');
for (var i = 0; i < buttons.length; i++) {
// 每一轮循环都给当前按钮(对象)设置一个自定义属性:存储它的索引
buttons[i].myIndex = i;
buttons[i].onclick = function () {
// this -> 当前点击的按钮
console.log(`当前点击按钮的索引:${this.myIndex}`);
};
}
方案三:事件委托 「比之前的性能高」
<!-- 在结构上设定自定义属性index,存储按钮的索引 -->
<button index='0'>我是第1个按钮</button>
<button index='1'>我是第2个按钮</button>
<button index='2'>我是第3个按钮</button>
//
// + 不论点击BODY中的谁,都会触发BODY的点击事件
// + ev.target是事件源:具体点击的是谁
document.body.onclick = function (ev) {
var target = ev.target,
targetTag = target.tagName;
// 点击的是BUTTON按钮
if (targetTag === "BUTTON") {
var index = target.getAttribute('index');
console.log(`当前点击按钮的索引:${index}`);
}
};
只绑定一次事件