javascript-闭包

loading 2022年04月07日 167次浏览

1.引子

JS有着自身独特的变量作用域,其特殊之处就在于:

  • 函数内部可以直接读取全局变量
   var n=999;

  function f1(){
    alert(n);
  }

  f1(); // 999
  • 函数外部无法读取函数内局部变量

此处函数内如果不使用var命令声明变量相当于声明全局变量

  function f1(){
    var n=999;
  }

  alert(n); // error

那该如何从外部也能读取函数内局部变量呢,那就是在函数的内部再定义一个函数

  function f1(){
    var n=999;
    function f2(){
      alert(n); // 999
    }
  }

f2是可以访问f1中的变量n的,那么只要return f2,就可以在f1外部读取到f1内的局部变量了。

        function f1(){
            var n = 999;
            return function f2(){
                console.log(n);
            }
        }
        var result = f1();
        result(); //999
	//不能写下面这行代替上面两行!
	f1()();
	
	

而这个f2函数,就是‘闭包’。

1.1 匿名函数

上面的代码也可以把f2省略掉,那就涉及到匿名函数的概念,匿名函数1是可以用于闭包,二是可以形成块级作用域

没有名字的函数就是匿名函数,听着陌生其实实际应用中很常见了,比如

       setInterval(()=>{

        },100)

第一个参数就是匿名函数
而匿名函数写成立即执行函数就可以封装出块级作用域

function fn(){
    (function(){
        var la="啦啦啦!";
    })();
    console.log(la);//报错---la is not defined
}
fn();

2.闭包的概念

闭包是能够读取其他函数内部变量函数

因为只有函数内部的子函数才能读取它的变量,所以可以把闭包理解为:
定义在一个函数内部的函数

3.闭包范例

闭包可以用在许多地方。它的最大用处有两个,一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。

3.1 解绝循环打印问题

改进方法一:立即执行函数
使用 IIFE(立即执行函数)会为每一轮循环都生成一个新的函数作用域,使得定时器函数的回调可以将新的作用域封闭在每一轮循环内部,每一轮循环内部都会含有一个具有正确值的变量可以访问

改进方法二:let替换var
变量i是let声明的,当前的i在本轮循环有效,所以每一次循环的i其实都是一个新的变量。JavaScript 引擎内部会记住上一轮循环的值,初始化本轮的变量i时,就在上一轮循环的基础上进行计算。

请戳此链接,var和let中有例子和对比。

3.2 一道经典闭包题目

        const fn = function(n , o){
            console.log('o' , o);
            return {
                test : function(m){
                    return fn(m , n)
                }
            }
        }

        const a = fn(0);//undefined
        a.test(1);//0
        a.test(2);//0
        a.test(3);//0

        const b = fn(0).test(1).test(2).test(3);//undefiend 0 1 2

首先分析a这个变量:

const a = fn(0);//undefined

fn函数有两个参数function(n , o),现在只传入第一个参数n=0,那么o就是undefined,因此打印出o为undefined。

a.test(1);//0

test是fn中的闭包,它传入一个参数m,返回fn(m , n)。

这里执行a.test(1),也就是使得m=1,而n的值在上方定义时已经定义为0,因此相当于test将return fn(1,0)。

因此,a.test(1),就相当于执行fn(1,0),打印出来的o就是0。

同理a.test(2)就是执行fn(2,0),打印出来的o还是0,以此类推。

随后分析b这个变量:

const b = fn(0).test(1).test(2).test(3);//undefiend 0 1 2

首先fn(0).test(1)和上方变量a的a.test(1)道理相同,因此首先输出undefined和0。

随后是fn(0).test(1).test(2),此时test传入2,使得m=2。

而上一步fn(0).test(1)相当于return(1,0),此时n=1,o=0,因此fn(0).test(1).test(2)相当于return(2,1)。也就相当于const fn = function(2,1),因此将1作为fn中o的参数,打印出1。

接下来则以此类推打印出2。

3.3 闭包中的this

fn中的this在非严格模式中指向window,严格模式中undefined

var obj = {
    birth: 1990,
    getAge: function () {
        var b = this.birth; // 1990
        var fn = function () {
            return new Date().getFullYear() - this.birth; // this指向window或undefined
        };
        return fn();
    }
};

将fn改成箭头函数,继承了上层中的this

var obj = {
    birth: 1990,
    getAge: function () {
        var b = this.birth; // 1990
        var fn = () => new Date().getFullYear() - this.birth; // this指向obj对象
        return fn();
    }
};
obj.getAge(); // 25

