4
头图
本文将带你用正确姿势看待JavaScript闭包。
在 JavaScript 中闭包描述的是 function 中 外层作用域的变量 被内层作用域 引用的场景,闭包的结构为 内层作用域 保存了 外层作用域的变量。

要理解闭包,首先要知道 JS词法作用域 是如何工作的。

JS词法作用域(lexical scoping)

来看这段代码:

let name = 'John';

function greeting() { 
    let message = 'Hi';
    console.log(message + ' '+ name);
}

变量 name 是全局变量。它可以在任何地方调用,包括在 greeting 函数内部。

变量 message 是局部变量,只能在 greeting 函数内部调用。

如果你尝试从 greeting() 外部访问 message 变量,会抛出一个错误:

ReferenceError: message is not defined

比较有意思的是 函数内部的作用域是可以嵌套的,如下:

function greeting() {
    let message = 'Hi';

    function sayHi() {
        console.log(message);
    }

    sayHi();
}

greeting();
// Hi

greeting() 函数 创建了一个局部变量 message 和一个局部函数 sayHi()。

sayHi() 是 greeting() 的一个内部方法,只能在 greeting() 内部访问。sayHi() 可以访问 greeting() 的 message 变量。在 greeting() 内部调用了 sayHi(),打印出了变量 message 的值。

JavaScript闭包(closures)

来修改一下greeting:

function greeting() {
    let message = 'Hi';

    function sayHi() {
        console.log(message);
    }

    return sayHi;
}
let hi = greeting();
hi(); // 仍然可以获取到message的值

这次我们不是在 greeting() 执行 sayHi(),而是在 greeting() 被调用时把 sayHi 作为结果返回。

在 greeting() 函数外部,声明了一个变量 hi,它是 sayHi() 函数的索引。

这时,我们通过这个索引来执行 sayHi() 函数,可以得到和之前一样的结果。

通常情况下,一个局部变量只会在函数执行的时候存在,函数执行完成,会被垃圾回收机制回收。

有意思的是,上边的这种写法当我们执行 hi(),message 变量是会一直存在的。这就是闭包的作用,换句话说上面的这种形式就是闭包。

其他示例

下面的例子阐述了闭包更加实用的情况:

function greeting(message) {
   return function(name){
        return message + ' ' + name;
   }
}
let sayHi = greeting('Hi');
let sayHello = greeting('Hello');

console.log(sayHi('John')); // Hi John
console.log(sayHello('John')); // Hello John

greeting() 接收一个参数(message),返回了一个函数接收 一个参数(name)。

greeting 返回的匿名函数 把 message 和 name 做了拼接。

这时 greeting() 表现的行为像 工厂模式。使用它创建了 sayHi() 和 sayHello() 函数,它们都维护了各自的 message ”Hi“ 和 ”Hello“。

sayHi() 和 sayHello() 都是闭包。它们共用了同一个函数体,但是保存了不同的作用域。

防抖和节流

在面试的时候,经常会有面试官让你手写一个防抖,节流函数,其实用到的就是闭包。

如果有兴趣可以 查看一下这篇文章 《防抖和节流实例讲解》

好处和问题

闭包的优势

闭包可以在自己的作用域保存变量的状态,不会污染全局变量。因为如果有很多开发者开发同一个项目,可能会导致全局变量的冲突。

闭包可能导致的问题

闭包的优势可能会成为严重的问题,因为闭包中的变量无法被GC回收,尤其是在循环中使用闭包:

function outer() {
  const potentiallyHugeArray = [];

  return function inner() {
    potentiallyHugeArray.push('Hello'); // function inner is closed over the potentiallyHugeArray variable
    console.log('Hello');
  };
};
const sayHello = outer(); // contains definition of the function inner

function repeat(fn, num) {
  for (let i = 0; i < num; i++){
    fn();
  }
}
repeat(sayHello, 10); // each sayHello call pushes another 'Hello' to the potentiallyHugeArray 
 
// now imagine repeat(sayHello, 100000)

在这个例子中,potentiallyHugeArray 会随着循环的次数增加而无限增大而导致内存泄漏(Memory Leaks)。

总结

闭包既有优势,也会导致问题。只有理解了它的原理,才能让它发挥正确的作用。

文章首发于 IICOOM-个人博客 《JavaScript闭包》


来了老弟
508 声望31 粉丝

纸上得来终觉浅,绝知此事要躬行