• 深入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)包装函数的返回值必须至 少包括一个对内部函数的引用,这样就会创建涵盖整个包装函数内部作用域的闭包。

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

    知识在于分享,如有问题欢迎评论! 原创博客禁止抄袭,原文地址:https://www.cnblogs.com/xiao-pengyou/
  • 相关阅读:
    多播委托和匿名方法再加上Lambda表达式
    委托
    从警察抓小偷看委托
    StringBuilder
    C#修饰符详解
    数据结构与算法之队列
    数据结构与算法之栈
    win10重复安装
    网络编程基础
    PrintPreviewControl
  • 原文地址:https://www.cnblogs.com/journey-blog/p/12827304.html
Copyright © 2020-2023  润新知