作者:大闲人柴毛毛
链接:https://www.zhihu.com/question/34210214/answer/136673471
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
闭包是JS语言的又一大核心,如果要从内存角度充分理解闭包的话,建议大家先预习下先前的几篇讲博客:
稳扎稳打JavaScript(一)——作用域链
稳扎稳打JavaScript(二)——图解对象内存模型
稳扎稳打JavaScript(三)——创建对象的几种方式
什么是闭包定义
闭包是一个能够访问其他函数作用域的函数。
这句话看似拗口,如果读过先前的几篇博客,那理解起来应该不难。下面来解析一下这句话:
* 首先,闭包是一个函数;
* 其次,这个函数不仅能访问自己的作用域,更为关键的是它还能访问其他函数的作用域。
换句话说,如果一个函数能访问其他函数作用域中的变量,那么这个函数就叫做“闭包”。
如何创建闭包?
只要在一个函数中再定义一个函数,这个内部函数就是一个闭包。
注意:只要满足一个函数在另一个函数的内部的条件,这个内部函数就是闭包,不管这个内部函数是以怎样的形式存在于外层函数中的。下面举几个例子:
* 形式一:
function father () {
    function son () {
    }
}
* 形式二:
function father () {
    var object = new Object();
    object.son = function () {
    }
}
* 形式三:
function father () {
    var object = {
        son : function(){}
    };
}
以上三种形式定义的内部函数都是闭包。
* 形式一:在一个函数中又定义了个函数,这完全符合闭包的定义,不用多说。
* 形式二:在一个函数中先创建了一个对象,然后在对象内部定义了函数,这也是闭包。
* 形式三:本质上和形式二一样,也是先在函数内部定义了一个对象,再在对象内部定义了个闭包。只不过定义对象的方式和形式二有所区别。
闭包有何用?
前面闭包的定义中说“闭包能访问其他函数作用域中的变量”。“其他函数”指的是闭包所在的外层函数。
也就是说,闭包能访问它所在外层函数全部变量,即使外层函数已经执行结束。
闭包的这种特性有助于我们在JavaScript这门非面向对象的语言中实现面向对象的一些特性。这个后面会详细介绍。
闭包的原理
闭包的原理涉及到作用域链的内存模型,这里带大家回顾下作用域链,更为详细的内容请看:稳扎稳打JavaScript(一)——作用域链
以下列代码为例:
function father () {
    var name = "爸爸";
    var company = "Google";
    function son () {
        var name = "儿子";
        var school = "启东中学";
        alert(name);
        alert(school);
    }
    return son;
}
这段代码在window作用域中定义了函数father;
father中有两个局部变量:name、company;
father中定义了一个闭包;
闭包中定义了两个局部变量:name、school。
* 当JS初始化完成后:
内存中出现与全局环境相关的四个东西:
    1. 全局环境的变量对象(用于存储全局环境的变量,比如:father函数)
    2. 全局环境的作用域 和 作用域链(作用域链是一个链表,由多个作用域构成,全局环境的作用域链中只有一个全局环境的作用域,它指向全局环境的变量对象)
    3. 全局环境的执行环境 和 执行环境栈(执行环境栈里面存放着一个个执行环境,栈顶的那个表示正在执行的环境。执行环境中有一个指针指向它的作用域链)
    4. father函数的作用域链(此时father函数的作用域链中只包含全局环境的作用域,并没有father函数自己的作用域)
注意:每个函数自己的作用域是在函数执行时才创建的,而函数的作用域链则是在函数所在的环境被执行时创建的。当函数被执行时,它自己的作用域才会被添加到已经创建的作用域链的头部。
* 当执行father函数时:
JS引擎也会为它创建上述四样东西:
    1. 一个属于它的变量对象(里面存着name、company、son)
    2. 一个属于它的作用域链
此时,这个作用域链中包含两个作用域,分别是:全局作用域 和 father函数的作用域,他们分别指向各自的变量对象;
    3. 一个属于他的执行环境,并将执行环境压入执行环境栈的栈顶;
    4. son函数的作用域链(其中包含father和全局环境的作用域)
