关于闭包的一些理解

对于闭包到底是什么,网上有有无数种解释,真是众说纷纭。这里对闭包的产生及运用做出一些整理。

闭包的定义

有人说所有的函数都是闭包,无论这句话对错与否,对我们理解闭包并没有什么帮助。
就像说万物皆对象一样,你总不能说 JS 的数据类型只有对象。

我觉得维基百科上的解释更为准确一些:

闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。

·

在函数中可以(嵌套)定义另一个函数时,如果内部的函数引用了外部的函数的变量,则可能产生闭包。运行时,一旦外部的 函数被执行,一个闭包就形成了,闭包中包含了内部函数的代码,以及所需外部函数中的变量的引用。

自由变量是指不在自身作用域的变量,如:

var a = 12;

function hello{
console.log(a);
}

在这里,hello 函数中的 a 即为自由变量,那么 hello 就是闭包了。但这怎么和我们平时看到的花里胡哨的闭包不太一样?

理论上,这就是闭包。然而实践上,要带上后面那句:

这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。

离开创造它的环境的意思是:即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回);

闭包与块级作用域

我以前一直有疑惑,为什么 只有 js 不停的说闭包,而用 java,python 的似乎从来不会有这种问题?原来这主要和 js 天生的缺陷有关,一个重要原因就是 js 没有块级作用域。

块级,直白的讲,就是{}包裹的区域。

在 es6 之前,js 只有全局作用域和函数作用域。所以就会出现各种 java 程序猿不会遇到的问题。

for (var i = 0; i < 3; i++) {
    console.log('hello');
}

 console.log(i);
 //这里输出 3

而如果在 java 中这么写,就会提示变量 i 未定义了,因为 i 起作用的区域只在 {} 里面。

因为没有块级作用域,所以 js 程序猿不得不使用一些更复杂的手段来实现本应该很简单的功能。

下面一道是常见面试题:

var data = [];

for (var i = 0; i < 3; i++) {
  data[i] = function () {
    console.log(i);
  };
}

data[0]();
data[1]();
data[2]();

我们的本意是分别输出 1,2,3。但最终结果都是 3。该代码等同于:

var data = [];
var i = 0;

  data[0] = function () {
    console.log(i);
  };
i = 1;
  data[1] = function () {
    console.log(i);
  };
i = 2;
  data[2] = function () {
    console.log(i);
  };
i = 3;  


data[0]();
data[1]();
data[2]();

如果要实现想要的效果,就要使用闭包。

var data = [];
for (var i = 0; i < 3; i++) {
 data[i] = (function (i) {
        return function(){
            console.log(i);
        }
  })(i);
}

data[0]();
data[1]();
data[2]();

这里通过 IIFE(立即执行表达式)来保持状态,实现闭包。每个函数都会保存当前环境,即都有自己的 i 变量。

这也太费劲了吧,JAVA 程序猿表示不能理解。幸好,在 ES6 中 let 实现了块级作用域,那上面的需求实现起来就简单了。

var data = [];

for (let i = 0; i < 3; i++) {
  data[i] = function () {
    console.log(i);
  };
}

data[0]();
data[1]();
data[2]();

闭包与垃圾回收

在前面,我们使用IIFE 保持状态来实现闭包。而状态的保持,也就是使自己的参数和变量不会被垃圾回收,那为啥它里面的变量不会被回收呢?

 function fn () {
        let a = 1;
        let b = 2;

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

    let foo = fn();
    foo();

在这里 fn() 返回了一个匿名函数,被变量 foo 引用,foo 所代表的匿名函数既是一个闭包。

又因为该函数对象被外部变量所引用,所以它的[[scop]]也就是作用域链无法清除。

所以变量 a,b 就会一直存在堆内存中,除非 foo 被回收。

结论:闭包和匿名函数、IIFE 并没有直接的联系,是否存在闭包要看函数对象及其自由变量是否会一直存在。

有闭包

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

无闭包

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

闭包与词法作用域

作用域有两种:词法作用域(静态作用域)和动态作用域,这和程序语言的解释器实现有关。现代的大多数程序语言都采用词法作用域。

标准语言编译器的第一个传统步骤称为词法分析(也就是分词),它们规定作用域是何时决定的。

举个🌰:

var a = 11;

function hello() {
    var a = 13;
    word();
    
}

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

hello()

这个代码的执行结果是什么呢?这里我们调用了 hello(),hello() 里调用了 word() ,word() 中输出了自由变量 a,因为自身没有变量 a,所以要沿着作用域链往上找。

这就存在两种情况:

  1. word()运行时在 hello()的作用域中,所以 a 是 13.
  2. word()定义时在全局作用域中,所以 a 是 11.

在 js 中这样的逻辑输出值最终为11.而在 Common Lisp 等动态作用域实现的语言里会输出 13.

至于为什么现代语言会采用函数定义时就确定的词法作用域,而不是运行时才能确定的动态作用域。当然是因为词法作用域更合理啦。

而要实现词法作用域就要使用闭包这种数据结构。

关于这方面,推荐垠神的博客 怎样写一个解释器.

结论

因为 js 自身的缺陷,所以为了实现模块化、块级作用域等所需的功能,所以人们不得不使用闭包。

而随着 ES6 的出现,JS 有了块级作用域,自带的模块机制。以前的各种闭包使用也就不怎么需要啦。但闭包的一些前因后果了解一下还是很有帮助的。

参考:
你不懂的 JS:作用域和闭包
为什么闭包不会被垃圾回收清除
前端模块化,AMD与CMD的区别
IIFE

评论

此博客中的热门博文

1. Angular 错误:ExpressionChangedAfterItHasBeenCheckedError