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

闭包的定义
有人说所有的函数都是闭包,无论这句话对错与否,对我们理解闭包并没有什么帮助。
就像说万物皆对象一样,你总不能说 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,所以要沿着作用域链往上找。
这就存在两种情况:
- word()运行时在 hello()的作用域中,所以 a 是 13.
- word()定义时在全局作用域中,所以 a 是 11.
在 js 中这样的逻辑输出值最终为11.而在 Common Lisp 等动态作用域实现的语言里会输出 13.
至于为什么现代语言会采用函数定义时就确定的词法作用域,而不是运行时才能确定的动态作用域。当然是因为词法作用域更合理啦。
而要实现词法作用域就要使用闭包这种数据结构。
关于这方面,推荐垠神的博客 怎样写一个解释器.
结论
因为 js 自身的缺陷,所以为了实现模块化、块级作用域等所需的功能,所以人们不得不使用闭包。
而随着 ES6 的出现,JS 有了块级作用域,自带的模块机制。以前的各种闭包使用也就不怎么需要啦。但闭包的一些前因后果了解一下还是很有帮助的。
评论
发表评论