当我们在使用 JS 编程时,闭包的概念其实已经无处不在了,可以用「润物细无声」来比喻。首先什么是闭包?我们先用一句话来解释。
当函数可以记住并访问所在的词法作用域时,就产生了闭包
对于这句话可以提炼出两个关键点,闭包整个概念就是围绕这两点展开。
- 记忆
- 词法作用域
记忆
怎么理解记忆 这个概念,要从 JS 变量环境上下文开始讲起。JS 运行完一段代码后,会去销毁当前的变量作用域,而记忆会使得销毁过程失效,换言之,使用闭包时是无法销毁已经运行过的变量作用域的。
可以看下面的一个例子
js
function foo() {
var a = 2;
function bar() {
return a;
}
return bar;
}
var baz = foo();
baz(); // 输出2;
在上面的例子中,foo()
执行完毕后,根据 JS 的垃圾回收机制,foo()
的作用域会被销毁,从而释放内存空间。但当我们执行 baz()
时,发现仍然可以访问到曾经 foo
函数内部的变量 a
。 所以上面的例子中 foo
函数的作用域并没有被回收,还是在使用中。这就是闭包的作用。
在《你不知道的JavaScript》(上) 中对于这种闭包作用解释如下
闭包使得函数可以继续访问定义时的词法作用域。
很明显,闭包的特征之一就是函数内部的函数,函数内部的函数可以访问到其上部作用域的变量。另外一个特征就是函数内部的函数可以被外部访问,这就需要将内部函数的引用传递出来,使得外部可以调用,不管通过什么方式。
词法作用域
词法作用域就是定义在词法阶段的作用域
词法作用域的概念还是很抽象。我们先从作用域的概念入手,什么是作用域?
作用域被定义为一套规则,这套规则用来管理引擎如何在当前作用域及嵌套的子作用域中根据标识名称进行变量的查找。
所以词法作用域也是一套规则,是在编写代码时定义的一套JS引擎查找变量所遵循的规则。
还是下面这段例子代码
js
function foo() {
var a = 2;
function bar() {
return a;
}
bar();
}
foo();
在上面的例子中,有3个逐级嵌套的作用域。作用域1:foo
函数的外部的作用域;作用域2:foo
函数内部的作用域;作用域3:bar
函数内部的作用域。这些作用域在编写代码时就已经确定了。
在拥有了词法作用域后,引擎会开始根据词法作用域来进行变量的查找,查找同样有一套规则。规则描述起来也很简单:引擎会在最内部的作用域开始,一级一级(逐级)的向上寻找所需要的变量,直到匹配到所需要的变量为止。
得益于这种机制,才使得闭包可以实现:函数内部的函数可以根据查找变量的规则,获取到内部函数上一级词法作用域中的变量。而内部的函数通过某种方式(如 return 或者值引用的方式)被外部调用,使得包含内部函数的函数执行完毕后,内部的作用域没有被销毁,反而被保存了下来。
所以闭包到底有什么用?
在前端开发中,如果回调函数访问了该函数外部的变量,那么这个回调函数就是闭包。
在 React 开发中,useState 的使用也是一种闭包应用场景:
js
function CountApp() {
const [count, setCount] = useState(0);
const handleClick = () => { // 本身作为回调函数
console.log(count); // 访问了外部作用域 count
}
return (
<button onClick={handleClick}>click!</button>
);
}
另外一种已经些许过时的闭包使用场景就是「模块」,不知道大家还记得有框架使用 define
来定义模块函数?
这是一种属于 AMD 规范的模块定义机制
js
// 定义模块 bar
MyModules.define('bar', [], function() {
function hello(who) {
return who;
}
return {
hello: hello,
};
});
// 定义模块 foo
MyModules.define('foo', ['bar'], function(bar) {
var hungry = 'hungry';
function awesome() {
return bar.hello(hungry);
}
return {
awesome,
};
});
var bar = MyModules.get('bar');
var foo = MyModules.get('foo');
bar.hello('me');
foo.awesome(); // 'hungry';
我们通过 MyModules 定义了两个模块函数,并且展示了如何设置模块间的依赖,并且可以在同一个模块中不通函数之间进行调用。那么 MyModules
内部是如何实现的呢?
js
var MyModules = (function Manager() {
var modules = {};
function define(name, deps, impl) {
for (var i = 0; i < deps.length; i++) {
deps[i] = modules[deps[i]];
}
modules[name] = impl.apply(impl, deps);
}
function get(name) {
return modules[name];
}
return {
define,
get,
};
})();
MyModules 内部维护了一个 modules 对象,通过 define 函数将新增的模块函数添加进 modules 对象中,并通过 IIFE 来实现单例的效果。没错,IIFE 本身也是一种闭包实现。
IIFE:自执行函数,在当下的实际开发中越来越少直接接触到 IIFE 了。IIFE 可以创建一个对立的作用域,避免全局污染。其重要特点是在定义时只执行一次。所以对于上述的 MyModules 而言,后续的每次调用其实始终调用的都是之前定义时得到的对象,这也就解释了为什么 IIFE 可以实现单例模式。
对于 ES6 来说,已经很少使用上述的「模块」和 IIFE 的方式了,最常见的 export
和 import
可以解决大部分模块定义和导出的问题。
闭包在 JS 中使用中如影随形,了解其原理和使用场景也很有必要。
欢迎大家互相交流。
本文例子大部分引用自 《你不知道的JavaScript》(上)。