轻松理解JS中,闭包的基本使用和装饰模式的实现

作者 : 开心源码 本文共4609个字,预计阅读时间需要12分钟 发布时间: 2022-05-13 共206人阅读

很多人学JS刚学到闭包的时候会比较懵,特别是从强类型语言(如Java)转而学JS的人,更是觉得这都啥跟啥呀。本文也就只针对这些刚学的新手,所以不会去谈闭包的原理,只谈闭包的基本使用,新手可以放心食用。只有在知道如何使用之后,你再深入理解就会得心应手,在用都不知道用的情况下就想对一个知识点理解的很透彻,这是不可能的。

理解闭包的使用之前,先得捋清一下少量基本的知识点,咱们一个知识点一个知识点慢慢往下捋,到最后你就会发现你已经知道如何使用闭包了。

思路梳理

JS中,函数内公告的变量其作用域为整个函数体,在函数体外不可引用该变量,听起来很玄乎,一看代码大家就很清楚了:

function outter() {    // 在函数内部公告一个变量    let x = 3;    // 在函数体内使用这个变量,这个一定没有什么问题    console.log('我在本函数里使用变量:' + x);}outter(); // 输出内容:我在本函数里使用变量:3console.log(x); // 报错,由于函数外部这样拿不到函数内部的变量

这个知识点相信大家都可以了解,我在这里说的再通俗少量,函数内部公告的变量,就只能在公告该变量的大括号内使用,大括号外就用不了。所以看函数内的变量作用域,直接找大括号就好了。

我们再继续前进,因为JS的函数可以嵌套,此时内部函数可以访问外部函数定义的变量,反过来则不行:

// 外部函数function outter() {    let x = 3;    // 内部函数    function inner() {        let y = x + 1; // 内部函数可以访问外部函数的变量x    }    let z = y + 1; // 报错,外部函数访问不了内部函数的变量y}

这一点也很好了解,和前面一个知识点是完全一致的,内部函数inner()由于在外部函数outter()的大括号内,当然即可以使用变量x,而外部函数outter()在内部函数inner()的大括号外面,自然就用不了变量y。

理解上面的基本知识点后即可以开始理解闭包了。假设现在我们有一个需求,我就是想在outter()外面拿到变量x怎样办? 好办呀,直接在outter()里将x当做返回值返回就好了:

function outter() {    let x = 3;    return x;}let y = outter(); // 3

OK,这样是拿到了变量x,但是,严格的来说这只是拿到了变量的值,并没有拿到变量。啥意思呢,就是说你无法对变量x的值进行修改,假如我想将变量x的值自增1呢?你是无法修改的,你就算修改变量y的值,x的值也不会被改变:

function outter() {    let x = 3;    return x;}let y = outter(); // 3y++;console.log(y); // 4, y的值的确被修改了console.log(outter()); // 3, 函数内部x并没有被修改

有可能你会想到,那我在函数内部将x自增,而后再返回不即可以了?

function outter() {    let x = 3;    x++;    return x;}console.log(outter()); // 4

OK,没问题,但是我想每次调用函数的时候,x都会自增,就像一个计数器一样,x的值会随着我的调用次数动态添加。我们可以按照上面的代码来演示一下看是否达到要求:

function outter() {    let x = 3;    x++;    return x;}console.log(outter()); // 4console.log(outter()); // 4, 但我想要的是5console.log(outter()); // 4, 但我想要的是6

会发现每次调用都是4,由于当你调用outter()的时候,x在最开始都会被重新赋值为3而后自增,所以每次拿到的值都是固定的,并不会动态添加。那这时该咋办呢? 这里闭包就能派上用场了!

闭包的最基本演示

还记得之前所说的吗,内部函数可以调用外部函数内公告的变量,我们先看一下在内部函数操作一番后,我们是否拿到x的值

// 外部函数function outter() {    let x = 3;    // 内部函数    function inner() {        // 在内部函数操作x        x++;    }    // 调用一次内部函数,将x进行升级    inner();    // 最后将x进行返回    return x;}console.log(outter()); // 4console.log(outter()); // 4console.log(outter()); // 4

这样是可以取得x的值,但这样还是达不到我们计数器的要求,由于每次调用outter()时,x的值都会被重新赋值为3。 我们应当绕过outter()函数重新赋值的步骤,只要要取得x自增的操作即可以了。 怎样只获取自增的操作呢,现在自增的操作是在内部函数inner()里,我们是否只拿到内部函数?当然可以啦!!

JS是一个弱类型语言,并且支持高级函数。就是说,JS里函数也可以作为一个变量来进行操作!我们在外部函数outter()里将内部函数作为变量进行返回,即可以拿到内部函数了 。接下来要仔细了解代码,这种操作就是闭包:

// 外部函数function outter() {    let x = 3;    // 内部函数    function inner() {        // 在内部函数里操作x        x++;        // 每次调用内部函数的时候,会返回x的值        return x;    }    // 将inner()函数作为变量返回,这样当别人调用outter()时即可以拿到inner()函数了    return inner;}let fun = outter(); // 此时拿到是函数inner(),就是说fun此时是一个函数// 我们接下来调用fun函数(就等于在调用inner函数)console.log(fun()); // 4console.log(fun()); // 5console.log(fun()); // 6

可以看到上面代码完美拿到了内部函数inner(),并实现了需求。内部函数对外部函数的变量(环境)进行了操作,而后外部函数将内部函数作为返回值进行返回,这就是闭包。上面代码的思路步骤就是:

调用外部函数outter() —> 拿到内部函数inner() —> 调用inner()函数 — > 成功对outter()函数内的变量进行了操作。

看到这有人可能会说,我为啥要多一节步骤,要先拿到内部函数,再对变量进行操作啊?不能直接在外部函数里对变量进行操作,省了中间两个步骤吗? 哥,之前不是演示了吗,假如直接从外部函数操作,变量值是“死”的,你是无法实现动态操作变量的。 由于外部函数每次调用完毕后,会销毁变量,假如再重新调用则会重新为变量开拓空间并重新赋值。内部函数的话则会将外部函数的变量存放到内存中,从而实现动态操作

通过闭包实现装饰模式

上面演示的是内部函数可以操作外部函数的变量,其实不仅仅是某个变量这么简单,内部函数可以操作外部函数所拥有的环境,并可以携带整个外部函数的环境。这句话假如不能了解也完全没关系,丝毫不影响你平时使用闭包,使用的多了自然而然就会明了。我们接下来继续演示闭包,更加加深了解:

现在我有一个需求,我想让少量函数运行的同时并打印日志。这个打印日志的操作并不属于函数本身的逻辑,需要剥离开来额外实现,这种“扩展功能”的需求就是典型的装饰模式。我们先来看一下普通的做法是怎么的:

function fun() {    console.log('fun函数的操作');}fun(); // fun函数的操作

我们要对fun()函数进行扩展功能,最直接的办法当然是修改fun()函数的源代码咯:

function fun() {    console.log('额外功能:在运行函数前打印日志');    console.log('fun函数的操作'); // fun()函数本身的功能    console.log('额外功能:在运行函数后打印日志');}

这样是达到了需求,但是假如我有几十个函数需要扩展功能呢,岂不是要修改几十次函数源代码?上面只是为了做演示,将扩展功能写的很简单只有两句代码,可往往很多扩展功能可不止几行代码那么简单。况且,很多时候就不允许你修改函数的源代码!所以上面这种做法,是完全不行的。

这时候,我们即可以用到闭包来实现了。函数可以当做变量并进行返回,那么函数自然也可以当做变量作为参数进行传递。这就非常非常灵活、方便了。我将需要扩展的函数当做参数传递进来,而后在我的函数里进行额外的操作即可以了

// 需要被扩展的函数function fun() {    console.log('fun函数的操作');}// 闭包的外部函数,需要接收一个是函数的参数function outter(f) {    // 此时f就是外部函数的一个成员变量,内部函数理所应当的可以操作这个变量    function inner() {        console.log('额外功能:在运行函数前打印日志');        f(); // 调用外部函数的变量f,也就是说调用需要被扩展的函数        console.log('额外功能:在运行函数后打印日志');    }    // 外部函数最后将内部函数inner返回出去    return inner;}// 调用外部函数,并传递参数进去. 这样即可以拿到已经扩展后的函数:inner()let f = outter(fun);f(); // 此时f函数已经将原来的fun()函数功能扩展了,就相当于是inner()// 一般装饰模式都是将原函数给覆盖:fun = outter(fun);fun(); // 此时再调用原函数的话,其实就是在调用inner(),是包含了扩展功能的/*输出内容:额外功能:在运行函数前打印日志fun函数的操作额外功能:在运行函数后打印日志*/

通过闭包就完美实现了装饰模式,假如还有其余函数需要扩展的话,直接调用outter()函数就可,简单方便。假如上面这个操作看不明白,千万不要想复杂了,第一个闭包演示是操作变量x,这个闭包演示也是操作变量,只不过这个变量f是一个函数罢了。本质没有任何区别。

闭包的总结

现在我们来对闭包进行总结一下,原理方面就不谈了,就只谈使用。

使用的思路是

调用外部函数outter() —> 拿到内部函数inner() —> 调用inner()函数 — > 成功对outter()函数内的变量(环境)进行了操作。

闭包是啥呢 ?就是将内部函数作为返回值返回,内部函数则对外部函数的变量(环境)进行操作。

为啥要通过内部函数这一步骤呢?由于内部函数可以将外部函数的环境存放到内存里,从而实现提供了更为灵活、方便的操作。

闭包的使用不难,当你使用熟练之后,再去理解背后原理就会非常轻松了。

小扩展

本文只终于讲解闭包的基本使用,其余略微深一点的东西就不讲了,有兴趣的可以去扩展一下:

  1. 在面向对象(OOP)的设计模式中,比方Java,装饰模式是需要通过继承和组合来实现,装饰者和被装饰者必需都继承了同一个笼统组件。 而JS中,则通过闭包非常灵活的实现了装饰模式,任何函数都可以被装饰从而扩展功能。不过这还不算最方便,在python里直接是从语法层面提供了装饰模式,即装饰器。 JS在ES6也通过语法层面实现了装饰器,不过和python的有些不太一样,有兴趣的可以去理解一下。
  2. 内部函数可以操作外部函数的变量,上面样式的那些变量都是固定的值,假如变量是一个引用的值(比方引用了外面的一个数组,在外部函数的外面也可以直接对数组进行修改),会产生什么后果。
  3. 运用闭包的好处上面已经演示了,那闭包的坏处是什么?提醒:内存
说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » 轻松理解JS中,闭包的基本使用和装饰模式的实现

发表回复