闭包总结

616 阅读9分钟
闭包面试题联想
  • 变量提升
  • 作用域
  • 使用场景
  • 闭包引起内存泄漏

image.png

闭包出现的原因:

JS中存在两种变量的作用域,一种是全局变量,一种是局部变量。两种变量的区别就是函数内部可以直接读取全局变量但是在函数外部无法读取函数内部的局部变量

总结:内部的函数存在外部作用域的引用就会导致闭包

闭包的定义:

闭包定义:在一个函数内部定义的函数可以访问外部函数的变量。声明在一个函数中的函数,叫做闭包函数。

形成闭包的原因

内部的函数存在外部作用域的引用就会导致闭包。

var a = 0 
function foo(){
   var b = 12
   function fo(){
      console.log(a,b)
   }
   fo()
}
foo()

子函数fo内存就存在外部作用域的引用a,b,所以就会产生闭包

闭包变量存储的位置

闭包中的变量存储的位置是堆内存,存储引用类型值,对象类型就是键值对,函数就是代码字符串

闭包用途:
  • 可以读取函数内部的变量
  • 变量的值始终保持在内存中
  • 方便调用上下文的局部变量,利于代码封装

简单闭包的例子:

function A() {
  var i = 1;
  function B() {
    return i  = i + 1
  }
  return B();
}
A()

闭包的特征:

  • 函数内再嵌套函数
  • 内部函数可以引用外层的参数和变量
  • 参数和变量不会被垃圾回收制回收

注意:

由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。

解决方法:在退出函数之前,将不使用的局部变量全部删除。

闭包的优缺点

缺点:
  • 内存泄漏:闭包中的函数引用外部函数的变量,而外部函数的作用域在函数执行结束后不会被销毁。导致闭包函数的中变量无法被销毁,占用内存空间。滥用闭包,可能会导致内存泄漏。
  • 性能问题:闭包中的函数访问外部函数的变量需要通过作用域链查找,而作用域链的长度决定查找的速度。如果闭包层数较深,作用域链就会很长,影响函数的执行效率。
优点
  • 私有变量和数据封装:通过闭包可以创建私有变量,只能在内部函数中访问和修改,外部无法直接访问。
  • 数据的持久性:闭包似的内部函数可以持续访问外部函数的变量,即使外部函数已经执行完毕。
  • 创建函数工厂和动态函数:通过闭包可以动态生成函数,每个函数都有自己的独立作用域和状态。

内存泄漏

内存泄露可以定义为:应用程序不再需要占用内存的时候,由于某些原因,内存没有被操作系统或可用内存池回收。

常见 JavaScript 内存泄露:

  • 被遗忘的计时器或回调函数

原因:定时器中有dom的引用,即使dom删除了,但是定时器还在,所以内存中还是有这个dom。

解决:手动删除定时器和dom。

  • 脱离 DOM 的引用

原因:虽然别的地方删除了,但是对象中还存在对dom的引用

解决:手动删除。

  • 闭包

原因:闭包可以维持函数内局部变量,使其得不到释放。

解决:将事件处理函数定义在外部,解除闭包,或者在定义事件处理函数的外部函数中,删除对dom的引用。

解决方法:

一、问题:针对意外的全局变量

答案如下:

  • 在 JavaScript 文件头部加上 'use strict',可以避免此类错误发生。启用严格模式解析 JavaScript ,避免意外的全局变量。

  • 如果必须使用全局变量存储大量数据时,确保用完以后把它设置为 null 或者重新定义。

二、问题:闭包

原始代码:

function Cars(){
  this.name = "Benz";
  this.color = ["white","black"];
}
Cars.prototype.sayColor = function(){
  var outer = this;
  return function(){
    return outer.color
  };
};
 
var instance = new Cars();
console.log(instance.sayColor()())

优化后代码:

function Cars(){
  this.name = "Benz";
  this.color = ["white","black"];
}
Cars.prototype.sayColor = function(){
  var outerColor = this.color; //保存一个副本到变量中
  return function(){
    return outerColor; //应用这个副本
  };
  outColor = null; //释放内存
};
 
var instance = new Cars();
console.log(instance.sayColor()())

闭包经典问题

for(var i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i);
  }, 1000);
}

答案:3个3 解析:首先,for 循环是同步任务,先执行三遍 for,i 变成了 3;然后,再执行异步代码 setTimeout,这时候输出的 i,只能是 3 个 3 了。

如何改为输出的是0,1,2呢?

  • var改成let,var是函数作用域,let是块级作用域。
for(let i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i);
  }, 1000);
}

答案:0,1,2

解析:因为每个 let 和代码块结合起来形成块级作用域,当 setTimeout() 打印时,会寻找最近的块级作用域中的 i,所以依次打印出 0 1 2。

  • 采用立即执行函数创建作用域
for(let i = 0; i < 3; i++) {
  (function(i){
    setTimeout(function() {
      console.log(i);
    }, 1000);
  })(i)
}

答案:0,1,2

闭包的使用场景

setTimeout

原生的setTimeout传递的第一个函数不能带参数,通过闭包可以实现传参效果。

//原生的setTimeout传递的第一个函数不能带参数
    setTimeout(function(param){
        alert(param)
    },1000)


 //通过闭包可以实现传参效果
    function func(param){
        return function(){
            alert(param)
        }
    }
    var f1 = func(1);
    setTimeout(f1,1000);  // 1秒之后打印1
