JavaScript闭包系列之闭包机制与循环中的事件绑定

158 阅读2分钟

个人笔记

闭包机制

闭包:函数运行的一种机制(不是某种代码形式)

函数执行会形成一个私有上下文,如果上下文中的某些内容(一般指的是堆内存地址,常见函数或对象产生的堆)被上下文以外的一些事物(例如:变量/事件绑定等)所占用(引用),则当前函数私有上下文不能被出栈释放「浏览器的垃圾回收机制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}`);
    }
};

只绑定一次事件