此时,内存图如下:
* 当father函数执行结束:
当函数执行结束后,它的执行环境 和 作用域链 将会被销毁,而它的变量对象就取决于是否有引用指向它。
由于son函数的作用域链指向了father的变量对象,因此它仍然驻留在内存中。
* 当执行son函数时:
同样的,JS引擎也会为son函数依次创建这些东西,并将新建的son函数作用域压入作用域链的头部,此时内存图如下:
* 当执行alert(company)时:
JS引擎会沿着son函数的作用域链依次查找变量对象中的值。
查找首先从son变量对象中开始,若该变量对象中没有,则沿着作用域链继续查找father变量对象,一旦找到,就停止。
因此,若查找name,首先就在son变量对象中找到,查找停止,因为无法访问到father中的name,除非在son函数中是否delete name将son中的name删除。
闭包的原理总结
综上所述,闭包之所以能访问其外层函数作用域中的变量,是因为闭包的作用域链中存在外层函数的变量对象。即使外层函数执行结束,但由于其变量对象仍然被内层函数的作用域引用,因此不会被内存回收,直到闭包执行结束后,外层函数的变量对象才会被回收。
闭包的特点--闭包访问外层函数变量的特点:
若闭包在外层函数执行结束后执行,那么它只能获取到外层函数中所有变量的最终状态。
* 例1:
function father(){
    var array = [];
    for (var i=0; i<10; i++) {
        array[i] = function(){
            return i;
        }
    }
    return array;
}
father函数执行后会返回一个包含闭包的数组,每个闭包都会返回i。
由于这里的闭包调用时,外层函数早就执行结束了,外层函数变量对象中i值已经变成了9,此时不管执行array中的哪个闭包,返回的结果都是9。
但是,如果在闭包外层函数执行过程中立即执行闭包,那么结果就不一样了,请看例2.
* 例2:
function father(){
    var array = [];
    for (var i=0; i<10; i++) {
        array[i] = (function(){
            return i;
        })();
    }
    return array;
}
此时的闭包在外层函数执行时就立即执行,在那个时刻,闭包中i的值就是外层函数当前i的值,因此返回的array中存储的将是0-9.
但是,例子1中array存放的明明是闭包,而这里的array存放的却是闭包执行的结果,那么若仍想让array存储闭包,我们还需要稍稍改造,请看例3。
* 例3:
function father(){
    var array = [];
    for (var i=0; i<10; i++) {
        array[i] = (function(){
            var curI = i;
            return function(){
                return curI;
            }
        })();
    }
    return array;
}
我们让立即执行的闭包再返回一个闭包,并且将for循环中的i值赋给立即执行函数的局部变量,此时array中存储的将是闭包,并且每个闭包都拥有正确的值。
闭包中的this指向 --window;闭包的内存问题--闭包占用内存比普通函数大
因为如果一个函数内部有闭包存在,那么函数执行结束后不会释放自己的变量对象,只有当闭包执行结束后才会释放,因此闭包将会占用更大的内存空间,用于存储外层函数的变量兑现。
因此,能避免的情况下就不要使用闭包。
闭包中涉及DOM对象可能会出现内存泄漏
我们知道,JavaScript由ECMAScript、BOM、DOM组成,在某些浏览器中他们使用不同的语言实现,因此他们具有不同的垃圾回收机制。
ECMAScript对象采用标记清除算法回收内存,而某些浏览器的DOM对象采用引用计数算法回收内存。引用计数有个致命的缺点——无法回收循环引用的对象。
举个例子:如果一个DOM对象A中的属性a指向另一个DOM对象B,而B中有属性b指向对象A,那么这两个对象存在循环引用,垃圾回收机制就无法回收他们,这就造成了内存泄漏。
退一步将,只要循环引用的两个对象中存在一个DOM对象,就会导致内存泄漏,请看下面的例子:
function func () {
    var dom = document.getElementById("xx");
    dom.onclick = function(){
        alert(dom.id);
    }
}
这段代码获取了一个DOM对象,并且让这个对象的onclick属性指向了一个JS函数对象;而这个函数对象又指向了DOM对象的id属性,从而出现了循环引用。由于这两个对象中存在一个DOM对象,因此就会出现内存泄漏。
如何避免?
要解决内存泄漏,我们只要破坏两个对象的相互引用即可。
上述代码要为dom添加一个点击事件,因此dom.onclick属性必须要指向一个JS函数对象,因此这个引用不能切断。
而第二个引用是由JS函数对象指向DOM对象的,目的是为了获取dom的id,我们可以通过如下代码切断这个引用:
function func () {
    var dom = document.getElementById("xx");
    var _id = dom.id;
    dom.onclick = function(){
        alert(_id);
    }
}

