likes
comments
collection
share

面试官:说说闭包吧。我:那还要从作用域说起

作者站长头像
站长
· 阅读数 6

前言

面试中经常会问及闭包,以及在实际开发过程中我们也接触过很多闭包,为了更好的解闭包、使用闭包,全面的了解闭包的过往今来还是很有必要的。

为了更全面的分析闭包,我们在上一章学习了执行上下文的相关知识,有了一定的知识储备后,再来看本篇的作用域和闭包,或许能给你带来不一样的思考。

本文为面试专题闭包相关之作用域与闭包。

作用域

简单来说,作用域指程序中定义变量的区域,它决定了当前执行代码对变量的访问权限。

JavaScript 中大部分情况下,只有两种作用域类型🤓:

  • 全局作用域:全局作用域为程序的最外层作用域,一直存在。
  • 函数作用域:函数作用域只有函数被定义时才会创建,包含在父级函数作用域 / 全局作用域内。
/* 全局作用域 开始 */
var a = 1;

function fn () { /* fn 函数作用域 开始 */
  var a = 2;
  console.log(a);
}                  /* fn 函数作用域 结束 */

fn(); // => 2

console.log(a); // => 1
/* 全局作用域 结束 */

作用域链

从刚才的用列中我们发现,函数是能够从自己内部作用域中找到变量a的;对于在函数内部没有找到变量的情况是怎样的?

/* 全局作用域 开始 */
var a = 1
var b = 2

function fn(){  /* fn 函数作用域 开始 */
    var c = 3
    console.log(a + b + c) // 6
}              /* fn 函数作用域 结束 */
/* 全局作用域 结束 */

我们可以发现的是,函数在内部未找到变量 ab 时,它会从所在的上级作用域查找。

这看起来有点像集合的包含概念,本地作用域 => 父级作用域 => 全局作用域

找到目标直接返回,否则会一直向上查找,而这就是所谓的作用域链。

👀注:函数参数是在函数作用域中的

function fn(a){ console.log(a) } 这里的 a 是属于函数 fn 的作用域中的

块级作用域

简单来说,花括号内 {...} 的区域就是块级作用域区域。

👀注:JavaScript 不是原生支持块级作用域的

ES6标准之后letconst创建的为块级作用域

测试代码:

if (true) {
  var a = 1;
}
console.log(a); // 1

if (true) {
  let a = 1;
}
console.log(a); // ReferenceError

词法作用域

在了解了作用域和作用域链之后,我们继续分析动态作用域和静态作用域。

先看个用例,然后简单思考下,JavaScript使用的是哪种?

var value = 1;

function foo() {
    console.log(value);
}

function bar() {
    var value = 2;
    foo();
}

bar();

// 结果是 ???

先说结果,打印值是 1

再说结论,JavaScript是采用词法作用域的,与之相对的是动态作用域(bash是动态的)。

  • 词法作用域,也被称为 “静态作用域”,意味着函数被定义的时候,它的作用域就已经确定了,和拿到哪里执行没有关系。
  • 这就意味着函数的执行依赖于函数定义的时候所产生(而不是函数调用的时候产生的)的变量作用域。

核心就是: 函数的作用域在函数定义的时候就决定了。

为了去实现这种词法作用域,JavaScript函数对象的内部状态不仅包含函数逻辑的代码,除此之外还包含当前作用域链的引用。函数对象可以通过这个作用域链相互关联起来,如此,函数体内部的变量都可以保存在函数的作用域内,这在计算机的文献中被称之为闭包。

严格意义上所有的函数都是闭包😮。

需要注意的是:我们常常所说的闭包指的是让外部函数访问到内部的变量,也就是说,按照一般的做法,是使内部函数返回一个函数,然后操作其中的变量。这样做的话一是可以读取函数内部的变量,二是可以让这些变量的值始终保存在内存中。

闭包

在前面我们说到了,函数是可以向外查找父作用域的变量,而不能向内查找子函数作用域内的变量,这是不允许的。我们调整下代码:

/* 全局作用域 开始 */
var a = 1
var b = 2

function fn(){  /* fn 函数作用域 开始 */
    var c = 3
    
    console.log(c + d)
    function foo() {  /* foo 函数作用域 开始 */
        var d = 4 
    }                 /* foo 函数作用域 结束 */
}              /* fn 函数作用域 结束 */
/* 全局作用域 结束 */

