全面总结闭包

执行环境和作用域链

  • 执行环境定义了变量或者函数有权访问的其它数据,决定了他们各自的行为。每个执行环境都有一个与之关联的变量对象(VO),环境中定义的所有变量和函数都保存在这个对象中。
  • 全局执行环境是最外围的执行环境。在Web服务器中,全局环境被认为是window对象(node中全局执行环境是global对象),因此所有全局变量和函数都作为window对象的属性和方法创建的。某个执行环境的所有代码执行完后,该环境被销毁,保存在其中的所有变量和函数定义也随之销毁(全局执行环境直到应用程序退出—例如关闭网页或浏览器时才会被销毁)
  • 每个函数都有自己的执行环境。当执行流进入一个函数时,函数的环境就会被推入到一个环境栈中。而在函数执行完成后,栈将其环境弹出,把控制权交给之前的执行环境。

image.png

  • 当代码在一个环境中执行时,会创建变量对象的一个作用域链(整个作用域链的本质是一个指向变量对象的指针列表,它只引用但不实际包含变量对象)。作用域链的用途是保证对执行环境有权访问的所有变量和函数的有序访问(内部环境可以通过作用域链访问所有的外部环境,但外部环境不能访问内部环境的任何变量和函数)。作用域链的前端,始终是当前执行的代码所在的环境变量。

理解了这些就更容易理解闭包是怎么产生的了。

闭包是什么?

有权访问另一个函数作用域中变量的函数

形成闭包的原因

function createComparisonFunction(propertyName){
    return function(object1,object2){
        //匿名函数中value1和value2访问了外部函数中的变量propertyName
        var value1=object1[propertyName];
        var value2=object2[propertyName];

        if(value1<value2){
            return -1;
        }else if(value1>value2){
            return 1;
        }else{
            return 0;
        }
    }
}
//创建函数
var compareNames=createComparisonFunction("name");
//调用函数
var result=compareNames({name:"Nicholas"},{name:"Gerg"});  
//解除对匿名函数的引用(以便释放内存)
compareNames=null;
复制代码

image.png

即使内部函数(匿名函数)被返回了,而且在其他地方被调用了,它仍然可以访问变量propertyName。之所以还能够访问这个变量,是因为内部函数的作用域链中包含外部函数createComparisonFunction()的作用域。
在另一个函数内部定义的函数会将包含函数(即外部函数)的活动对象添加到它的作用域链中。因此,在createComparisonFunction()函数内部定义的匿名函数的作用域链中,实际上将会包含外部函数createComparisonFunction()的活动对象。
在匿名函数从createComparisonFunction()中返回后,它的作用域链被初始化为包含createComparisonFunction()函数的活动对象和全局对象。这样匿名函数就可以访问在createComparisonFunction()中定义的所有变量。更为重要的是,createComparisonFunction()函数在执行完毕后,其活动对象也不会被销毁,因为匿名函数的作用域链仍然在引用这个活动对象。换句话说,当createComparisonFunction()函数返回后,其执行环境的作用域链会被销毁,但它的活动对象仍然会留在内存中;直到匿名函数被销毁后,createComparisonFunction()的活动对象才会被销毁。

闭包的作用

存在嵌套关系的函数之间,内部的函数可以访问父函数的变量,只不过写在内部的函数,当父函数执行完后就会被销毁。为了解决这个问题,我们想要在父函数外部访问其内部的变量,并使子函数引用的父函数的局部变量常驻内存,这里将内部的函数作为桥梁和外部的全局变量建立引用关系,这样就不会被垃圾回收器回收。

闭包的经典使用场景

  • 返回一个函数
  • 函数作为参数
  • IIFE立即执行函数

其实经常使用的闭包就是防抖和节流

  • 简易的防抖
function debounce(func,wait) {
    let timeout;
    return function () {
        let context = this;
        let args = arguments;

        if (timeout) clearTimeout(timeout);

        let callNow = !timeout;
        timeout = setTimeout(() => {
            timeout = null;
        }, wait)

        if (callNow) func.apply(context, args)
    }
}
function showTop  () {
    var scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
  console.log('滚动条位置:' + scrollTop);
}
window.onscroll = debounce(showTop,1000) 
复制代码
  • 简易的节流
