高级函数-闭包

什么是闭包

闭包是指有权访问另一个函数作用域中的变量的函数。

创建闭包的常见方式:在函数内部创建另一个函数。

当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。

简单来说,闭包是保存了执行环境的上下文的函数

词法作用域意味着作用域是由书写代码时函数声明的位置来决定的。

编译的词法分析阶段基本能够知道全部标识符在哪里以及是如何声明的,从而能够预测在执行过程中如何对它们进行查找。

闭包阻止了垃圾回收器释放外部函数的执行空间。参数和变量不会被垃圾回收机制回收。

即使外部函数栈内存已经释放闭包依旧存在。

为什么不会被垃圾回收机制回收:

首先 JS 垃圾回收机制中,如果一个对象不再被引用,那么这个对象就会被垃圾机制回收,如果两个对象相互引用,而不在被第三方引用,那么这两个对象就会被垃圾回收机制回收。

因为闭包中,父函数被子函数引用,子函数又被外部所引用,这就是父函数不被回收的原因。

经典闭包:

function foo() {
    var a = 2;
    function bar() {
        console.log(a);
    }
    return bar; // 将bar所应用的函数对象本身当作返回值
}
var baz = foo(); // 实际上只是通过不同的标识符引用调用了内部的函数bar
baz(); // 闭包

通常,foo()执行结束后,垃圾回收器会释放不在使用的内存空间,但是因为foo()内部bar()仍在使用foo()的内部作用域,因此foo()不会被回收。bar()依然持有对该作用域的引用,而这个引用就叫做闭包。baz()被调用是可以访问被定义时的词法作用域。

闭包使得函数可以继续访问定义时的词法作用域。

无论通过何种手段将内部函数传递到所在的词法作用域以外,闭包都会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。例如:

var fn;
function foo() {
    var a = 2;
    function baz() {
        console.log(a);
    }
    fn = baz; // 将baz分配给全局变量,但baz仍可以访问foo的作用域
}
function bar() {
    fn(); // 闭包
}
foo();
bar(); // 2

闭包作用可以避免变量全局污染,例如:

function add(x){
    return function(y){
        return x + y
    };
}

var addFun1 = add(4)
var addFun2 = add(9)

console.log(addFun1(2)) //6
console.log(addFun2(2))  //11

addFun1addFun2 拥有自己的内部的变量,相关应用有 vue 组件用函数返回对象的方式定义 data,这样可以避免组件复用的时候 其中一个组件data 修改影响到其他组件。

回调函数和IIFE是闭包的一种。

立即执行函数的好处:通过定义一个匿名函数,创建了一个新的函数作用域,相当于创建了一个“私有”的命名空间,该空间的变量和方法,不会破坏和污染全局的命名空间。

var arr = [];
for (var i=0;i<3;i++){
    //使用IIFE
    (function (i) {
        arr[i] = function () {
            return i;
        };
    })(i);
}
console.log(arr[0]()) // 0
console.log(arr[1]()) // 1
console.log(arr[2]()) // 2

闭包访问值范围:

  • 闭包可以访问当前函数以外的变量。
  • 即使外部函数已经返回,闭包仍能访问外部函数定义的变量与参数。
  • 闭包可以更新外部变量的值。
  • 内层函数可以访问外层作用域。

闭包的应用

  • 避免全局变量的污染。
  • 能够读取函数内部的变量。
  • 可以在内存中维护一个变量。(大型框架和库常用闭包进行数据传递)

应用场景:

闭包通常用来创建内部变量,使得这些变量不能被外部随意修改,同时又可以通过指定的函数接口来操作。例如 setTimeout 传参、回调、IIFE、函数防抖节流柯里化、模块化等等

模块机制

模块模式:

  1. 必须有外部的封闭函数,该函数必须至少被调用一次。
  2. 封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或修改私有的状态。
var MyModules = (function Manager() {
    var modules = {};

    function define(name, deps, impl) {
        for (var i = 0; i < deps.length; i++) {
            deps[i] = modules[deps[i]]
        }
        // 为了模块的定义引入了包装函数(可以传入任何依赖),并且将返回值(模块的API),储存在一个根据名字来管理的模块列表中
        modules[name] = impl.apply(impl, deps); 
    }

    function get(name) {
        return modules[name];
    }

    return {
        define: define,
        get: get
    };
})(); // 单例模式 - 当只需要一个实例时

MyModules.define('bar', [], function() {
    function hello(who) {
        return 'Let me introduce: ' + who;
    }
    return {
        hello: hello
    };
});

MyModules.define('foo', ['bar'], function(bar) {
    var hungry = 'hippo';
    function awesome() {
        console.log(bar.hello(hungry).toUpperCase());
    }
    return {
        awesome: awesome
    };
});

var bar = MyModules.get('bar');
var foo = MyModules.get('foo');

console.log(bar.hello('hippo')); // Let me introduce: hippo
foo.awesome(); // LET ME INTRODUCE: HIPPO

调用包装了函数定义的包装函数,并且将返回值作为该模块的API。

闭包的缺点

  • 维护难度加大: 闭包内部是可以访问上级作用域,改变上级作用域的私有变量,我们使用的使用一定要小心,不要随便改变上级作用域私有变量的值。

  • 内存泄漏:由于闭包会使得函数中的变量都保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄漏。解决方法是,在退出函数之前,将不使用的局部变量全部删除(引用设置为 null ,这样就解除了对这个变量的引用,其引用计数也会减少,从而确保其内存可以在适当的时机回收)

    程序的运行需要内存。对于持续运行的服务进程,必须及时释放不再用到的内存,否则占用越来越高,轻则影响系统性能,重则导致进程崩溃。不再用到的内存,没有及时释放,就叫做内存泄漏

  • this 指向:闭包的 this 指向为 window / global

面试题

  1. for 循环输出 index

  2. 题目:https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/455

    var foo = function(...args) { 
        // 要求实现函数体
    }
    
    var f1 = foo(1,2,3); f1.getValue(); 
    // 6 输出是参数的和
    
    var f2 = foo(1)(2,3); f2.getValue(); 
    // 6
    
    var f3 = foo(1)(2)(3)(4); f3.getValue(); 
    // 10

    解答:

    function foo(...args) {
      const target = (...arg1s) => foo(...[...args, ...arg1s])
      target.getValue = () => args.reduce((p, n) => p+ n, 0)
      return target
    }

    个人认为这是用的递归。

参考链接

第165题:闭包的使用场景,使用闭包需要注意什么