JavaScript基础–闭包
理解闭包的概念对于学习JavaScript至关重要,很多新手(包括我)开始学习闭包时,都会感觉似懂非懂,之前看了一些资料,整理了闭包的一篇博客,若有疏忽与错误,希望大家多多给意见。
概述
理解闭包的概念前,建议大家先回想一下JS作用域的相关知识,如果有疑问的同学,可以参考:JavaScript基础–作用域。闭包的定义如下:
Closure is when a function is able to remember and access its lexical scope even when that function is executing outside its lexical scope.
意译出来就是:当函数在其词法作用域外执行时,依然可以访问其词法作用域里的变量。这里的“词法作用域”,就是我们通常理解的作用域。
我们先来看个例子
eg1
function foo() { var a = 2; function bar() { console.log( a ); } bar(); } foo(); //--> 2
上述例子中,在调用函数bar()
时,变量a
的值是取自函数foo
的作用域,也就是函数bar()
的上层作用域,从闭包的概念来说,这个例子基本属于一个闭包。为什么说是“基本”,因为实际上a
也是属于函数bar()
的作用域链上的变量,我们更多称之为嵌套作用域。我们再看一个例子:
eg2
function foo() { var a = 2; function bar() { console.log( a ); } return bar; } var test = foo(); test(); // --> 2
eg2的例子也许更能体现闭包的概念:我们在定义函数foo()
时,返回的是一个函数;var test = foo();
将函数的引用赋值给test
,然后在执行test();
语句时,我们发现a
的值依然能够取到,我们称bar()
为一个闭包。
闭包的原理:编译器在执行var test = foo();
时,会标识其为一个闭包,垃圾回收器在回收内存时,就会保留闭包的作用域链。所以运行test()
时,就可以访问到闭包所定义的词法作用域了。
函数setTimeout()
其实我们自己在写JS代码时,经常用到闭包,只是我们没有意识到,比如setTimeout()
:
eg3
function wait(message) { setTimeout( function timer(){ console.log( message ); }, 1000 ); } wait( "Hello, closure!" );
相信同学们或多或少的用到过setTimeout()
,在eg3中我们细心注意,就可以发现我们定义在wait()
中的匿名函数是延迟运行的,但它依然可以访问到变量message
。对照闭包的概念,是不是就明白了。同样我们在定义很多异步的函数时,都用到了闭包。是不是发现闭包其实我们时时刻刻都在用。
循环中的闭包
大家还是先来看一个例子
eg4
for (var i=1; i<=5; i++) { setTimeout( function timer(){ console.log( i ); }, i*1000 ); }
大家觉得eg4中会输入什么?是1,2,3,4,5吗?如果你把代码赋值到浏览器console面板中,也许会让你失望,代码输出结果为6,6,6,6,6;很多同学觉得每一个i
不是单独运行的吗?输出怎么都是6。
分析这个例子前,我们脑子中要有一个概念:JS应用的是函数作用域,而不是块级作用域。反映到eg4中,就是循环中利用的i
是公有的。所以在执行timer()
时,i
已经变为了6。
如果应用JS的IIFE(立即执行函数),输出结果还是5个6吗?比如:
eg5
for (var i=1; i<=5; i++) { (function(){ setTimeout( function timer(){ console.log( i ); }, i*1000 ); })(); }
我们可以测试一下,结果还是6,6,6,6,6。或者有人认为如果把延迟时间缩小的足够短,结果是不是就可以正常了?实际结果也许会让你失望。我们就算把延迟时间设置为0,结果还是一样的,这是因为for
执行效率天生就比setTimeout()
高,setTimeout()
再怎么缩短延迟时间,也赶不上for
。
为了达到我们的期望的结果,解决的办法就是将每次循环的i
引入到time()
的作用域中,如:
eg6
for (var i=1; i<=5; i++) { (function(){ var j = i; setTimeout( function timer(){ console.log( j ); }, j*1000 ); })(); }
或者是:
eg7
for (var i=1; i<=5; i++) { (function(j){ setTimeout( function timer(){ console.log( j ); }, j*1000 ); })( i ); }
扩展:在ES6中引入了let
关键字,而引入的目的就是为了在JS中实现块级作用域,所以eg5中代码还可以修改为:
eg8
for (var i=1; i<=5; i++) { let j = i; setTimeout( function timer(){ console.log( j ); }, j*1000 ); }
或者
eg9
for (let i=1; i<=5; i++) { setTimeout( function timer(){ console.log( i ); }, i*1000 ); }
闭包的应用–模块(module)
模块是应用闭包的典型例子,我们先来看一个例子:
eg10
function CoolModule() { var something = "cool"; var another = [1, 2, 3]; function doSomething() { console.log( something ); } function doAnother() { console.log( another.join( " ! " ) ); } return { doSomething: doSomething, doAnother: doAnother }; } var foo = CoolModule(); foo.doSomething(); // cool foo.doAnother(); // 1 ! 2 ! 3
在eg10中CoolModule
是一个函数,返回值为一个对象;那么foo
在调用doSomething
和doAnother
时,就产生了闭包。这是module
中最简单的利用闭包的例子,接下来我们来看一个怎么解决module
依赖的例子。
eg11
定义依赖模块的实现
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: define, get: get }; })();
我们分析一下以上的代码。首先定义了一个空对象module
,其次定义了函数define
,其中三个参数:name
,定义模块的名称;deps
,定义模块的依赖项;impl
,定义模块的实现方法。
MyModules.define( "foo", ["bar"], function(bar){ var hungry = "hippo"; function awesome() { console.log( bar.hello( hungry ).toUpperCase() ); } return { awesome: awesome }; } ); var bar = MyModules.get( "bar" ); var foo = MyModules.get( "foo" ); console.log( bar.hello( "hippo" ) ); // Let me introduce: hippo foo.awesome(); // LET ME INTRODUCE: HIPPO
当然ES6中也引入了module,使得调用更加方便,直接看例子吧
eg12
bar.js
function hello(who) { return "Let me introduce: " + who; } export hello;
foo.js
// import only `hello()` from the "bar" module import hello from "bar"; var hungry = "hippo"; function awesome() { console.log( hello( hungry ).toUpperCase() ); } export awesome;
// import the entire "foo" and "bar" modules module foo from "foo"; module bar from "bar"; console.log( bar.hello( "rhino" ) ); // Let me introduce: rhino foo.awesome(); // LET ME INTRODUCE: HIPPO
注意在eg12中调用module有两种方式,分别是import
和module
,前者调用的是接口,而后者调用的是模块,用法也有些许不同,前者是直接接口本身hello(hungry)
,而后者则是调用模块中的方法bar.hello("rhino")
。