js闭包问题

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的属性值。

image.png

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,013评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,205评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,370评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,168评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,153评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,954评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,271评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,916评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,382评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,877评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,989评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,624评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,209评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,199评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,418评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,401评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,700评论 2 345

推荐阅读更多精彩内容