前端百题斩【013】——用“闭包”问题征服面试官

600 阅读4分钟

写该系列文章的初衷是“让每位前端工程师掌握高频知识点,为工作助力”。这是前端百题斩的第13斩,希望朋友们关注公众号“执鸢者”,用知识武装自己的头脑。

img

13.1 定义

在JavaScript中,根据词法作用域的规则,内部函数总是可以访问其外部函数声明的变量,当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,就把这些变量的集合称为闭包。

13.2 闭包实现

在一个函数中嵌套另一个函数或者将一个匿名函数作为值传入另一个函数中。

// 函数fun1中嵌套了fun2,fun2作为参数返回,次吃调用时仍能打印val1,构成闭包
function fun1() {
    const val1 = 10;
    function fun2() {
        console.log(val1);
    }

    return fun2;
}

function fun3() {
    const val2 = 20;
    // 定时器中的为一个匿名函数,其作为参数传入了,函数fun3执行完毕之后,1s钟后才会执行定时器函数,但此时还能打印val2,构成闭包
    setTimeout(function() {
        console.log(val2);
    }, 1000);
}

13.3 流程

根据下面的函数来看看闭包的整个执行流程

function main() {
    const val1 = 20;
    var val2 = 2
    function valResult() {
        return val1 * val2;
    }

    return valResult;
}

var result = main();
console.log(result()); // 40

image.png

image.png

image.png

上图中展示了各个时期的调用栈,需要重点关注以下几点:

  1. 当main函数执行完毕后,main函数的执行上下文从栈顶弹出;
  2. 返回的方法(valResult)中调用了main函数中的val1和val2变量,这两个变量就会打包成closure闭包,加到[[scopes]];
  3. 调用返回的方法时,作用域链为:result函数作用域——Closure(main)——全局作用域

13.4 优缺点

  1. 优点

    (1)可以重复使用变量,并且不会造成变量污染;

    (2)可以用来定义私有属性和私有方法

  2. 缺点

    (1)会产生不销毁的上下文,导致栈/堆内存消耗过大

    (2)会造成内存泄露。

扩展:闭包是怎么回收的?

  1. 如果闭包引入的函数是一个全局变量,那么闭包会一直存在直到页面关闭;但如果这个闭包以后不再使用的话,就会造成内存泄露;
  2. 如果引用闭包的函数是一个局部变量,等函数销毁后,在下次JavaScript引擎执行垃圾回收时,判断闭包内容已经不再被使用,则js引擎的垃圾回收器就会进行回收。

13.5 用途

闭包用途主要有以下两个:

  1. 创建私有变量
function MyName(name) {
    return {
        getName() {
            return name;
        }
    }
}

const myName = MyName('lili');
// 只能通过getName访问对应的名字,别的方式访问不到
console.log(myName.getName()); // lili
  1. 作为回调函数。当把函数作为值传递到某处,并在某个时刻进行回调的时候就会创建一个闭包。例如定时器、DOM事件监听器、Ajax请求。
function fun(name) {
    setTimeout(() => {
        console.log(name);
    }, 1000);
}

fun('linlin');

13.6 经典闭包问题

多个子函数的[[scope]]都是同时指向父级,是完全共享的。因此当父级的变量对象被修改时,所有子函数都受到影响。

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

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

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

1.如果觉得这篇文章还不错,来个分享、点赞吧,让更多的人也看到

2.关注公众号执鸢者,与号主一起斩杀前端百题。