回调

定义行为,然后把它关联到某个用户事件上(点击或者按键)。代码通常会作为一个回调(事件触发时调用的函数)绑定到事件。

function changeSize(size){
        return function(){
            document.body.style.fontSize = size + 'px';
        };
    }

    var size12 = changeSize(12);
    var size14 = changeSize(14);
    var size16 = changeSize(16);

    document.getElementById('size-12').onclick = size12;
    document.getElementById('size-14').onclick = size14;
    document.getElementById('size-16').onclick = size16;
    // 我们定义行为,然后把它关联到某个用户事件上(点击或者按键)
    // 我们的代码通常会作为一个回调(事件触发时调用的函数)绑定到事件上
函数防抖

在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。

实现的关键就在于setTimeOut这个函数,由于还需要一个变量来保存计时,考虑维护全局纯净,可以借助闭包来实现。

/*
* fn [function] 需要防抖的函数
* delay [number] 毫秒,防抖期限值
*/
function debounce(fn,delay){
 let timer = null //借助闭包
 return function() {
  if(timer){
   clearTimeout(timer) //进入该分支语句,说明当前正在一个计时过程中,并且又触发了相同事件。所以要取消当前的计时,重新开始计时
   timer = setTimeOut(fn,delay) 
  }else{
   timer = setTimeOut(fn,delay) // 进入该分支说明当前并没有在计时,那么就开始一个计时
  }
 }
}
封装私有变量、模块化

使用闭包可以实现模块化,将一些相关的函数和变量封装在一个函数中,并返回一个公共接口。防止这些函数和变量被其他代码意外修改,提高代码的安全性和可维护性。

//用闭包定义能访问私有函数和私有变量的公有函数。
    var counter = (function(){
        var privateCounter = 0; //私有变量
        function change(val){
            privateCounter += val;
        }
        return {
            increment:function(){   //三个闭包共享一个词法环境
                change(1);
            },
            decrement:function(){
                change(-1);
            },
            value:function(){
                return privateCounter;
            }
        };
    })();

    console.log(counter.value());//0
    counter.increment();
    counter.increment();//2
    //共享的环境创建在一个匿名函数体内,立即执行。
    //环境中有一个局部变量一个局部函数,通过匿名函数返回的对象的三个公共函数访问。
    
    counter = null //不使用闭包时,要清除。
节点循环绑定事件
function showContent(content){
        document.getElementById('info').innerHTML = content;
    };

    function setContent(){
        var infoArr = [
            {'id':'email','content':'your email address'},
            {'id':'name','content':'your name'},
            {'id':'age','content':'your age'}
        ];
        for (var i = 0; i < infoArr.length; i++) {
            let item = infoArr[i];      //限制作用域只在当前块内
            document.getElementById(item.id).onfocus = function(){
                showContent(item.content)
            }
        }
    }
    setContent()
缓存数据

闭包可以实现数据缓存,当需要多次计算某个值时,可以将计算结果缓存,下次需要直接使用缓存中的值,避免重复计算。

额外补充

函数的词法作用域

掌握嵌套函数的词法作用域规则:函数被执行时使用的作用域链(scope chain)是被定义时的作用域链,而不是执行时的作用域链。

var scope = "global scope"; 
function checkScope() {
    var scope = "local scope";
    function f() {
        return scope;
    }
    return f();
}
checkScope();   //=> "local scope"

解析代码:

该代码定义了一个全局变量 scope,以及一个函数checkScope,在函数checkScope中,定一个一个局部变量,同样命名为scope,以及一个函数f(嵌套函数)。 1.在js中,函数可以用来创建函数作用域; 2.函数就像一层半透明玻璃,在函数内部可以看到函数外部的变量,但是在函数外部,看不到函数内部的变量。 3.变量的搜索是从内向外而不是从外向内搜索的。

立即执行函数

定义:JS 立即执行函数可以让函数在创建后立即执行。

写法:

// 第一种写法
(function(){
  ...
})()
// 第二种写法
(function(){
  ...
}())

作用:创建一个独立的作用域。这个作用域里面的变量,外面访问不到(即避免了「变量污染」)。

立即执行函数与闭包的区别

  • 立即执行函数和闭包只是有一个共同优点就是能减少全局变量的使用。

  • 立即执行函数只是函数的一种调用方式,只是声明完之后立即执行,这类函数一般都只是调用一次,调用完之后会立即销毁,不会占用内存。

  • 闭包则主要是让外部函数可以访问内部函数的作用域,也减少了全局变量的使用,保证了内部变量的安全,但因被引用的内部变量不能被销毁,增大了内存消耗,使用不当易造成内存泄露。

垃圾回收机制

方法:

  • 标记清除(常规) 浏览器将所有引用变量加上标记,然后将全局引用的变量以及闭包的标记清除。在执行js代码的时候会进入一个执行环境,当离开当前执行环境时,当前执行环境内标记的变量会被清除,大多数浏览器都是使用这种方式。
  • 引用计数(IE7/8,Netscape Navigator3) 每次引用一个变量,都会在引用计数中+1,如果这个值赋给另一个引用,那么再+1,相反,如果当引用这个值的变量引用了其他的变量,那么就会-1,当引用数量为0时,会被垃圾回收器清除。