闭包(closure)的介绍

222 阅读5分钟

什么是闭包?

闭包(closure)是一个函数以及其捆绑的周边环境状态(lexical environment词法环境)的引用的组合。换而言之,闭包让开发者可以从内部函数访问外部函数的作用域。在 JavaScript 中,闭包会随着函数的创建而被同时创建。

闭包与作用域有关,ES5之前只有全局作用域函数作用域,因此var也只有全局作用域和函数作用域, ES6引入let,const新增块级作用域,所以js中的作用域包括:全局作用域,函数作用域和块级作用域; 用一个例子介绍一下闭包:

function makeFunc() {
var name = "Mozilla";
function displayName() {
 alert(name);
}
return displayName;
}

var myFunc = makeFunc();
myFunc();

在一些编程语言中,一个函数中的局部变量仅存在于此函数的执行期间。一旦makeFunc() 执行完毕,你可能会认为 name 变量将不能再被访问。然而,因为代码仍按预期运行,所以在 JavaScript 中情况显然与此不同。

原因在于,JavaScript 中的函数会形成了闭包。 闭包是由函数以及声明该函数的词法环境组合而成的。该环境包含了这个闭包创建时作用域内的任何局部变量。在本例子中,myFunc 是执行 makeFunc 时创建的 displayName 函数实例的引用。displayName 的实例维持了一个对它的词法环境(变量 name 存在于其中)的引用。因此,当 myFunc 被调用时,变量 name 仍然可用,其值 Mozilla 就被传递到alert中。

作用域链:JavaScript 利用作用域链来解析变量查找。每个执行上下文都有对其外部(封闭)上下文的引用,从而创建 JavaScript 引擎用来搜索变量值的链。该链是通过词法作用域建立的,其中代码中函数和块的放置决定了作用域层次结构。

闭包有哪些作用?

闭包很有用,因为它允许将函数与其所操作的某些数据(环境)关联起来。

数据封装和隐私:

闭包允许在模块或对象内创建私有变量和方法,类似于基于类的语言中的私有成员。该模式广泛用于模块模式、揭示模块模式以及在 JavaScript 中实现面向对象的概念。

var Counter = (function () {
  var privateCounter = 0;
  function changeBy(val) {
    privateCounter += val;
  }
  return {
    increment: function () {
      changeBy(1);
    },
    decrement: function () {
      changeBy(-1);
    },
    value: function () {
      return privateCounter;
    },
  };
})();

console.log(Counter.value()); /* logs 0 */
Counter.increment();
Counter.increment();
console.log(Counter.value()); /* logs 2 */
Counter.decrement();
console.log(Counter.value()); /* logs 1 */

柯里化函数:

柯里化是一种函数式编程技术,其中具有多个参数的函数被转换为一系列函数,每个函数都具有单个参数。闭包对于实现柯里化函数至关重要,因为它们允许函数保留传递给它的参数,直到提供所有参数。

function multiply(a) {
return function(b) {
return a * b;
};
}
const double = multiply(2);
console.log(double(5)); // Outputs: 10

事件回调函数也是一种闭包

Web 中很常见一种是基于事件的定义某种行为,然后将其添加到用户触发的事件之上。我们的代码通常作为回调:为响应事件而执行的函数。

function showHelp(help) {
  document.getElementById("help").textContent = help;
}

function setupHelp() {
  const helpText = [
    { id: "email", help: "Your e-mail address" },
    { id: "name", help: "Your full name" },
    { id: "age", help: "Your age (you must be over 16)" },
  ];

  for (let i = 0; i < helpText.length; i++) {
    const item = helpText[i];
    document.getElementById(item.id).onfocus = () => {
      showHelp(item.help);
    };
  }
}

setupHelp();

这里在循环中创建了三个闭包(响应事件回调函数),根据词法作用域,它们可以访问:

 // 定义时各自独立的块级作用域变量:
  const item = helpText[i];
 // 共享的函数作用域变量:
    const helpText = [
    { id: "email", help: "Your e-mail address" },
    { id: "name", help: "Your full name" },
    { id: "age", help: "Your age (you must be over 16)" },
  ];
// 共享的全局作用域变量:
    function showHelp(help) {
      document.getElementById("help").textContent = help;
   }

可以把const item = helpText[i]中写成 var item = helpText[i] 代码会符合预期吗😊?

哪些实际场景中使用了闭包?

这里以React官网,对useState例子介绍,函数创建伴随着闭包产生,内部嵌套函数setState逻辑比较简单,将传入变量赋值给需要更新值pair 也就是state返回内容,这里React 通过在组件之外的一层定义 componentHooks, currentHookIndex(这些状态不与其他组件共享),来维护不同useState对应各自state渲染顺序,这也是为什么React规定useHook不能出现在条件和循环中,因为这样每次渲染,hooks顺序都可能发生改变,导致渲染一致问题,具体可以看这篇博客

let componentHooks = [];
let currentHookIndex = 0;

// useState 在 React 中是如何工作的(简化版)
function useState(initialState) {
  let pair = componentHooks[currentHookIndex];
  if (pair) {
    // 这不是第一次渲染
    // 所以 state pair 已经存在
    // 将其返回并为下一次 hook 的调用做准备
    currentHookIndex++;
    return pair;
  }

  // 这是我们第一次进行渲染
  // 所以新建一个 state pair 然后存储它
  pair = [initialState, setState];

  function setState(nextState) {
    // 当用户发起 state 的变更,
    // 把新的值放入 pair 中
    pair[0] = nextState;
    updateDOM();
  }
    // 存储这个 pair 用于将来的渲染
  // 并且为下一次 hook 的调用做准备
  componentHooks[currentHookIndex] = pair;
  currentHookIndex++;
  return pair;
}

function Gallery() {
  // 每次调用 useState() 都会得到新的 pair
  const [index, setIndex] = useState(0);
  const [showMore, setShowMore] = useState(false);

  function handleNextClick() {
    setIndex(index + 1);
  }

  function handleMoreClick() {
    setShowMore(!showMore);
  }
  ...
 }

闭包需要性能考量

如果不是某些特定任务需要使用闭包,在其他函数中创建函数是不明智的,因为闭包在处理速度和内存消耗方面对脚本性能具有负面影响。

例如,在创建新的对象或者类时,方法通常应该关联于对象的原型,而不是定义到对象的构造器中。原因是这将导致每次构造器被调用时,方法都会被重新赋值一次(也就是说,对于每个对象的创建,方法都会被重新赋值)。

function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
  this.getName = function () {
    return this.name;
  };

  this.getMessage = function () {
    return this.message;
  };
}
function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
}
MyObject.prototype.getName = function () {
  return this.name;
};
MyObject.prototype.getMessage = function () {
  return this.message;
};

继承的原型可以为所有对象共享,不必在每一次创建对象时定义方法。参见 对象模型的细节 一章可以了解更为详细的信息。

参考: