javascript 闭包的概念,闭包的作用,闭包经典面试题详解(配图解)

函数作用域(闭包前置知识)

要彻底弄懂 闭包,必须先理解 JS的 变量作用域,变量分为: 全局变量局部变量,JS的特殊之处在于:每个函数都会创建一个新的作用域,函数内部可以读取函数外部的变量,相反 函数外部无法读取内部变量。

  var a = 123;
     function fun(){
         console.log(a);
     }
    fun();    // 输出123,函数内部的a在自身找不到,会跳到外部全局中去找。

    function foo(){
        var b = 345;
    }
    console.log(b);   
     // error, 全局作用域中并无 b 这个变量,b只产生于 foo 这个函数作用域中。

闭包的理解

所谓“闭包”,指的是一个父函数嵌套着另一个子函数,那么这个子函数也是这个父函数表达式的一部分,可以理解为嵌套函数和函数作用域链。
正常来说,一个嵌套函数,内部的子函数不能被外部作用域引用,但如果,把这个子函数作为一个返回值传给父函数,那么全局作用域就能执行这个子函数内的结果了。
闭包的用途
相同函数可以用多个相互独立的对象引用,避免代码冗余、相互污染。
否则同时调用一个函数,不仅会造成后面的函数不能正常使用,如果改动了全局变量,还会对前面正常的函数产生影响、造成污染。因此我们需要用到闭包。而且可以使代码简单化,体积小。
如何产生闭包?
在嵌套内部函数定于并引用父函数的属性时,就产生了闭包。
产生闭包必须要有嵌套函数,以及子函数引用父函数的属性或者变量
才能产生闭包。

  function fn1(){
        var a = 2;            // 从第二行,到第五行结束,在这个定义过程,结束时经产生了闭包
        function fn2(){
            cosnole.log(++a);
        }                        // 定义结束,产生闭包
        return fn2;
    }
    var abc = fn1();
    abc();

闭包的死亡时机:
在嵌套的内部函数成为垃圾对象时,闭包就死亡了,一般嵌套的内部函数之所以会变成垃圾对象,极大可能是程序猿手动把这个变量或者对象设置为null,这样这个对象就变成垃圾对象了,被浏览器执行回收。

闭包语法:

执行普通的嵌套函数的语法,是这这样的:

function fun(){
    var a = 100;
      function fn(){
          console.log(++a);
      }
    fn();
}
fun();    // 101
fun();    // 101
fun();    // 101

在这个例子中,如果想在全局作用域下,调用这个 fn()内部的函数执行结果,是调用不出来的,因为他是个局部作用域,只能在本函数体内调用执行,调完即释放内存。
由于这个例子的 fn() 没有被持久化(每调完一次结束后释放内存),调用时只能直接调用外层函数 fun() 。此时内层的 fn() 每次被调用后就结束了生命周期,
相关的作用域也被销毁,因此 var a = 100 这个变量值也不会被保留引用,这样每次调用 fun() 时,都只会输出 101 。

执行闭包的写法

 function fun(){
    var a = 20;
       return function fn(){    // 直接将执行结果返回给 fun,这个 fn就是闭包了。
            console.log(++a);
    }
}
var fns = fun();    // 接收这个返回结果
fns()    // 输出21;
fns()    // 输出22;
fns()    // 输出23;

当 fn() 调用了上层函数内的 a 时,就已经产生了这个闭包行为了。 当每次执行 fn() 时,这个内存并不会被释放(因为它在调用外层的a,形成了闭包的依赖关系),
所以在 fn() 内的 a 的值内存,不会被释放,而是被保存下来。
所以每次调用这个返回值时,它总是以累加的形式输出,并不会被释放掉,生命周期比较长。
闭包实际上就是以某种方式持久化,并保留了对外层自由变量的引用的函数。
JS 中,通常一个函数执行完后,内部的整个作用域都会被销毁,被JS引擎的垃圾回收器回收,但闭包的出现阻止了这件事,上个例子中函数 fun() 的作用域就不会销毁,
因为它内部的作用域依然还存在,原来是本身在使用变量 a 的引用,而 fn 在 fun() 的作用域之外被执行,当每次调用 fns() 时,便又访问到函数 fun() 内部的变量 a() 。
简而言之,这个例子中的函数 fn() 就是一个闭包。

各种专业文献上的”闭包“(closure)定义的非常抽象,晦涩难懂。我的理解是,闭包就是能够读取其他函数内部变量的函数。由于在 Javascript 语言中,只有函数内部的子函数才能读取该函数的局部变量,因此可以把闭包简单理解成 “定义在一个函数内部的函数”。在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。闭包可以用在许多地方。它的最大用处有两个,一个是前面提到的可以在函数外部读取内部的变量,另一个就是让这些变量的值始终保持在内存中。

闭包的现实中的应用
定义JS模块
具有特定功能的JS文件
将所有的数据和功能都封装在一个函数内部(私有)
只向外部暴露一个包 n个方法的对象或者函数
模块的使用者,只需要通过模块暴露的对象,调用这个方法来实现对应的功能

方法一:

function fun(){    // JS 模块,并且有特定的功能,转换大小写
        var msg = "my name is a juzheng";    // 私有属性

        // 操作数据的函数
        function f1(){
            console.log("输出小写" + msg.toLowercase());    // 调用了上层的局部变量属性
        }
        function f2(){
            console.log("输出大写"+ msg.toUpperCase());    
        }

        // 向全局暴露两个对象,需要一个对象容器来保存
        return {
            one:f1,
            two:f2
        }
    }
    
// 接收返回值
    var abc = fun();    // 通过暴露对象,接收数据
        abc.one();    // 接收容器的对象

方法二:

(function fun(window){    // JS模块,匿名函数自调用, 并且具有转换大小写功能
        var msg = "my name is a juzheng";
        
        function f1(){
            console.log("输出小写" + msg.toLowerCase());    //调用上层变量属性
        }
        function f2(){
            console.log("输出大写" + msg.toUpperCase());    
        }

        window.myModel = {    将容器对象,转为 window全局对象,将容器对象暴露出来,
            one:f1,
            two.f2
        }

     })(window)    // 推荐函数自调用时,形参和实参写上 window, 这样可以实现压缩代码

// 调用暴露对象
    myModel.one();
    myModel.two();    
// 不需要用一个变量来接收,因为不是用return方法,而是在闭包中用window属性将暴露对象给定义好了,
所以在全局作用域下,只需要调用即可。

闭包的缺陷:

函数执行完后,函数内的局部变量没有释放,占用内存时间变长,容易造成内存泄漏
解决方法:
让内部函数变成垃圾对象,赋值为null,及时释放,让浏览器回收闭包。
内存溢出
一种程序运行出现的错误,当程序运行需要的内存超出了剩余内存时,就抛出了内存溢出的错误,浏览器直接奔溃。

var obj = {};
for(var i = 0; i<1000;i++){
    obj[i]  =  new Array(10000000)    // obj是伪数组,有下标
}	// 一直输出,占用浏览器大量内存,导致浏览器原地奔溃

在这里插入图片描述
内存泄漏
占用的内存没有及时释放,内存泄漏积累多了容易导致内存溢出
常见的内存泄漏:
意外的全局变量,没有及时清理的计时器或者回调函数,闭包。

1:全局变量引起的内存泄漏

function fn(){
    a = 10;     //a 是全局变量,当函数执行完后并不会自动释放内存
    console.log(a);
}
fn();
console.log(a);

2:计时器未结束引起的内存泄漏

setInterval(function fn(){        // 计时器没有定义结束条件,会一直无限执行
    console.log("aaaaaa");
},1000);

// 正确做法
var stop = setInterval(function fn(){       
    console.log("aaaaaa");
},1000);

clearInterval(stop);    //结束定时器

3:闭包引起的内存泄漏

function fun(){
    var a = 10;
    function f1(){
        console.log(++a);
    }
    return f1;
}
var f = fun();
f();
// 由于闭包是会一直引用着函数内的局部变量,所以闭包内的变量
并不会被浏览器所释放,导致内存一直被占用着

闭包的两道经典题

题目一:

var name = "the window";
var obj = {                    
    name : "myObject",          // 属性
    getNameFunc:function(){     // 方法
        return function(){
            return this.name;
        };
    }
};
alert(obj.getNameFunc()());        // 输出了 “the window”;

答案: 输出了全局 the window,为什么不是对象属性 myObject 呢?
解法: 形成一个闭包环境,需要两个条件:函数嵌套,子函数引用父函数局部变量,但是上面的例子,我只看到了函数嵌套,但没看到子函数引用父函数局部变量,所以这个函数嵌套根本就产生不了闭包环境,所以他不是个闭包。

调用 alert(obj.getNameFunc()()),注意,他有两个 () () ,调用第一个 () 时,实际上就是执行了getNameFunc()这个函数,但这个函数又嵌套了 return function(){ return this. name },

所以执行第一个括号时,就是执行了 return function(){ return this. name },到执行第二个 () 时,才是真正执行到了第二个函数的内部了,也就是 return this.name 了,但由于这个函数方法,他不是个闭包环境,所以 this 指向了 window 了,也就是 var name = “the winodw” ;
在这里插入图片描述
图解程序执行顺序

题目二:

var name2 = "the window";
var obj2 = {                            
    name2:"my name is a obj",
    getNameFunc2 :function fn1(){
        var that = this;    
        return function(){
            return that.name2;  
        };
    }
}
alert(obj2.getNameFunc2()());    //输出 my name is a obj

这次是输出的是 对象内的属性了,因为这次的才是真正的闭包环境,有函数嵌套,有父函数变量给子函数引用,形成依赖关系,产生闭包环境。

父函数明确定义了局部变量,var that = this;然后这个 that 呢,他又被子函数所引用,当 alert(obj2.getNameFunc()()) 执行时,就是执行子函数内部的 return that.name
先在自身查找有没有 that.name,没有就返回上一层父函数,找到了 var that = this 了,这个就是闭包的环境。当然,两个函数执行域内都没有这个变量属性时,再往上一层,
也就是obj2对象内的属性,有个 name,终于在对象的执行域找到了,那就输出了 对象name的属性值。
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值