如果你尝试了,控制台会输出 Uncaught ReferenceError: d is not defined 这样一个错误。

现在我们再来看一下什么是闭包?

闭包是指有权访问另一个函数作用域中变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,创建的函数可以访问到当前函数的局部变量。

也可以这样理解,闭包就是函数内部定义的函数,被返回了出去并在外部调用。

这样看,闭包的这种变体是不是就相当于,可以从外部获取一个函数内部的变量信息

👀注意:这里是一个类比。

不过也正是因为闭包的这种特性,才极大的丰富了开发人员的编码方式。

闭包有两个常用的用途:

  • 创建私有变量:闭包使我们在函数外部能够访问到函数内部的变量,使用闭包,可以在外部调用闭包函数,从而在外部访问到函数内部的变量
  • 保留变量对象在内存中:使已经运行结束的函数上下文中的变量对象继续留在内存中,因为闭包函数保留了这个变量对象的引用,所以这个变量对象不会被回收。
function foo() {
  var a = 2;

  function bar() {
    console.log( a );
  }

  return bar;
}

var baz = foo();

baz(); // 这就形成了一个闭包

认识到指针的作用,foo()的执行结果 bar函数被返回赋值给了 baz,这时虽然 baz在作用域之外,但是指针还是指向的 bar,因此哪怕它位于 foo 作用域之外,它还是能够获取到 foo 的内部变量。

垃圾回收机制

先了解下GC是什么?GC是Garbage Collection的缩写,意为垃圾回收。

C语言和C++不同,因为没有GC,需要程序员手动管理内存;JavaJavaScript等高级语言是带有GC的, JavaScript的GC是工作在V8引擎内部的,不需要用户手动管理。

一般常用的GC算法有:标记清除、引用计数法。因为引用计数算法的循环引用问题的存在,现代浏览器中主要使用的是标记清除算法。

标记清除算法是先把所有活动对象做上标记,在清除阶段的时候回收没有打上标记的对象

GC标记类似一个定时任务,它会在下次执行的时候回收不再被标记(不再使用)的内存。

内存泄露

是指当一块内存不再被应用程序使用的时候,由于某种原因,这块内存没有返还给操作系统或者内存池的现象。内存泄漏可能会导致应用程序卡顿或者崩溃。

回想下,在上一章中我们学习的执行上下文的销毁阶段是怎样的(创建阶段、执行阶段、销毁阶段)?

  1. 执行上下文是以执行栈的形式进行管理的,遵循后进先出(LIFO)全局作用域会一直存在栈的底部
  2. 引擎会执行那些执行上下文位于栈顶的函数。当该函数执行结束时,执行上下文从栈中弹出,控制流程到达当前栈中的下一个上下文。

也就是说,原本 foo 函数执行完之后,就应该弹出 foo 的执行栈,并销毁 foo 的执行上下文的。但是在闭包场景中, foo 内部的 bar 函数使用了 foo 作用域中的变量,形成了一个作用域链,导致 foo 函数不能释放。

再看下这个闭包:

function foo() {
  var a = 2;

  function bar() {
    console.log( a );
  }

  return bar;
}

var baz = foo();

baz(); // 这就形成了一个闭包

导致内存泄漏的行为操作:

  • 全局变量的无意创建
  • 闭包使用过度导致内存占用无法释放的情况
  • DOM 的事件绑定(移除 DOM 元素前如果忘记了注销掉其中绑定的事件方法,也会造成内存泄露)

解决方案:

  • 使用严格模式
  • 避免过度使用闭包。
  • 关注 DOM 生命周期,在销毁阶段记得解绑相关事件(或者可以使用事件委托的手段统一处理事件,减少由于事件绑定带来的额外内存开销)

总结

  • JavaScript 语言层面只原生支持两种作用域类型:全局作用域函数作用域
  • JavaScript不是原生支持块级作用域的,ES6标准之后letconst创建的为块级作用域
  • JavaScript是采用词法作用域的,函数的作用域在函数定义的时候就决定了,和拿到哪里执行没关系
  • 闭包的本质是利用了作用域的机制,来达到外部作用域访问内部作用域的目的
  • 过度使用闭包容易导致内存泄漏

交流

面试相关的文章及代码demo,后续打算在这个仓库(JS-banana/interview: 面试不完全指北 (github.com))进行维护,欢迎✨star,提建议,一起进步~

资料