1

这是上月面试碰到的一道面试题,作为一个有着十年开发经验七年前端经验的大龄青年,碰到这种没有一点铺垫的八股文,真的只想说一句毁灭吧。

记得以前刚做前端不久的时候,就在百度搜闭包,大多搜到的结果都说的是闭包是什么样子的,比如说在函数外部可以访问到函数内部的变量,又或者说闭包会导致什么问题,比如会影响GC回收。总之没什么标准说法,就好像现在网上的吐槽,你背面试题没用,得和面试官背的同一套才行,但是天知道面试官学的是哪一套。

《JavaScript高级程序设计》作为前端的必看书籍,算是比较受认可的正经学习途径。它给出了如下关于闭包的定义:

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

这里面有两个关键词:作用域和函数。

众所周知,JS中声明一个函数,会在函数内部形成一个新的作用域,在这个作用域中可以访问包含了这个函数声明的外部作用域中的变量和函数,形成一个作用域链,但正常情况下,外部是访问不到这个函数作用域内部的变量和函数的。

在函数执行时,要从作用域链中查找变量,以便读、写值。

每个函数执行时,其执行上下文中都会有一个包含其中变量的对象。全局上下文中的叫变量对象,它会在代码执行期间始终存在;而函数局部上下文中的叫活动对象,只在函数执行期间存在。

作用域链其实是一个包含指针的列表,每个指针分别指向一个变量对象。

比如:

function compare(value1, value2) {
  if(value1 < value2) {
    return -1;
  } else if(value1 > value2) {
    return 1;
  } else {
    return 0;
  }
}
let result = compare(5, 10);
  • 在定义compare()函数时,就会为它创建作用域链,预装载全局变量对象,并保存在内部的[[Scope]]中;
  • 在调用compare()函数时,会创建相应的执行上下文,然后通过复制函数的[[Scope]]来创建其作用域链;接着会创建函数的活动对象(用作变量对象)并将其推入作用域链的前端。

函数执行完毕后,局部活动对象会被销毁,内部作用域会被销毁,内存中就只剩下全局作用域。

闭包就制造了一个例外的情况,使得在函数作用域外部,也能访问到函数内部的变量,这是因为通过某种方式(比如函数返回值)把函数内部变量的引用暴露给外部,并且外部也存在对其内部变量的引用,即函数外部存在对函数内部作用域的引用,使得函数执行完毕后,作用域也无法被销毁。这就会影响内存回收,可能造成内存泄漏,所以建议仅在十分必要时使用。

闭包的例子:

setTimeout(() => console.log(value * 2), 1000);

前置知识,函数的参数也存在于函数的内部作用域。

这里调用了setTimeout定时器方法,它的第一个参数是函数(或者是可以通过eval转为函数的string),真正执行这个函数的是浏览器运行时,所以在setTimeout外部存在对这个函数的引用,即在setTimeout外部存在对其内部作用域的引用,生成了一个闭包,当这个函数出列被执行后,setTimeout的作用域链才会被销毁。

虽然闭包有其弊端,但是也有可以利用的地方。

  1. 防止变量外溢,全局污染
    在前端开发中,经常会使用一些三方库,这很可能会造成与业务代码的冲突,此时三方库可以借助立即执行函数(IIFE),将必要的变量通过函数返回,或者挂在某个特定的全局变量上暴露给外部,而其他内部变量则不会被访问到,也就不会与业务代码冲突;这在团队协作中也很有用,防止与他人的代码造成不必要的冲突。
  1. 访问私有变量
    有时我们想更规范代码,将相关业务的代码放在一起,此时可以通过函数来将这些代码集合起来,这样外部也不能轻易修改这部分代码的变量数据,保障了数据的安全性。但有时我们需要访问内部的私有变量,此时可以通过创建能够访问函数私有变量/函数的公有方法,并对外暴露此方法的引用形成闭包,来达到访问内部私有变量的目的。
  1. 创建模块
    属于对访问私有变量的一种扩展使用。一个模块通常用对象来表示,就是一些变量和一些函数的集合,通过对象的属性来访问这些变量和函数。
    模块可能有一些初始化操作,和一些不想暴露给外部的私有变量/函数,就可以通过立即执行函数,将这些操作和变量/函数限制在内部作用域,在执行后返回一个模块对象,在这个对象上可以暴露一些公有属性和公有方法。

beckyyyy
550 声望414 粉丝

工作多年的一只前端菜鸟