JS闭包全面解析

前置知识,js变量作用域

要想了解闭包,我们先要了解javascript中变量作用域的概念。
js中变量作用域分为两种:全局作用域、局部作用域
在继续学习下面内容前,建议您复习下《JS变量作用域&作用域链》,该文中详细阐述js变量作用域的知识,这里就不做过多解释。

通过了解变量作用域我们知道,js的变量作用域很特殊,采用的是“词法作用域”。
子作用域可以访问父作用域的变量。
但是父作用域无法访问到子作用域的变量。

为何使用闭包

但是出于一些原因,有时候我们需要得到函数内部的局部变量,通过上面的解释知道常规的手段是不行的,
那就使用非常规手段,请出今天的主角——闭包

闭包实现

闭包的概念:闭包的概念也可以理解为函数的概念,即
函数对象可以通过作用域链关联起来,函数体内部的变量都可以保存在函数作用域内,这种特性在计算机科学文献中成为“闭包”。
这句话是犀牛书8.6节闭包中的一段定义,可能过于官方,很多人都不太理解,那我们把这句话再翻译一下:
一个函数内部的函数可以访问到外部函数的变量。

从技术角度来说,所有的JavaScript函数都是闭包。
注:函数内不一定要有return,不过没有return没有意义。当然也可以返回一个对象(见最后一个栗子)。

下面通过几个栗子,让大家快速了解闭包

  • 栗子1
function fn(){
    var a = 5;
}

a是函数fn的局部变量,在外部是无法访问到的,但是由于子作用域可以访问父作用域的变量,我们将代码简单修改代码↓

function fn(){
    var a = 5;
    function fn2(){
        console.log(a);
    }
}

在函数fn内部定义函数fn2,fn2内部可以访问到a变量,那是不是可以将函数fn2作为返回值,这样是不是就可以在函数fn外部获取到变量a了,再改写代码↓

function fn(){
    var a=5;
    return function(){
        console.log(a);
    }
}
var fn3 = fn();
fn3(); //5 

将fn函数内部函数作为返回值,然后在函数fn外部调用返回的函数,正确输出a。这样就实现了我们最开始的需求(在函数外部拿到函数的局部变量)。

为什么会这样?

首先我们再复习一遍闭包定义,“一个函数的内部函数可以拿到外部函数的变量”。
再具体一点就是:一个函数的内部函数可以拿到外部函数的变量,然后将这个内部函数作为返回值返回
这样在函数外部调用返回的函数时同样可以拿到函数内部的这个变量,这就是闭包

什么原理?

一个普通的函数在执行完后,内部的变量都会被释放,但是这在个栗子中,js引擎发现返回的函数中使用了变量a,并且这个返回的函数在外部是有可能被执行的,所以变量a没有被释放,而是放到了一个只有这个返回的函数可以访问到的地方,此时a变量可以且只能被这个函数访问,每次调用fn()都会创建一个新的作用域链和一个新的私有变量

到这你还是有点懵,没理解,不用怕,刚接触都会懵,将上面的栗子反复看几遍,总会有所收获的。
如果到这你都能看懂,那么恭喜你,你已经掌握了闭包的基础用法。可以继续向下看了。

  • 栗子2
function fn(){
    var a = 1;
    return  function(){
        a++;
        console.log(a);
    }
}
var fn2 = fn();
fn2(); //2
fn2(); //3
fn2(); //4

这里可以看到,我们不光可以获取到fn函数内的局部变量a,还可以对其进行修改。因为变量a是一直存放在内存中fn2函数可以访问到的地方
再升级下代码↓

  • 栗子3
function fn() {
    var a = 1;
    return function() {
        a ++;
        console.log(a);
    }
}
var fn1 = fn();
fn1(); //2
var fn2 = fn();
fn2(); //2
fn2(); //3
var fn3 = fn();
fn3(); //2

上面代码将fn的返回函数分别赋给3个对象,fn1、fn2、fn3,
三次赋值相当于初始化3个a变量放到内存中,分别只供fn1、fn2、fn3使用。
fn1、fn2、fn3函数在执行的时候,分别访问的是各自区域内的a变量,3个区域不共享
原理:每次调用fn()都会创建一个新的作用域链和一个新的私有变量。

  • 栗子4
//第一题
function q1() {
    var a = {};
    ruturn function() {
        return a;
    }
}
var t1 = q1();
var o1 = t1();
var o2 = t1();
console.log(o1 == o2);//true

//第二题
function q2() {
    var a = {};
    ruturn function() {
        return a;
    }
}
var t1 = q2();
var t2 = q2();
var o1 = t1();
var o2 = t2();
console.log(o1 == o2);//false

分别输出true和false

  • 栗子5
    一些情况下,需要返回多个函数,这时候就用到返回对象
function fn() {
    var a = 10;
    return {
        add:function(addNum) {
            a += addNum;
            console.log(a);
        },
        sub:function(subNum) {
            a -= subNum;
            console.log(a);
        }
    }
}

var obj1 = fn();
obj1.add(5); // 15
obj1.add(20); // 35
obj1.sub(3); // 32

var obj2 = fn();
obj2.add(2); // 12
obj2.add(6); // 18

返回对象和返回函数用法基本一致,变量在不同对象间依然不共享。

内存释放

之前说到闭包中的变量在函数执行完后不会被释放,还是存放在内存中,势必会造成内存浪了。
没办法直接释放这个变量,如需释放变量就释放访问变量的函数,如上面栗子中的obj1、obj2。

//两种方式均可
obj1 = null;
obj2 = undefined;

释放掉对闭包函数的引用后,垃圾回收机制就会回收变量a。

总结

很多同学学完闭包都会有一个这样的问题,“我知道什么是闭包,可是闭包是做什么的呢?”。有这个问题的人应该不在少数。

  • 闭包的两个应用场景
    • 模块化
    • 防止变量被破坏

防止变量被破坏的意思是,一个变量需要被外部访问并且修改,但是修改的方式需要由函数内部来确定,外部调用暴露的接口即可。
模块化是闭包的主要应用场景。许多模块的封装都是用的闭包来实现的,可以简单参考《闭包简单应用实例》(哈哈,还没有写好,预计这两天就会完成)

猜你喜欢

转载自blog.csdn.net/weixin_42397257/article/details/87895509