JavaScript闭包理解

本文深入探讨了JavaScript中的闭包概念,包括变量作用域、闭包的形成及其在函数返回、作为参数、IIFE、定时器和循环等经典场景中的应用。通过实例分析,展示了闭包如何保留对外部作用域的引用,以及如何防止内存泄漏。同时,文章提到了闭包在实际使用中需要注意的内存消耗问题。最后,通过两个思考题进一步巩固了闭包的理解。
摘要由CSDN通过智能技术生成

目录

什么是闭包?

变量的作用域

初识闭包

分析闭包经典使用场景

1. 使用return 返回函数

知识拓展--简化

2. 函数作为参数

3.IIFE(自执行函数)

4. 定时器setTimeout(回调函数都是闭包)

知识拓展-回调函数

5.循环和闭包

思考题

思考题1

思考题1解答

思考题2

思考题2解答

使用闭包注意点

参考


我们学习闭包之前先了解作用域和作用域链的相关知识 

 

什么是闭包?

闭包:闭包就是有权访问另一个函数作用域变量的函数

闭包简单定义为在一个函数内部的函数

形成闭包的原因:存在上级作用域的引用

 

阮一峰的学习JavaScript闭包(Closure)

初识闭包

    function foo(){
        var a = 2;
        function bar(){
            console.log(a);
        }
        return bar;
    }
    var baz=foo();
    baz();//输出 2
    
复制代码

bar()词法作用域能访问foo()的内部作用域,将bar()函数对象本身当作返回值。foo()执行后,其返回值(也就是内部的bar()函数)赋值引用给变量baz并调用baz(),实际上只是通过不同的标识符调用了内部的函数bar().在foo()执行后,通常会期待foo()的整个内部作用域都被销毁,因为我们知道引擎有垃圾回收器用来释放不在使用的内存空间.由于看上去foo()内容不会再被使用,所以很自然地会考虑对其进行回收。

闭包的神奇之处正是可以阻止这件事情发生,事实上内部作用域依然存在,因此没有被回收。谁在使用这个内部作用域呢?原来是bar()本身在使用。bar()拥有涵盖foo()内部作用域的闭包,使得该作用域能够一直存活,以供bar()在之后任何时间进行引用。bar()依然持有对该作用域的引用,而这个引用就叫做闭包。

分析闭包经典使用场景

1. 使用return 返回函数

function foo(){
    var a = 2;
    function bar(){
         console.log(a);
    }
    return bar;
}
var baz = foo();
baz(); //输出为2 
复制代码

其实可以这样写,如下:

function foo(){
    var a = 2;
    function bar(){
       console.log(a);
    }
    return bar;
}
foo()();//输出为2
复制代码

bar函数是一个闭包,它在foo函数内部定义的函数

知识拓展--简化

  • f()执行f函数,返回子函数
  • f()()执行子函数,返回孙函数
  • f()()()执行孙函数,返回重孙函数

注意:但注意,如果想这样执行,函数结构必须是这样,f的函数体里要return 子函数,子函数里要return 孙函数,如果没有return关键字,是不能这样连续执行的,会报错的。

    function fun(){
        return 5 //子函数
    }
    var a=fun
    var b=fun()
    console.log(a); //[Function:fun]
    console.log(b);// 输出为5
复制代码
    function fun(){
        return k;
        function k(){
           return '555 '
        }

    }
    var a=fun;
    var b=fun();
    var kk=fun()();
    console.log(a); //[Function:fun]
    console.log(b);// [Function:k]
    console.log(kk); //555   
复制代码

函数只要是要调用它进行执行的,都必须加括号。此时,函数实际上等于函数的返回值或者执行效果,当然,有些没有返回值,但已经执行了函数体内的行为,就是说,加括号的,就代表将会执行函数体代码。

2. 函数作为参数

var a = '函数外'
function fo(){
    var a = ' fo 函数内'
    function foo(){
        console.log(a)
    }
    return foo
}

function f(p){
    var a = 'f 函数内'
    p()
}
f(fo())//输出为fo函数内 
复制代码

使用retuen foo 返回, foo()是一个闭包,f(fo())执行的参数就是函数foo,因为foo() console.log(a) 中的a的上级作用域是函数fo,所以输出的是fo函数内部定义的a的内容fo 函数内

思考:如果将上面的f()函数改一下你是否能理解?如果你能理解那么上面的补充知识你也懂了。

var a = '函数外'
function fo(){
    var a = ' fo 函数内'
    function foo(){
        console.log(a)
    }
    return foo
}

function f(p){
    var a = 'f 函数内'
    p()()
}
f(fo)//输出为fo函数内 
复制代码

3.IIFE(自执行函数)

    var a = 2;
    (function IIFE(){
        console.log(a);
    })() //输出为 2
复制代码

