| JavaScript对话面试官系列 | - 前言与闭包

1,029 阅读6分钟

起因

当我输入了大量来自优质个人博客文章,经典书籍,名牌讲师的 JavaScript 系统知识之后,我反而变得有些困惑。我明明可以流利的书写八大继承,流利的书写 Underscore 库中的防抖节流,深拷贝……,我也知晓什么是闭包,什么是迭代器,生成器……。但是问题来了,当我试图将一个 JavaScript 核心概念,比如闭包……,介绍给同学,讲解给自己,对话面试官的时候。原本自认为可以脱口而出的语言,到嘴边却显得吞吞吐吐。只能用只言片语,或者东拼西凑的知识点来表达给对方。这无疑让我有了一种,虽然花了大量时间但是却从未拥有过 JavaScript 的感觉。

目的

所以这个系列要尝试解决的问题就是当别人询问或者考察我JavaScript 核心概念的时候,我可以尽可能流畅的,清晰的表达给他人。

期望

希望掘金优秀的前端技术人员和前辈们可以在百忙之中多多补充这篇文章,多多审查这篇文章,多多提问我。我期望可以通过这个系列来解决我当前的问题。

你说一下你是怎么样理解闭包的?

首先要告诉面试官闭包的定义是什么,你自己如何来定义闭包?

回答闭包的定义

首先我想和面试官聊一聊闭包的定义,闭包的定义在众多经典书籍里的呈现其实大同小异。《JavaScript忍者秘籍》说闭包是纯函数式编程语言的一个特性,《你不知道的JS》说闭包是基于词法作用域书写代码时所产生的自然结果。所以在我看来,闭包就是书写 JavaScript 代码所产生的自然结果。

其次要告诉面试官闭包的解释是什么,你自己如何来解释闭包?

回答闭包的解释

其次我想和面试官聊一聊闭包的解释,闭包的解释在众多经典书籍里的呈现仍然不谋而合。《红宝书》说闭包是指有权访问另一个函数作用域中变量的函数。MDN 说闭包是一个可以访问自由变量的函数。《JavaScript 权威指南》当中说 JavaScript 当中所有的函数都是闭包。在我看来,闭包的解释就是外层函数返回内层函数,内层函数在外部被调用但是仍然可以引用到其外层函数的变量。其实就是《你不知道的 js》 这本书当中说的:无论通过何种手段将内部函数传递到所在词法作用域之外, 内部函数会持有对原始作用域的引用,这种引用就是闭包。

最后告诉面试官闭包的本质是什么,你自己如何理解闭包的本质?

回答闭包的本质

最后我想和面试官聊一聊闭包的本质,闭包的本质在经典书籍里的呈现任然保持相同。书籍当中谈到闭包本质时总是绕不开,作用域,作用域链,执行上下文和垃圾回收机制,这些 JavaScript 重要的核心概念。我认为闭包的本质就是外层函数执行上下文的变量环境对象在内存中被内层函数的作用域链引用,而且根据 JavaScript 垃圾回收机制并不会销毁这一引用,所以内层函数作用域才可以通过作用域链引用外层函数作用域,所以才有了闭包这种自然结果。

你说一下闭包的意义和闭包的应用场景?

首先要告诉面试官闭包的意义,你自己怎么样运用和理解闭包的意义?

回答闭包的意义

通过经典书籍当中应用闭包的代码示例。我认为闭包的意义就是为 JavaScript 函数增加高级特性,帮助我们减少代码的数量和复杂性。在高级代码库当中也经常找到闭包的使用。比如在 Vue2 源码当中的 createPatchFunction 函数当中,外层 createFunction 函数保存的是各个平台下对应的不同的 modules 和 nodeOps,返回的闭包 patch 函数是各个平台相同的主要逻辑部分。利用闭包将差异化的参数提前固化。大大减少了代码的数量和复杂性。

最后告诉面试官闭包的应用场景,这些应用场景解决了什么样的问题?

回答闭包的应用场景

经典书籍对于闭包的使用场景数不胜数。比如使用闭包实现缓存记忆,使用闭包实现一个模块模式来返回可以访问私有变量和函数的特权接口,使用闭包模拟面向对象特征,使用闭包实现众多设计模式如命令模式,单例模式,状态模式,职责链模式,策略模式等。使用闭包实现偏应用函数,组合函数,柯里化函数。使用闭包和立即执行函数创建一个独立的作用域。使用闭包和立即执行函数解决闭包与循环的问题……。

  • 列举一个示例:闭包实现缓存记忆
Function.prototype.memorize = function () {
  const fn = this;
  return function () {
    return fn.memorized.apply(fn, arguments);
  };
};
Function.prototype.memorized = function (key) {
  this._values = this._values || {};
  if (this._values[key] == undefined) {
    console.log("缓存了" + this.name + "函数");
    this._values[key] = this.apply(this, arguments);
  } else {
    console.log("执行缓存结果");
  }
  return this._values[key];
};
const prime = function (a) {
  // 执行逻辑
  return a;
};
const primeFun = prime.memorize();
console.log(primeFun(1));
console.log(primeFun(1));
// 缓存了prime函数;
// 1;
// 执行缓存结果;
// 1;

你说一下闭包带来了什么样的问题?

回答闭包的带来的问题,你是怎么样解决,你是怎么样理解这个问题的?

闭包带来的最大的问题就是闭包的内存泄露。我认为内存泄露的本质就是代码执行完毕之后,内存当中的内层函数对象和外层函数执行上下文的变量对象仍然没有删除且占据内存。造成了内存泄露。我们可以利用 JavaScript 垃圾回收机制去解决闭包的内存泄露问题。我们可以手动销毁全局变量和内层函数的引用。如 fn = null;

function out() {
  const name = "cyan";
  return function in() {
    console.log(name);
  }
}
const fn = out();
fn();
fn = null;

参考

  • 《JavaScript高级程序设计》
  • 《你不知道的JavaScript》
  • 《JavaScript忍者秘籍》
  • 《JavaScript语言精髓》