知识点
1.实质问题
当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。
function foo(){ var a = 2; function bar(){ console.log(a); // 2 } bar()}foo()复制代码
根据前面的定义,严格来说上述代码并不是闭包,最准确地用来解释bar()对a的引用的方法是词法作用域的查找规则,而这些规则只是闭包的一部分。
function foo(){ var a = 2; function bar(){ console.log(a); } return bar;}var baz = foo();baz(); // 2 ---- 这就是闭包的效果复制代码
上述代码中,在foo()执行后,其返回值(也就是内部的bar()函数)赋值给变量baz并调用baz(),实际上只是通过不同的标识符引用调用了内部的函数bar()。
bar()显然可以被正常执行。但是在这个例子中,它在自己定义的词法作用域以外的地方执行。
在foo()执行后,通常会期待foo()的整个内部作用域都被销毁,因为我们知道引擎有垃圾回收器来释放不再使用的内存空间。而闭包的“神奇”之处正是可以阻止这件事情的发生。事实上内部作用域依然存在,因此没有被回收,因为bar()本身在使用。
因为bar()所声明的位置,它拥有涵盖foo()内部作用域的闭包。使得该作用域一直存活,以供bar()在之后任何时间进行引用。
bar()依然持有对该作用域的引用,而这个引用就叫作闭包。
闭包使得函数可以继续访问定义时的词法作用域。当然,无论使用何种方式对函数类型的值进行传递,当函数在别处被调用时都可以观察到闭包。
function foo(){ var a = 2; function baz(){ console.log(a); } bar(baz);}function bar(fn){ fn(); // 这就是闭包}复制代码
把内部函数baz传递给bar,当调用这个内部函数时(现在叫作fn),它涵盖的foo()内部作用域的闭包就可以观察到了,因为它能够访问a。
传递函数当然也可以是间接的。
var fn ;function foo(){ var a = 2; function baz(){ console.log(a); } fn = baz; // 将baz分配给全局变量}function bar(){ fn(); // 这就是闭包}复制代码
无论通过何种手段将内部函数传递到所在的词法作用域以外,它都会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。
2.现在我懂了
function wait(message){ setTimeout(function timer(){ console.log(message); },1000)}wait("Hello,closure!")复制代码
将一个内部函数(名为timer)传递给setTimeout(..)。timer具有涵盖wait(..)作用域的闭包,因此还保有对变量message的引用。
wait(..)执行1000毫秒后,它的内部作用域并不会消失,timer函数依然保有wait(..)作用域的闭包。
本质上,无论何时何地,如果将(访问它们各自词法作用域的)函数当作第一级的值类型并到处传递,你就会看到闭包在这些函数中的作用。在定时器、事件监听器、Ajax请求、跨窗口通信、Web Workers或者任何其他的异步(或者同步)任务中,只要使用了回调函数,实际上就是在使用闭包。
3.循环和闭包
for(var i = 1;i <= 5; i++){ setTimeout(function timer(){ console.log(i); // 每秒一次的频率输出5次6 },i*1000)}复制代码
根据作用域的工作原理,实际情况是尽管循环中的五个函数是在各个迭代中分别定义的,但是它们都被封闭在一个共享的全局作用域中,因此实际上只有一个i。
for(var i = 1; i <= 5; i++){ (function(j){ setTimeout(function timer(){ console.log(j); },j*1000); })(i)}复制代码
在迭代内使用IIFE会为每个迭代都生成一个新的作用域,使得延迟函数的回调可以将新的作用域封闭在每个迭代内部,每个迭代中都会含有一个具有正确值的变量供我们访问。
4.重返块作用域
for(let i = 1; i <= 5; i++){ setTimeout(function timer(){ console.log(i); },i*1000)}复制代码
5.模块
function foo(){ var something = "cool"; var another = [1,2,3]; function doSomething(){ console.log(something); } function doAnother(){ console.log(another.join(" ! ") }}复制代码
私有数据变量something和another,以及doSomething()和doAnother()两个内部函数,它们的词法作用域(而这就是闭包)也就是foo()的内部作用域。
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(); // coolfoo.doAnother(); // 1!2!3复制代码
这个模式在JavaScript中被称为模块。
模块模式需要具备两个必要条件。
1.必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块实例)。
2.封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态。
一个从函数调用所返回的,只有数据属性而没有闭包函数的对象并不是真正的模块。
var foo = (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 }})()foo.doSomething(); // coolfoo.doAnother(); // 1!2!3复制代码
将模块函数转换成了IIFE,立即调用这个函数并将返回值直接赋值给单例的模块实例标识符foo。
模块模式的一个简单但强大的用法是命名将要作为公共API返回的对象:
var foo = (function CoolModule(id){ function change(){ // 修改公共API publicAPI.identify = identify2; } function identify1(){ console.log(id); } function identify2(){ console.log(id.toUpperCase()); } var publicAPI = { change: change, identify: identify1 } return publicAPI;}("foo mocule");foo.identify(); // foo modulefoo.change();foo.identify(); // FOO MODULE复制代码
通过在模块实例的内部保留对公共API对象的内部引用,可以从内部对模块实例进行修改,包括添加或删除方法和属性,以及修改它们的值。
总结
我们在词法作用域的环境下写代码,而其中的函数也是值,可以随意传来传去。
当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这时就产生了闭包。
闭包是一个非常强大的工具,可以用多种形式来实现模块等模式。
模块有两个主要特征:
- 1.为创建内部作用域而调用了一个包装函数
- 2.包装函数的返回值必须至少包括一个对内部函数的引用,这样就会创建涵盖整个包装函数内部作用域的闭包。
巴拉巴拉
关于脑子一热
我的经历告诉我,脑子一热做的事情,多半会后悔,而且会非常后悔。但是怎么去避免呢,方法我还没找到,每次我遇到这样的情绪,都会找各种理由去逃避,这是目前我的低级应对措施,相当低级。如果能从根源消除是最好不过的了,可是我还没有那么大的控制力,所以只能慢慢去培养,尽量减少这种上头的次数了。