这样产生的是闭包IIFE(),但是严格来讲它并不是闭包,为什么?因为IIFE()并不是在它本身的词法作用域以外执行的。自执行函数本身是没有变量作用域的,因此会使用外层函数的变量作用域。所以输出为2.尽管IIFE本身并不是观察闭包的恰当例子,但它的确创建了闭包,并且也是最常用来创建可以被封闭起来的闭包的工具。因此IIFE的确同作用域息息相关,即使本身并不会真的创建作用域。

4. 定时器setTimeout(回调函数都是闭包)

    function wait(m){
        setTimeout(function timer(){
            console.log(m);
        },1000) 
    } 
    wait("Hello");//1000毫秒后输出 Hello
复制代码

闭包timer传递给 setTimeout()timer()具有涵盖wait()作用域的闭包,因此保存了对变量m的引用。wait()执行1000毫秒后,它的内部作用域并不会消失,timer()函数依然保有wait()作用域的闭包。

知识拓展-回调函数

一个函数被作为参数传递给另一个函数(在这里我们把另一个函数叫做“otherFunction”),回调函数在otherFunction中被调用。注意回调函数都是闭包

  • 将回调函数的参数作为与回调函数同等级的参数进行传递

22.png

  • 回调函数的参数在调用回调函数内部创建

222.png

5.循环和闭包

思考下面的代码:

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

复制代码

输出的答案是什么呢?1,2,3,4,5?错误正确输出是6,6,6,6,6,6。为什么呢?解析一下6是从哪来的?相信你和我一开始一样都是千百个???,而且循序的终止条件是i不在是<=5。条件首次成立时i的值为6,因此,输出显示的是循环结束i的最终值。延迟函数的回调会在循环结束时才会执行,不管定时器 setTimeout的时间是多少,所有的回调函数依然是在循环结束后才会被执行,因此每一次都是输出一个6来。

想要输出1,2,3,,4,5的结果,那么要如何修改我们的代码呢?

方法一(传参)

    for(var i = 1;i<=5;i++ ){
    (function(j){
           setTimeout(function timer(){
            console.log(j);
            },j*1000)
     })(i);//传入参数 i 
    }
    //输出1,2,3,4,5
复制代码

通过传参的方法,将i传递进去,将变量名取为j 来获取i的值

方法二(定义变量接收i的值)

    for(var i = 1;i<=5;i++ ){
    (function(){
            var j=i;
           setTimeout(function timer(){
            console.log(j);
            },j*1000)
     })();
    }
    //输出1,2,3,4,5
复制代码

通过定义变量的方法,将变量名取为j用来在每个迭代中存储i的值

方法三(将var改为let)

    for(let i = 1;i<=5;i++ ){
    (function(){
           setTimeout(function timer(){
            console.log(i);
            },i*1000)
     })();
    }
    //输出1,2,3,4,5
复制代码

for循环头部使用let声明,let具有块级作用域,let每次迭代都会声明,随后的每个迭代都是使上一个迭代结束时的值来初始化这个变量,迭代变量的作用域仅限于for循环块内部。let可以在任意代码块中隐式的创建或是劫持块作用域var声明的其中块代码的作用域是全局的,所以当执行完循环之后运行setTimeout中闭包之后,其中引用的i就是全局作用域中的i,然而let就不会。

知识拓展-- letvar的区别

   for(let i = 0;i<=5;i++){
        console.log(i);
    }
    console.log(i,'----');
    //输出 i is not defined
复制代码
    for(var i = 0;i<=5;i++){
        console.log(i);
    }
    console.log(i,'++++');
    //输出0,1,2,3,4,5,6 ++++
复制代码

varfor循环头部声明变量i,在for循环结束后i会被暴露在全局作用域中,然而let就不会,因为let可以在任意代码块中隐式的创建或是劫持块作用域

思考题

思考题1

 var name = "The Window";
  var object = {
    name : "My Object",
    getNameFunc : function(){
      return function(){
            //console.log(this); 
        return this.name;  
      };
    }
  };
console.log( object.getNameFunc()());
复制代码

输出的是The Window还是"My Object?首先我们需要了解this.namethis指向的到底是什么?

思考题1解答

this指向的是全局(window)因为上一级getNameFunc()函数没有name 属性 ,因此就去找全局的name属性,所以输出“The Window”

思考题2

  var name = "The Window";
  var object = {
    name : "My Object",
    getNameFunc : function(){
      var that = this;   
      return function(){
        return that.name;
      };
    }
  };
 console.log(object.getNameFunc()());
复制代码

输出的是The Window还是"My Object?首先我们需要了解that = thisthis指向的到底是什么?

思考题2解答

this赋值给一个变量,内部函数是可以访问外部函数变量的.所以this指向的是object,由于this关键字不是在包含的函数中引用的,而是通过that=this这个调用的,所以这个this不是在闭包内的,因此这个this就不能调用函数体内的全局对象,而是他的局部对象object.name,所以输出的是"My Object"

使用闭包注意点

1)由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。

2)闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。

参考

面试 | JS 闭包经典使用场景和含闭包必刷题

阮一峰的学习JavaScript闭包(Closure)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值