One today is worth two tomorrows

深入JS——理解闭包可以看作是某种意义上的重生

     JS中有一个非常重要但又难以完全掌握的概念,那就是闭包。很多JS程序员自以为已经掌握了闭包,但实质上是一知半解,就像“JS中万物皆为对象”这个常见的错误说法一样,很多前端开发者到现在还固执己见。对于闭包,笔者现在也不敢说是完全掌握,但是希望通过自己对闭包的理解,让更多人对闭包的概念有一个更深刻的认识。

     理解闭包的重要性,我想用一本书中的话来描述:“对于那些有一点JavaScript使用经验但从未真正理解闭包概念的人来说,理解闭包可以看作是某种意义上的重生”。

 

一、概念

1、定义:当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。

2、下面一段代码,清晰地展示了闭包:

function foo() {

var a = 2;

function bar() {

console.log( a );

}

return bar;

}

var baz = foo();

baz(); // 2,这就是闭包的效果。

 

函数bar()的词法作用域能够访问foo()的内部作用域。然后我们将bar()函数本身当作一个值类型进行传递。在这个例子中,我们将bar所引用的函数对象本身当作返回值。

在foo()执行后,其返回值(也就是内部的bar()函数)赋值给变量baz并调用baz(),实际上只是通过不同的标识符引用调用了内部的函数bar()。

bar()显然可以被正常执行。但是在这个例子中,它在自己定义的词法作用域以外的地方执行。

 

在foo()执行后,通常会期待foo()的整个内部作用域都被销毁,因为我们知道引擎有垃圾回收器用 来释放不再使用的内存空间。由于看上去foo()的内容不会再被使用,所以很自然地会考虑对其进行回收。

 

而闭包的“神奇”之处正是可以阻止这件事情的发生。事实上内部作用域依然存在,因此没有被回收。谁在使用这个内部作用域?原来是bar()本身在使用。

 

拜bar()所声明的位置所赐,它拥有涵盖foo()内部作用域的闭包,使得该作用域能够一直存活,以 供bar()在之后任何时间进行引用。

 

bar()依然持有对该作用域的引用,而这个引用就叫作闭包

 

因此,在几微秒之后变量baz被实际调用(调用内部函数bar),不出意料它可以访问定义时的词法 作用域,因此它也可以如预期般访问变量a。 这个函数在定义时的词法作用域以外的地方被调用。闭包使得函数可以继续访问定义时的词法作用域

当然,无论使用何种方式对函数类型的值进行传递,当函数在别处被调用时都可以观察到闭包。

function foo() {

var a = 2;

  function baz() {

console.log( a ); // 2

}

bar( baz );

}

function bar(fn) {

fn(); // 看,这就是闭包!

}

 

传递函数当然也可以是间接的。

var fn;

function foo() {

var a = 2;

function baz() {

console.log( a );

}

fn = baz; // 将baz分配给全局变量

}

function bar() {

fn(); // 看,这就是闭包!

}

foo();

bar(); // 2

 

3、深入理解

闭包绝不仅仅是一个好玩的玩具。你已经写过的代码中一定到处都是闭包的身影。现在让我们来 搞懂这个事实。

function wait(message) {

setTimeout( function timer() {

console.log( message ); }, 1000 );

}

wait( "Hello, closure!" );

 

将一个内部函数(名为timer)传递给setTimeout(..)。timer具有涵盖wait(..)作用域的闭包,因此 还保有对变量message的引用。

wait(..)执行1000毫秒后,它的内部作用域并不会消失,timer函数依然保有wait(..)作用域的闭 包。 深入到引擎的内部原理中,内置的工具函数setTimeout(..)持有对一个参数的引用,这个参数也许 叫作fn或者func,或者其他类似的名字。引擎会调用这个函数,在例子中就是内部的timer函数,而 词法作用域在这个过程中保持完整。 这就是闭包。

或者,如果你很熟悉jQuery(或者其他能说明这个问题的JavaScript框架),可以思考下面的代码:

function setupBot(name, selector) {

$( selector ).click(

function activator() {

console.log( "Activating:" + name ); } );

}

setupBot( "Closure Bot 1", "#bot_1" );

setupBot( "Closure Bot 2", "#bot_2" );

 

本质上无论何时何地,如果将函数(访问它们各自的词法作用域)当作第一级的值类 型并到处传递,你就会看到闭包在这些函数中的应用。在定时器、事件监听器、Ajax请求、跨窗口通 信、Web Workers或者任何其他的异步(或者同步)任务中,只要使用了回调函数,实际上就是在使用闭包!

 

二.闭包的应用(或表现)

只知道闭包是什么还不够,要知道它的使用场景或说表现方式:

1、  要说明闭包,for循环是最常见的例子。

for (var i=1; i<=5; i++) {

(function(j) {

setTimeout(

function timer() {

console.log( j );

}, j*1000 ); })( i );

}

 

for (var i=1; i<=5; i++) {

let j = i; // 是的,闭包的块作用域!

setTimeout( function timer() {

console.log( j ); }, j*1000 );

}

for循环头部的let声明还会有一个特殊的行 为。这个行为指出变量在循环过程中不止被声明一次,每次迭代都会声明。随后的每个迭代都会使 用上一个迭代结束时的值来初始化这个变量。

块作用域和闭包联手便可天下无敌。

 

2、  模块

还有其他的代码模式利用闭包的强大威力,但从表面上看,它们似乎与回调无关。下面一起来研究 其中最强大的一个:模块。

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

 

这个模式在JavaScript中被称为模块。最常见的实现模块模式的方法通常被称为模块暴露,这里展 示的是其变体。

 

模块模式需要具备两个必要条件。

1. 必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块实例)。

2. 封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以 访问或者修改私有的状态。

 

一个具有函数属性的对象本身并不是真正的模块。从方便观察的角度看,一个从函数调用所返回 的,只有数据属性而没有闭包函数的对象并不是真正的模块。

 

三、总结

闭包就好像从JavaScript中分离出来的一个充满神秘色彩的未开化世界,只有最勇敢的人才能够 到达那里。但实际上它只是一个标准,显然就是关于如何在函数作为值按需传递的词法环境中书 写代码的。

当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这时就产生 了闭包。

如果没能认出闭包,也不了解它的工作原理,在使用它的过程中就很容易犯错,比如在循环中。但 同时闭包也是一个非常强大的工具,可以用多种形式来实现模块等模式。

模块有两个主要特征:(1)为创建内部作用域而调用了一个包装函数;(2)包装函数的返回值必须至 少包括一个对内部函数的引用,这样就会创建涵盖整个包装函数内部作用域的闭包。

现在我们会发现代码中到处都有闭包存在,并且我们能够识别闭包然后用它来做一些有用的事!

 

posted @ 2020-12-22 17:27  雙木的碼上人生  阅读(196)  评论(0编辑  收藏  举报

Have you ever worked hard for yourself in life?