分享一个知乎答案 最详细易懂的 js闭包的更多相关文章

  1. 分享一个好用的函数吧,将js中的对象转成url参数

    JavaScript&jQuery获取url参数方法 这个函数呢是自己在写基于Vue+ElementUI管理后台时用到的,,下面列出来两种使用方式: 最普通的,封装一个js函数 /** * 对 ...

  2. 分享一个自己写的基于canvas的原生js图片爆炸插件

    DEMO访问地址: https://bupt-hjm.github.io/BoomGo/博客地址: http://bupt-hjm.github.io/2016/07/10/boom/插件及使用方法地 ...

  3. 最详细易懂的CRC-16校验原理(附源程序)(转)

    最详细易懂的CRC-16校验原理(附源程序) from:http://www.openhw.org/chudonganjin/blog/12-08/230184_515e6.html 最详细易懂的CR ...

  4. 最详细易懂的CRC-16校验原理(附源程序)

    from:http://www.openhw.org/chudonganjin/blog/12-08/230184_515e6.html 最详细易懂的CRC-16校验原理(附源程序) 1.循环校验码( ...

  5. 分享一个基于长连接+长轮询+原生的JS及AJAX实现的多人在线即时交流聊天室

    实现网页版的在线聊天室的方法有很多,在没有来到HTML5之前,常见的有:定时轮询.长连接+长轮询.基于第三方插件(如FLASH的Socket),而如果是HTML5,则比较简单,可以直接使用WebSoc ...

  6. 分享一个漂亮的ASP.NET MVC界面框架

    本文分享一个插件化的界面框架,该框架提供了用户.角色.权限管理功能,也提供了插件的管理和插件中心.下图是该界面框架的样式(全部源码和原理介绍下一篇分享,推荐越多,源码放的越早,呵呵). 要使用该界面框 ...

  7. [Unity3D入门]分享一个自制的入门级游戏项目"坦克狙击手"

    [Unity3D入门]分享一个自制的入门级游戏项目"坦克狙击手" 我在学Unity3D,TankSniper(坦克狙击手)这个项目是用来练手的.游戏玩法来自这里(http://ww ...

  8. 分享一个解决MySQL写入中文乱码的方法

    分享一个解决MySQL写入中文乱码的方法 之前有发帖请教过如何解决MySQL写入中文乱码的问题.但没人会,或者是会的人不想回答.搜索网上的答案并尝试很多次无效,所以当时就因为这个乱码问题搁浅了一个软件 ...

  9. 翻译:非常详细易懂的法线贴图(Normal Mapping)

    翻译:非常详细易懂的法线贴图(Normal Mapping) 本文翻译自: Shaders » Lesson 6: Normal Mapping 作者: Matt DesLauriers 译者: Fr ...

随机推荐

  1. VMware 虚拟化编程(14) — VDDK 的高级传输模式详解

    目录 目录 前文列表 虚拟磁盘数据的传输方式 Transport Methods Local File Access NBD and NBDSSL Transport SAN Transport Ho ...

  2. 阶段1 语言基础+高级_1-3-Java语言高级_06-File类与IO流_05 IO字符流_2_字符输入流读取字符数据

    读取的文件有中文也有英文 强转为char类型 缓冲读取多个字符 使用string的构造方法转换为字符输出

  3. 解决Delphi窗体缩放の疑难杂症

    http://anony3721.blog.163.com/blog/static/511974201082235754423/ 解决Delphi窗体缩放の疑难杂症 2010-09-22 15:57: ...

  4. Git - 版本回溯

    在git push的时候,有时候我们会想办法撤销git commit的内容 1.找到之前提交的git commit的id git log 找到想要撤销的id 2.git reset –hard id  ...

  5. PHP上传文件到阿里云OSS,nginx代理访问

    1. 阿里云OSS创建存储空间Bucket(读写权限为:公共读) 2. 拿到相关配置 accessKeyId:********* accessKeySecret:********* endpoint: ...

  6. 如何统计序列中元素的频度---Python数据结构与算法相关问题与解决技巧

    实际案例: 1. 某随机序列 [12,5,6,4,6,5,5,7]中,找到出现次数最高的3个元素,它们出现的次数是多少? 2. 对于某英文文章的单词,进行词频统计,找到出现次数最高的10个单词,它们出 ...

  7. linux系统镜像iso文件下载

    linux系统镜像iso文件下载 首先你要选择你想要的linux版本,常见版本有CentOS,Ubuntu.选择一个你需要的. 有两个镜像站推荐: 网易镜像站:http://mirrors.163.c ...

  8. 应用安全 - PHPCMS - Joomla漏洞汇总

    Joomla 反序列化(版本低于3.4.5) CVE-2015-8562 RCE Date:October, 2019原理:https://blog.hacktivesecurity.com/inde ...

  9. Js基本类型中常用的方法总结

    1.数组 push() -----> 向数组末尾添加新的数组项,参数为要添加的项,返回值是新数组的长度,原数组改变: pop() -----> 删除数组末尾的最后一项,参数无,返回值是删除 ...

  10. [Python3] 008 列表内涵,“满腹经纶”

    目录 简述 少废话,上例子 例1 用 for 创建列表 例2 看看乘法"向"着谁 例3 给列表加一张"滤纸" 例4 列表生成式可以嵌套 例5 列表生式还能嵌入条 ...