闭包基础知识

149 阅读4分钟

闭包的概念

红宝书中的定义:闭包指的是那些引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现的

从定义中我们知道闭包是一个函数,只不过这个函数有超能力,可以访问到另一个函数的作用域。

为什么说闭包有超能力呢?

因为我们都知道函数作用域是独立的、封闭的,外部的执行环境是访问不了的,但是闭包具有这个能力和权限。

那闭包是怎样的一个表现形式呢?

  • 第一,闭包是一个函数,而且存在于另一个函数中
  • 第二,闭包可以访问父级函数的变量,且该变量不会销毁
functio animal() {
    const name = 'doudou';
    function cat() {
        console.log(name)
    }
    return cat;
}

const animal1 = animal(); // animal1的值就是return后的结果,即cat函数
animal1(); // doudou, animal1()就相当于cat()
animal1(); // doudou 同上 变量name没有销毁,一直存在内存中,供函数cat调用
animal1(); // doudou

闭包原理

闭包的实现原理,其实就是利用了作用域链的特性,作用域链就是在当前执行环境下访问某个变量时,如果不存在就一直向外层寻找,最终寻找到最外层也就是全局作用域,这样就形成了一个链条。

例如:

var age = 2;
function cat() {
    age++;
    conoslo.log(age); // cat函数内输出age,该作用域没有,则向外寻找,结果找到了,输出3
}
cat();

看到这里,大家都会说这不就是最简单的函数和变量形式吗?闭包在哪里?别急,我们接着往下看: 如果我们再次调用时,结果会一直增加,也就变量age的值一直递增。

cat();//4
cat();//5
cat();//6

如果程序其他函数也需要用到age的值,则会受到影响,而且全局变量还同意被人修改,比较不安全,这就是全局变了容易污染的原因。解决全局变量污染的问题,就可以利用闭包把变量封装到函数内,让它成为局部变量。

function animal(){
    var age = 2;
    function cat(){
        age++;
        console.log(age);
    }
    return cat;
}
var per = animal();//per相当于函数cat
per();// 3
per();// 4
per();// 5

这样变量age在函数内部,不易修改和外泄,相对来说比较安全。

闭包作用

作用1: 可以读取函数内部的变量

function f1(){
    var n='test';
    function f2(){
        console.log(n);
    }
    return f2;
}
var result=f1();
result()  /// test

作用2: 将创建的变量的值始终保持在内存中, 不影响全局变量

var a = 10;
function Add3(){
    var a = 10;
    return function(){
        a++;
        return a;
    };
};
var cc =  Add3();
console.log(cc()); // 11
console.log(cc()); // 12
console.log(cc()); // 13
console.log(a); //  10

作用3: 封装对象的私有属性和方法

代码中,a1 和 a2 是相互独立的,各自返回自己的私有变量。

function f1(n) {
  return function () {
    return n++;
  };
}
var a1 = f1(1);
a1() // 1
a1() // 2
a1() // 3

var a2 = f1(5);
a2() // 5
a2() // 6
a2() // 7

常见的一些闭包

function foo(a) {
    setTimeout(function timer(){
        console.log(a)
    }, 1000)
}
foo(2);

foo执行1000ms 后,它的内部作用域不会消失,timer函数依然保有 foo 作用域的引用。timer函数就是一个闭包。

定时器,事件监听器,Ajax请求,跨窗口通信,Web Workers或者其他异步或同步任务中,只要使用回调函数,实际上就是闭包。

闭包应用(后面继续补充)

模仿块级作用域

比如我们可以使用闭包能使下面的代码按照我们预期的进行执行(每隔1s打印 0,1,2,3,4)。

function(var i =0; i<5; i++){
    (function(j){
        setTimeout(function(){
            console.log(j)
        }, j*1000)
    })(i)
}

我们应该尽量避免往全局作用域中添加变量和函数。通过闭包模拟块级作用域。

定义模块,将操作函数暴露给外部,而细节隐藏在模块内部

function module () {
  const arr = [];
  function add (val) {
    if (typeof val == 'number') {
      arr.push(val);
    }
  }
  function get (index) {
    if (index < arr.length) {
      return arr[index]
    } else {
      return null;
    }
  }
  return {
    add: add,
    get: get
  }
}
let mod1 = module();
mod1.add(1);
mod1.add(2);
console.log(mod1.get(0));  //1

使用闭包的注意点

  • 由于闭包会使得函数中的变量都保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能会导致内存泄漏。解决方法时,在退出函数之前,将不使用的局部变量全部删除。
  • 闭包会在父函数外部,改变赴韩束内部变量的值。所以,如果你把父函数当作对象使用,把闭包当作它的公用方法,把内部变量当作它的私有变量,这时一定要小心,不要随便改变父函数内部变量的值。

经典面试题

for (var i = 1; i < 5; i++) {
    setTimeout(() => console.log(i), 1000);
}

上述代码本意是输出1、2、3、4,但结果却是四个5,为了解决该问题,主要有三种办法。

1. 变量可以通过函数参数的形式传入
for (var i = 1; i < 5; i++) {
    (function(j) {
        setTimeout(() => console.log(j), 1000);
    })(i);
}
2. 使用setTimeout包裹,通过第三个参数传入。(注:setTimeout后面可以有多个参数,从第三个参数开始其就作为回掉函数的附加参数)
for (var i = 1; i < 5; i++) {
    setTimeout(value => console.log(value), 1000, i);
}
3. 使用块级作用域,让变量成为自己上下文的属性,避免共享
for (let i = 1; i < 5; i++) {
    setTimeout(() => console.log(i), 1000);
}

参考:
segmentfault.com/a/119000002…
juejin.cn/post/684490…