Javascript之闭包理解、使用场景和问题(附示例代码)

这篇文章仔细整理和总结下我对闭包的理解。

一、先从一道题来看

var test = (function(i){
  return function(){
    console.log(i * 2);
  }
})(2);
// console.log(test);
test();
test(10);

不用着急看答案,可以先说一下自己的结果,再看看对不对。

正确的结果应该是:

test();         // 结果是4
test(10);       // 结果是4

这里可以尝试输出一下test函数:

ƒ (){
  console.log(i * 2);
}

那为什么test(10)的执行结果会是4,这里test无论给它传什么参数都会输出4?这就是闭包。

二、闭包是什么

那么在上面的题里我们可以把内部返回的这个函数就看作是一个闭包函数,简单点说,闭包就是能够访问其他函数作用域内变量的函数。在上一题里内部被返回的这个函数可以访问到定义它的父函数内的变量i,所以它是一个闭包函数。

大家都知道在JavaScript里面有函数作用域的概念,也就是函数外部无法访问到函数内部的变量,但是函数可以顺着作用域链访问它到外部的变量。

举一个最简单的例子:

var i = 2;
function func(){
  var j = 3;
  console.log(i);     // 输出2
}
func()
console.log(j);       // 报错

这里的函数func可以访问到位于它外部的变量i,但是函数外部也就是全局执行环境无法访问到函数func的变量j。

那么想要访问一个函数A内部的变量要怎么做呢?我们可以利用闭包的特性在这个函数A内部定义一个函数B,那么这个函数B就会沿着作用域链访问到函数A的变量,即使函数B被return在全局作用域下执行。

根据这个原理,我们改造一下上面的代码使之成为一个闭包函数,实现在全局作用域内可以访问到函数func内部的变量j:

function func(){
  var j = 3;
  return function in_func(){
    console.log(j);
  }
}
out_func = func();
console.log(out_func);
out_func();		// 3

到这里应该就解释清楚了闭包是什么,以及闭包为什么可以访问到其他函数作用域内的变量。

三、使用场景

既然知道了闭包是什么,那接下来我们就想知道,为什么要去访问其他函数内部的变量?闭包有哪些具体的使用场景?

1、封装私有变量

可以通过闭包来封装私有变量,并且通过一些公用方法来访问和修改私有变量。示例如下:

function test () {
    var value = 1;
    return {
        get : function () {
            return value;
        },
        add : function () {
            value += 1;
        },
        reduce : function () {
            value -= 1;
        }
    }
}

a = test();

console.log(a.get());       // 1
a.add()
console.log(a.get());       // 2
a.reduce();
console.log(a.get());       // 1

上面的例子里我们通过公有方法get来访问私有变量value的值,通过方法add和reduce来修改私有变量value的值。

2、为节点循环绑定事件

示例代码后补。

3、setTimeOut循环输出

for(var i = 0; i < 10; i ++){
    setTimeout(function(){
        console.log(i);
    }, 1000)
}

这里的例子是一道经典的面试题,本来是想每隔1秒输出一个数,最后输出0~9,结果代码的执行结果是输出10个10。
这里是因为setTimeout是一个异步任务,这10个异步任务会被放在宏观任务的执行队列中,等待当前执行栈(也就是全局执行环境)空的时候再去宏观任务的执行队列中依次取任务执行,这时宏观任务也就是这里的console.log(i);中的i保持的是对同一个for循环中i的的引用,这时for循环已经执行结束,i的值已经变为10,所以这10个宏观任务中i的值也全为10。(这部分的解释涉及到了js中的事件机制,这部分也是可以单独作为一个知识点写一篇博客的)。

要想使得上面代码可以按照预期结果输出,有很多方法。
1)使用闭包

for(var i = 0; i < 10; i ++){
    (function(i){
        setTimeout(function(){
        console.log(i);
    }, 1000)})(i)
}

这里使用闭包在setTimeout的外层包裹一层函数并马上执行,这样就使得10个异步任务有了10个包裹自己的执行环境,setTimeout内部分别保持着对外层函数中i的引用,这里i的值是执行for循环时传入的当前i的值。

2)使用setTimeout的第三个参数

for(var i = 0; i < 10; i ++){
    setTimeout(function(i){
        console.log(i);
    }, 1000, i)
}

setTimeout的第三个参数以及后面的参数会作为第一个函数的参数传进去。

  1. 使用let
for(let i = 0; i < 10; i ++){
    setTimeout(function(){
        console.log(i);
    }, 1000)
}

这里的let是ES6新增,具有块级作用域。

四、闭包的问题

前面提到了使用闭包会使函数内部的局部变量一直存在于内存中,增加内存开销,因此滥用闭包可能会降低网页的性能。所以我们要在变量使用结束以后释放它。在前面的例子中:

function func(){
    var j = 3;
    return function in_func(){
        console.log(j);
    }
}
out_func = func();
console.log(out_func);
out_func();		   		  // 3
  
out_func = null;          // 释放对变量的引用

五、跟匿名函数的区别

因为闭包常常会以匿名函数立即执行的方式实现,因此常常会有人把闭包和匿名函数混淆,其实这两者天差地别。匿名函数只是定义了一个没有函数名的函数,而闭包的特性在于从作用域的角度延长了变量在内存中的生存时间。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值