function throttle(func, wait) {
    let previous = 0;
    return function() {
        let now = Date.now();
        let context = this;
        let args = arguments;
        if (now - previous > wait) {
            func.apply(context, args);
            previous = now;
        }
    }
}
复制代码

闭包要注意的问题

每个父函数调用完成,都会形成新的闭包,父函数中的变量始终会在内存中,相当于缓存,小心内存消耗的问题。

引申出来的垃圾回收问题

JavaScript具有自动垃圾收集机制,也就是说,执行环境会负责管理代码执行过程中使用的内存。原理:找出那些不再继续使用的变量,然后释放其占用的内存。为此,垃圾收集器会按照固定的时间间隔(或代码执行中预定的收集时间),周期性地执行这一操作。

  • 引用计数

当声明了一个变量并将一个引用类型值赋给该变量时,则这个值的引用次数就是1。如果同一个值又被赋值给另一个变量,则这个值的引用次数加一。相反,如果包含这个值的引用变量又取得了另一个值,则这个值的引用次数减1.当这个值的引用次数变为0的时候,说明没有变量在使用,这个值没法被访问。这样,当垃圾回收器下次再运行的时候,它就会释放引用次数为0的值所占用的内存

限制:无法处理循环引用

  • 标记清除

垃圾收集器先给内存中所有的变量都加上标记,这个算法假定设置一个叫做根(root)的对象(在Javascript里,根是全局对象)。垃圾回收器将定期从根开始,找所有从根开始引用的对象,然后找这些对象引用的对象……从根开始,垃圾回收器将找到所有可以获得的对象然后去掉它们的标记,剩下的被标记的对象就是无法访问的等待的回收的对象。

V8如何进行垃圾回收

  • 栈内存

调用栈上下文切换后就被回收

  • 堆内存
  1. 新生代

首先将新生代内存空间一分为二,其中From部分表示正在使用的内存,To 是目前闲置的内存。当进行垃圾回收时,V8 将From部分的对象检查一遍,如果是存活对象那么复制到To内存中(在To内存中按照顺序从头放置的),如果是非存活对象直接回收即可。当所有的From中的存活对象按照顺序进入到To内存之后,From 和 To 两者的角色对调,From现在被闲置,To为正在使用,如此循环。
缺点:内存碎片

  1. 老生代
  • 新生代对象的晋升

新生代内存空间用来存放存活时间较短的对象,老生代内存空间用来存放存活时间较长的对象。新生代中的对象可以晋升到老生代中,具体有两种方式:
在垃圾回收的过程中,对象从From空间复制到To空间时,会检查该对象是否经历过一次回收。如果已经经历过,会将该对象从From空间复制到老生代空间中;否则,则复制到To空间中。
另一个判断条件是To空间的内存占用比。当要从From空间复制一个对象到To空间时,如果To空间已经使用了超过25%,则这个对象直接晋升到老生代空间中。
设置25%这个限制的原因是当本次回收完成后,这个To空间将变成form空间,接下来的内存分配将在这个空间中进行。如果占比过高,会影响后续内存分配。

  • 标记清除

首先会遍历堆中的所有对象,对它们做上标记,然后对于代码环境中使用的变量以及被强引用的变量取消标记,剩下的就是要删除的变量了,在随后的清除阶段对其进行空间的回收。

  • 整理内存碎片

把存活的对象挪到内存一端。由于是移动对象,它的执行速度不可能很快,事实上也是整个过程中最耗时间的部分。

另外介绍一下常见的内存泄露

  • 意外声明的全局变量
function foo(){
    a='hello'//本来变量a只能在函数作用域内被使用,但没有用var去声明它,就创建了一个全局变量
}
复制代码
function foo(){
    this.a='hello' //这里的this指向全局
}
foo()
复制代码
  • 被遗忘的定时器和回调函数
var someResource = getData(); 
setInterval(function() { 
    var node = document.getElementById('Node'); 
    if(node) { 
        // 处理 node 和 someResource 
        node.innerHTML = JSON.stringify(someResource)); 
    } 
}, 1000);

复制代码
  • 闭包
  • 脱离DOM的引用

当你保存了一个dom的引用,然后该dom从html中删除后,应该将这个引用赋值为null,负责GC不会回收。(解决方法也可以用推出了两种新的数据结构:WeakSet 和 WeakMap。它们对于值的引用都是不计入垃圾回收机制的)

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享