js中的闭包机制

146 阅读4分钟

闭包

一般情况下,在函数执行完成后,所形成的私有上下文都会出栈被释放掉,也就是说私有上下文中的内容都会被销毁,进而优化栈内的空间。但是,如果该函数形成的上下文中有东西被当前上下文以外的事物所占用,那么当前上下文就不会被出栈释放,进而形成所谓的闭包。利用好闭包机制可以很好的保护私有变量不被全局的变量污染,可以说闭包的应用在项目中基本上无处不在

想要彻底了解闭包到底是什么回事,我们得从一个经典的案例来分析。
    <div>0</div>
    <div>1</div>
    <div>2</div>

    <script>
        var divList = document.querySelectorAll('div');
        for (var i = 0; i < divList.length; i++) {
            divList[i].onclick = function () {
                console.log(i);				
            }
        }
    </script>

在这个案例中我们首先给div绑定了事件,我们想获得被点击的div的索引。按照惯例,我们点击div就会获取当前循环的i,也就是0、1、2,然而当我们运行时却发现事实并非如此 点击时输出的都是却都是3

那为什么实现不了我们想要的结果呢,我们就得从声明变量的方式var来讲起

var:

  • var声明的变量会进行变量提升,JavaScript引擎会在代码执行之前将当前存储的变量加入到当前执行环境的词法环境中,进而提前对变量进行声明。
  • var声明的变量是可以重复定义的,并且可以修改值
 var a;				//声明了一个变量a,并没有赋值
 console.log(a);   // undefined
 
 var a = 1;		//声明了一个变量a
 var a = 2;		//又对a进行重复声明
 console.log(a);	//a = 2;只是对a进行了一个重新指向的过程
  • 暂时性死区问题:
console.log(typeof a); // =>"undefined"在原有浏览器渲染机制下,基于typeof等逻辑运算符检测一个未被声明过的变量,不会报错,返回undefined

我们回到之前的案例:var就相当于是在全局声明了一个变量,我们在每一轮循环中给对应元素的click绑定方法时,此时函数并没有执行。而当循环结束的时候此时全局的变量i=3 而当我们点击div时执行函数,此时形成一个全新的上下文,而他的上级上下文是全局上下文,代码执行时遇到i,查找的不是自己私有的,而是全局的,此时全局的i已经是循环结束的3了。

这就是为什么不会输出他的索引0,1,2的原因

下面我们思考一下解决问题的思路

造成问题的原因很简单,是因为当到代码执行阶段的时候我们查找的是全局的i。那我们只要在查找i的时候不要往全局查找就好了,我们查找本轮循环下私有的i就可以了。这就自然而然的运用到了闭包。

  • 方法一
    for (var i = 0; i < divList.length; i++) {
       (function (n) {
           divList[i].onclick = function () {
               console.log(n);	//0 1 2
           };
       })(i);
   }

我们将事件用自执行函数包起来,形成一个全新的私有上下文,并且把当前这轮全局的i作为实参传递给自执行函数执行形成的私有上下文中,所以当点击按钮执行的时候,遇到的i就不是全局的i而是当前私有上下文的i,以此来形成闭包,并且利用闭包机制来达到我们想要的结果

下面方法和上面原理都是一样的

    for (var i = 0; i < divList.length; i++) {
        divList[i].onclick = (function (i) {
            // i是每一轮形成的闭包中的私有变量
            // 每一次都是把小函数返回,赋值给元素的点击事件,当点击元素的时候,执行返回的小函数
            return function () {
                console.log(i);		//=>0 1 2 
            };
        })(i);
    }
  • 方法二 还是利用闭包的机制,但是不是自己去构建函数,而是利用ES6中的let自带的机制来进行实现
	//直接将声明变量的方式换成let,利用let执行时自带的块级私有上下文形成闭包,可以说let自带闭包机制。
   for (let i = 0; i < divList.length; i++) {
       divList[i].onclick = function () {
           console.log(i);	//=>0 1 2
       };
   } 
关于var/let/const的区别我会在下期详细分析哦。

所以闭包的用途:

  • 对函数内私有变量的保存和保护,并且对函数内部变量进行读取
  • 让变量的值始终保存在私有上下文中,不会被调用完毕后被浏览器自动清除掉
  • 方便调用上下文中的局部变量。有利于代码封装
  • 基于'闭包'实现早期的模块化思想(很多js的高阶编程技巧本质上都是基于闭包的机制来完成的)
    • AMD=>require.js
    • CMD=>sea.js
    • CommonJs=>Node
    • ES6Module
    • 单例设计模式

闭包虽然用起来很爽,但是也是有弊端的:

  • 弊端:私有上下文被占用调用完毕后无法释放掉,会占用内存,影响浏览器的性能。所以闭包不可乱用,我们平常为了防止闭包产生的堆无法释放对性能有所影响,经常会手动进行释放。这里就涉及到浏览器的垃圾回收机制了。