JavaScript--有哪些情况会产生闭包?

说起闭包,总是让人觉得陌生又熟悉,听起来似乎并不是那么难,却又好像从来没有知道哪些地方会永=用到闭包。我们来归纳一下,有什么情况下会是闭包。

闭包的概念

在你不知道的Javascript上卷一书中,闭包的定义是这样的:当函数可以记住并访问所在的词法作用域,即使的函数是当前词法作用域之外执行,这时就产生了闭包。怎么理解这句话呢?就是说当函数内部的东西,能在函数外面执行的时候,这就产生了闭包。是不是很简单?那么哪些情况会产生闭包呢?

产生闭包的情况

(一)回调闭包

function wait (message) {
    setTimeout(function timer () {
        console.log(message)
    }, 1000)
}

我们可以看到setTimeout定时器中有个函数叫timer,这个函数就是一个回调函数,我们可以看到,timer具有对wait函数作用域的全覆盖。也就是说,timer 具有涵盖 wait(…) 作用域的闭包,因此还保有对变量 message 的引用。当wait被执行1000毫秒之后,它的内部作用域并不会消失,timer 函数依然保有 wait(…)作用域的闭包。所以说,本质上无论何时何地,如果将函数(访问它们各自的词法作用域)当作第一 级的值类型并到处传递,你就会看到闭包在这些函数中的应用。在定时器、事件监听器、 Ajax 请求、跨窗口通信、Web Workers 或者任何其他的异步(或者同步)任务中,只要使 用了回调函数,实际上就是在使用闭包!。

(二)自调函数(IIFE模式)

var a = 2
(function IIFE () {
    console.log(a) //   2
})()

我们再来看下闭包的定义,函数可以记住并且访问当前词法作用域,使得函数是在当前词法作用域之外执行,这时会产生闭包。所以,IIFE模式看起来并不闭包的一种,因为函数并不是在他定义的作用域之外执行,变量a是普通的词法作用域查找得到的,并不是通过闭包找到的。但是需要注意的是,它的确创建了闭包,并且也是最常用来创建可以被封闭起来的闭包的工具。因此 IIFE 的确同闭包息息相关,即使本身并不会真的使用。

(三)循环闭包

循环闭包,最常见的就是for循环。看下面的代码(我们期待最后输出的结果是1,2,3,4,5):

for (var i = 1; i <= 5;i++) {
    setTimeout(function(){
        console.log(i) //   6(5次)
    }, i*1000)
}

先来解释一下,6是怎么来的。我们可以看到循环的终止条件是 i 不再 <=5。条件首次成立时 i 的值是 6。因此,输出显示的是循环结束时 i 的最终值。当定时器运行时即使每个迭代中执行的是setTimeout(…, 0),所有的回调函数依然是在循环结束后才会被执行,因此会每次输出一个 6 出来。根据作用域的工作原理,实际情况是尽管循环中的五个函数是在各个迭代中分别定义的, 但是它们都被封闭在一个共享的全局作用域中,因此实际上只有一个 i。
我们来看IIFE模式:

for(var i = 1;i < 5;i++) {
    (setTimeout(function () {
        console.log(i) //   6 (5次)
    }, 1000))()
}

这样不行。但是为什么呢?我们现在显然拥有更多的词法作用域了。的确每个延迟函数都会将IIFE 在每次迭代中创建的作用域封闭起来。如果作用域是空的,那么仅仅将它们进行封闭是不够的。仔细看一下,我们的 IIFE 只是一 个什么都没有的空作用域。它需要包含一点实质内容才能为我们所用。

for (var i=1; i<=5; i++) {
    (function(j) {
        setTimeout( function timer() {
            console.log( j );
        }, j*1000 );
    })( i );
}

ok,这样就完美了。这些 IIFE 也不过就是函数,因此我们可以将 i 传递进去,在迭代内使用 IIFE 会为每个迭代都生成一个新的作用域,使得延迟函数的回调可以将新的作用域封闭在每个迭代内部,每个迭代中都会含有一个具有正确值的变量供我们访问。

(四)块作用域闭包

是的,你想的没错,就是let.let将一个块转换成一个可以被关闭的作用域看代码:

for (var i=1; i<=5; i++) {
    let j = i; // 是的,闭包的块作用域!
    setTimeout(function timer() {
        console.log( j ); 
    }, j*1000 );
}

这还不够完美。for 循环头部的 let 声明还会有一 个特殊的行为。这个行为指出变量在循环过程中不止被声明一次,每次迭代都会声明。随 后的每个迭代都会使用上一个迭代结束时的值来初始化这个变量。

for (let i=1; i<=5; i++) {
    setTimeout( function timer() {
        console.log( i );
    }, i*1000 );
}

这样就完美多了。

(五)模块闭包

不知道你有没有想过,模块的实现竟然能和闭包联系在一起吗?但是是肯定的。看下面的代码:

function myModule () {
    var name = '小明'
    var age = 23
    function getName () {
        console.log(name)
    }
    function getAge () {
        console.log(age)
    }
    return {
        getName,
        getAge
    }
}
let foo = myModule()
foo.getName() //    小明
foo.getAge() // 23

这个模式在 JavaScript 中被称为模块。最常见的实现模块模式的方法通常被称为模块暴露.

首先,myModule() 只是一个函数,必须要通过调用它来创建一个模块实例。如果不执行 外部函数,内部作用域和闭包都无法被创建。
其次,myModule() 返回一个用对象字面量语法 { key: value, … } 来表示的对象。这个返回的对象中含有对内部函数而不是内部数据变量的引用。我们保持内部数据变量是隐 藏且私有的状态。可以将这个对象类型的返回值看作本质上是模块的公共 API。
getName() 和 getAge() 函数具有涵盖模块实例内部作用域的闭包(通过调用 myModule() 实现)。当通过返回一个含有属性引用的对象的方式来将函数传递到词法作 用域外部时,我们已经创造了可以观察和实践闭包的条件。
模块模式需要具备两个必要条件:

必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块 实例)。
封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并 且可以访问或者修改私有的状态。

(六) ES6模块闭包

ES6 中为模块增加了一级语法支持。但通过模块系统进行加载时,ES6 会将文件当作独立 的模块来处理。每个模块都可以导入其他模块或特定的 API 成员,同样也可以导出自己的 API 成员。

bar.js
    function hello(who) {
        return "Let me introduce: " + who;
    }
    export hello;

foo.js
    // 仅从 "bar" 模块导入 hello() import hello from "bar";
    var hungry = "hippo";
    function awesome() { console.log(
        hello( hungry ).toUpperCase() );
    }
    export awesome;

baz.js
    // 导入完整的 "foo" 和 "bar" 模块
    module foo from "foo"; module bar from "bar";
    console.log(
        bar.hello( "rhino" )
    );  // Let me introduce:
    rhino foo.awesome(); // LET ME INTRODUCE: HIPPO

import 可以将一个模块中的一个或多个 API 导入到当前作用域中,并分别绑定在一个变量 上(在我们的例子里是 hello)。module 会将整个模块的 API 导入并绑定到一个变量上(在 我们的例子里是 foo 和 bar)。export 会将当前模块的一个标识符(变量、函数)导出为公 共 API。这些操作可以在模块定义中根据需要使用任意多次。模块文件中的内容会被当作好像包含在作用域闭包中一样来处理,就和前面介绍的函数闭 包模块一样。

尽管在这么多的场景中会出现闭包,所以我们一定要多多注意。

使用闭包注意点

1)由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
2)闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

leoxiaoge

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值