JS闭包的讲解和应用

147 阅读9分钟

什么是闭包

闭包:它是函数运行时候所产生的机制,函数执行会形成一个全新的私有上下文,可以保护里面的私有变量和外界互不干扰(保护机制)。但是大家所认为的闭包,需要当前上下文不能被出栈释放,这样私有变量及它的值也不会被释放掉(保存机制)。

闭包的作用:保护作用和保存作用。

文字描述太过抽象,下面通过题目进行讲解。

题目精讲

题目一

let x = 1;
function A(y) {
    let x = 2;
    function B(z) {
        console.log(x+y+z);
    }
    return B;
}
let C = A(2);
C(3);

解析

代码执行的时候,会形成对应的上下文,然后进栈执行。

首先会有一个EC(G)即全局上下文,其中VO(G)存储全局上下文中的变量对象。

在全局下的代码执行中,先创建一个值1,再创建一个变量x,让x和1关联在一起。对于函数function A,会开辟一个堆内存AAAFFF000,在此堆内存中,记录了当前函数的作用域是EC(G),我们自己标注一下形参y,以及函数体作为字符串存储其中。然后把这个堆内存的地址AAAFFF000放到栈里面以供调用。全局下创建了一个变量A,和AAAFFF000关联起来。

let C = A(2);是先把函数执行(传递实参是2),执行的返回结果赋值给变量C。

函数A执行,传递参数是2,会形成一个私有上下文,记作EC(A),在私有上下文中AO(A)用来存放私有的变量对象。然后依次进行:初始化作用域链<EC(A),EC(G)>,初始化this(目前用不到,先省略,后面的笔记中会对this进行专门的讲解),初始化arguments(用不到,图中不再标注,注意,箭头函数是没有arguments的),形参赋值:y=2(私有的,在私有变量对象中给予记录),变量提升(暂时不标注,在后面的笔记中会专门讲解关于变量提升的专题),代码执行。

EC(A)中的代码执行:在私有上下文下创建一个值2,然后声明的x和2进行关联在一起。创建一个函数B,首先开辟一个堆内存BBBFFF000,在此堆内存中,记录了当前函数的作用域是EC(A),我们自己标注一下形参z,以及函数体作为字符串存储其中。然后把这个堆内存的地址BBBFFF000放到A(2)的私有上下文EC(A)中以供调用。EC(A)中创建了一个私有变量B,和BBBFFF000关联起来。最后return B,return是函数返回值,返回的需要是一个值,BBBFFF000。这个返回值赋值给全局下的C。

我们注意到,函数A执行的时候,形成一个私有上下文,而依赖于这个私有上下文,又创建了一个堆BBBFFF000,而这个堆又被当前私有上下文以外的全局上下文中的C所调用。所以,函数A执行的私有上下文在执行完毕后不能释放,因为一旦释放,依赖于这个上下文的BBBFFF000也会被释放,也就导致C没有指向了,这样显然是不行的。

接下来C执行,形成一个私有的上下文EC(C),在私有上下文中AO(C)用来存放私有的变量对象。然后依次进行:初始化作用域链<EC(C),EC(A)>,初始化this(目前用不到,先省略,后面的笔记中会对this进行专门的讲解),初始化arguments(用不到,图中不再标注,注意,箭头函数是没有arguments的),形参赋值:Z=3(私有的,在私有变量对象中给予记录),变量提升(暂时不标注,在后面的笔记中会专门讲解关于变量提升的专题),代码执行。console.log(x+y+z);x不是自己私有的,是上级上下文EC(A)中的,是2,y不是自己私有的,是上级上下文EC(A)中的,是2,z是自己的私有变量,是3。最后输出结果是7.当前上下文代码执行完成后,它里面没有东西被外面占用,为了优化栈内存空间,默认会把其移除栈释放掉。

题目二

let x = 5;
function fn(x) {
    return function(y) {
        concole.log(y + (++x));
    }
}
let f = fn(6);
f(7);
fn(8)(9);
f(10);
console.log(x);

解析

题目三

let a = 0,
    b = 0;
function A(a) {
    A = function(b) {
        alert(a+b++);
    };
    alert(a++);
}
A(1);
A(2);

解析 注意:A在第一次执行完毕之后,会被重构,其指向会改变,暂且记作从A1变成A2,所以在第二次执行的时候,所执行的函数是A2。而第二次执行的上下文是在A1中创建的,所以,A2的上级上下文是A1。 上级上下文只跟在哪创建的有关。

JS中的内存优化

  1. 栈内存(执行上下文)

一般情况下,函数执行完,所形成的上下文会被出栈释放掉。