4.内存泄露

闭包可能导致严重的内存泄露问题

一般情况下,函数执行会形成一个新的私有的作用域,当私有作用域中的代码执行完成后,我们当前作用域都会主动的进行释放和销毁。但当遇到函数执行返回了一个引用数据类型的值,并且在函数的外面被一个其他的东西给接收了,这种情况下一般形成的私有作用域都不会销毁。

像这种情况:

        function fn() {
            var num = 100;
            return function () {}
        }
        var f = fn();//fn执行形成的这个私有的作用域就不能再销毁了

fn内部的私有作用域会被一直占用,导致内存泄露,需要手动将引用变量指向null。

像这道内存泄露的经典题目:

        function outer() {
            var num = 0;//内部变量
            return function add() {
                num++;//内部函数有引用,作为add函数的一部分了
                console.log(num);
            };
        }

        var func1 = outer();
        func1();//实际上是调用add函数, 输出1
        func1();//输出2 因为outer函数内部的私有作用域会一直被占用

        var func2 = outer();
        func2();// 输出1  每次重新引用函数的时候,闭包是全新的。
        func2();// 输出2

5.闭包的应用场景

任何闭包的使用场景都离不开这两点:

  • 创建私有变量
  • 延长变量的生命周期

一般函数的词法环境在函数返回后就被销毁,但是闭包会保存对创建时所在词法环境的引用

5.1 函数节流和防抖

详细解释见函数节流和防抖博客,这里就把防抖的代码贴上来

        function debounce(func , delay){
            let timeout = null;
            return function(e){
                console.log(e)
                //在规定时间间隔到达前,一触发就重新计时
                clearTimeout(timeout);
                //每次触发事件就会新增一个setTimeOut开始计时,经过规定时间后执行func
                timeout = setTimeout(() =>{
                    func.apply(this , arguments);
                } , delay);
            }
        }
        function test(){
            console.log('debounce' , Math.random());
        }
        window.addEventListener('scroll' , debounce(test , 500))

5.2 封装私有变量

如下面这个例子中的sum外部是无法访问到的。

        function f1(){
            let sum = 0;
            return {
                fn : function(){
                    sum++;
                    console.log(sum);
                }
            }
        }
        //f1返回一个对象传递给res
        let res = f1();

        //res通过'.'操作符运行对象中的方法属性
        res.fn(); //1 
        res.fn(); //2
        res.fn(); //3

5.3 函数柯里化

柯里化的目的在于避免频繁地调用具有相同参数的函数,同时又能够轻松地重用。

        const getArea = function(width , height){
            console.log(width * height);
        }
        //假如我们要处理的长方形的宽总是10
        const rec1 = getArea(10 , 20);
        const rec2 = getArea(10 , 30);

        //可以使用闭包柯里化来计算面积
        const getArea1 = width =>{
            return height => {
                console.log(width * height);
            }
        }
        const getTenWidthArea = getArea1(10);
        //之后只要是求宽度为10的长方形面积就传长这个参数就好了
        const rec3 = getTenWidthArea(40);

5.4 计数器

对象中能持续访问到外层的cur,注意还能访问到init,因为闭包既能访问外层函数的局部变量,也能访问外层函数的参数。

var createCounter = function(init) {
    let cur = init;
    return {
        increment: () => ++cur,
        decrement: () => --cur,
        reset: () => (cur = init),
    }
};

6. 修改闭包中的内容?

看到这么一道很有意思的题目,要求在不修改下面这段代码的情况下修改obj的内容:

let o = (function () {
  let obj = {
    a: 1,
    b: 2,
  };
  return {
    get: function (k) {
      return obj[k];
    },
  };
})();

漏洞在于闭包没有对obj[k]能访问到的内容作出限制,所以可以让obj[k]访问到obj原型上的方法,因此我们可以给原型添加一个访问器方法,用来获取到闭包中被保护的obj:

Object.defineProperty(Object.prototype, "evil", {
  get() {
    return this;
  },
});

let o2 = o.get("evil");
o2.a = 1111111111;
console.log(o.get("a"));

要解决这个问题对属性进行一下判断即可,必须是要自身携带的属性才返回:

let o = (function () {
  let obj = {
    a: 1,
    b: 2,
  };
  return {
    get: function (k) {
      if (obj.hasOwnProperty(k)) {
        return obj[k];
      }
      return undefined;
    },
  };
})();