特殊情况:当前上下文中某些内容被上下文以外的事物占用了,此时不能出栈释放。

全局上下文:是在加载页面的时候创建的,所以也只是在有页面关闭的时候才会被释放掉。

  1. 堆内存

浏览器的垃圾回收机制:1、引用计数(以IE为主):浏览器空闲的时候会把所有计数为0的内存释放掉。在某些情况下会导致计数混乱,这样会造成内存不能被释放掉(内存泄露)。2、检测引用(检测占用或者标记清除,以谷歌为主):浏览器会在空闲的时候会一次检测所有的堆内存,把没有被任何事物占用的内存释放掉,以此来优化内存。

拿上文中第三题为例,对谷歌浏览器下的内存优化进行解释:程序执行结束之后,AAAFFF000堆内存已经不再被占用,浏览器会在空闲的时候将其释放,而BBBFFF000内存被全局下的A占用,如何优化内存?让A=null(或者等于其它任何值,只要不再等于BBBFFF000这个地址),**手动释放内存,其实就是解除占用(其实就是接触指针执行),我们一般都是手动赋值为null(空对象指针)。**这样全局的A不再占用这个堆内存,浏览器在空闲的时候会检测到BBBFFF000不再被占用,就会释放掉这个堆内存,这样EC(A1)这个栈内存无人占用,也会被释放掉。

对于上文中的题目二,未被释放的内存有AAAFFF000,EC(FN1),BBBFFF000,需要手动释放: fn=null, f=null。

对于上文中的题目一,未被释放的内存有AAAFFF000,BBBFFF000,需要手动释放: A=null, C=null。

闭包的弊端

大量应用闭包肯定会导致内存的消耗,但是闭包的保护和保存作用,在真实开发中我们还是需要的,所以需要学会“合理使用闭包”。

闭包的一些应用

ECSTACK/EC/AO/VO/SCOPE/SCOPE-CHAIN/释放不释放/垃圾回收机制...

  1. 实战用途
  2. 高阶编程:柯理化/惰性函数/compose函数
  3. 源码分析:JQ/LODASH/REACT(REDUX/高阶组件/HOOKS)...
  4. 自己封装插件组件的时候

实战用途举例

需求:实现换肤效果:点击按钮,页面背景颜色发生相应的改变。

<!DOCTYPE html>
<html>
    <head>
    	<meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <meta name="viewport" content="width=device-width, user-scalable=no,initial-scale=1.0">
        <titlt>闭包的应用</titlt>
        <!--import css-->
        <style>
        	html,
            body {
                margin: 0;
                padding: 0;
                height:100%;
            }
            
        </style>
    </head>
    <body>
        <button></button>
        <button>绿</button>
        <button></button>
        <button></button>
        <button></button>
        <!--import js-->
        <script>
        	var arr = ['red', 'green', 'blue', 'black', 'pink'];
            var buttonList = document.getElementsByTagName('button');
            for (var i = 0; i < buttonList.length; i++) {
                buttonList[i].onclick = function() {
                    //i的值都是5
                    
                    //点击按钮执行函数 形成私有上下文EC(AN)
                    //作用域链<EC(AN), EC(G)>
                    //color是私有的,但是i不是私有的,i是全局下的,此时全局的i已经变成了循环结束的5了,不是我们期望的索引
                    var color = arr[i];
                    document.body.style.backgroundColor = color;
                }
            }
        </script>
    </body>
</html>

以上的JS代码是不能实现我们的需求的。在ES6之前,我们的上下文只有全局上下文和函数私有上下文。for循环是全局上下文。EC(G)中的变量有arr, buttonList, i, i的初始值是0.

i = 0

​ 第一轮循环给第一个按钮的点击事件绑定一个方法(此时没执行,点击才执行),i++

i=1

​ 第二轮循环给第二个按钮的点击事件行为绑定一个方法(此时没执行,点击才执行),i++

......

i=5 循环结束

点击按钮执行函数 形成私有上下文EC(AN),作用域链<EC(AN), EC(G)>。color是私有的,但是i不是私有的,i是全局下的,此时全局的i已经变成了循环结束的5了,不是我们期望的索引。

如何解决?——利用闭包是可以解决的:

var arr = ['red', 'green', 'blue', 'black', 'pink'];
var buttonList = document.getElementsByTagName('button');
for (var i = 0; i < buttonList.length; i++) {
    //i=0 第一轮循环
    //  自执行函数执行,形成一个私有的上下文
    (function(i) {
        /*
         *EC(AN)
         *	作用域链<EC(AN), EC(G)>
         *	形参赋值:i=0
         *此上下文是不能被释放的
         */
        //buttonList[0].onclick = AAAFFF000 ([[scope]]:EC(AN))
        buttonList[i].onclick = function(){
            /*
             *私有上下文EC(EV)
             *	作用域链:<EC(EV), EC(AN)>
             */
            //这里再遇到的i找的不是全局了,而是上级上下文EC (AN),而在这个闭包中存储了对应的索引i
            var color = arr[i];
        	document.body.style.backgroundColor = color;
        };
    })(i)
}

i=0第一轮循环,自执行函数执行(把每一轮循环全局i的值作为实参传递给私有上下文中的形参,第一轮循环传递的是0),形成一个私有上下文EC(AN),作用域链<EC(AN), EC(G)>,形参赋值 i=0,代码执行的时候,buttonList[0].onclick = AAAFFF000([[scope: EC(AN)]]) (把私有上下文中创建的堆内存地址赋值给当前上下文以外的buttonList第一个按钮的点击事件,导致当前上下文不能释放),此上下文EC(AN)是不能释放的。第一轮循环形成了一个闭包。

当点击第一个按钮的时候,触发其函数执行,又会形成一个私有上下文EC(EV),作用域链<EC(EV), EC(AN)>,遇到i就找的不是全局了,而是上级上下文EC(AN),而在这个闭包中存储了对应的索引i

另一种写法:

var arr = ['red', 'green', 'blue', 'black', 'pink'];
var buttonList = document.getElementsByTagName('button');
for (var i = 0; i < buttonList.length; i++) {
    //把自执行函数执行的返回值赋值给onclick
     buttonList[i].onclick = (function (i){
         retrun function(i) {
     				var color = arr[i];
     				document.body.style.backgroundColor = color;
    			};
     })(i);
}

另一种写法:用ES6中的let,和上面的原理是一样的,都是闭包的机制(但是因为let的块级作用域是浏览器底层机制实现的,比我们自己创建的闭包性能要更好一些)。

var arr = ['red', 'green', 'blue', 'black', 'pink'];
var buttonList = document.getElementsByTagName('button');
for (let i = 0; i < buttonList.length; i++) {
    //每一轮循环都会形成一个私有的块级作用域,并且有一个私有的变量i,分别存储每一轮的循环的索引
    buttonList[i].onclick = function() {
        //上级作用域是创建的那个块级作用域(也不是全局的)
        var color = arr[i];
        document.body.style.backgroundColor = color;
    }
}

补充:关于let的处理

在ES6中,基于let/const创建的变量,如果是出现在非函数和对象的大括号中,大括号包裹的范围是一个“全新的块级作用域”

//ES5是不存在块级作用域的,所以出函数以外,都是全局上下文中的变量
{
    var n = 10;
    console.log(n);//10
}
console.log(n);//10

//ES6产生了块级作用域,n是块中私有的
{
    let n = 10;
    console.log(n);//10
}
console.log(n);//n is not defined
for (let i = 0; i < 5; i++) {
    console.log(i)
}
//会产生6个作用域,除了每次循环都产生的作用域,还有一个父作用域,用来控制循环

真实项目中遇到循环事件绑定的,我们最好告别闭包(包括let),有一种方法可以实现:

var arr = ['red', 'green', 'blue', 'black', 'pink'];
var buttonList = document.getElementsByTagName('button');
for (var i = 0; i < buttonList.length; i++) {
    //每一轮循环给当前的buttonList加一个自定义属性,即把每一轮的i存到各自按钮的自定义属性中
    //每一轮循环把后期需要用到的索引存储在自定义属性上
    buttonList[i].myIndex = i;
    buttonList[i].onclick = function() {
        //this=>当前操作的按钮
        var color = arr[this.myIndex];
        document.body.style.backgroundColor = color;
    }
}

但这还不是最好的,性能最好的实现方案是:事件委托

在html中给Button加上自定义属性index。即在结构上存储元素索引。
<body>
        <button index="0"></button>
        <button index="1">绿</button>
        <button index="2"></button>
        <button index="3"></button>
        <button index="4"></button>
        <!--import js-->
        <script>
        	var arr = ['red', 'green', 'blue', 'black', 'pink'];
            document.body.onclick = function (ev) {
                let target = ev.target,
                    targetTag = target.tagName;
                //当前点击的是五个按钮中的一个,target事件源就是点击的这个按钮
                if(targetTag === "BUTTON") {
                    var index = target.getAttribute('index');
                    document.body.style.backgroundColor = arr[index];
                }
            }
        </script>
</body>