• JavaScript之闭包


    闭包的定义

    function init() {
        var name = "Mozilla"; // name 是一个被 init 创建的局部变量
        function displayName() { // displayName() 是内部函数,一个闭包
            alert(name); // 使用了父函数中声明的变量
        }
        displayName();
    }
    init();
    
    function makeFunc() {
        var name = "Mozilla";
        function displayName() {
            alert(name);
        }
        return displayName;
    }
    
    var myFunc = makeFunc();
    myFunc();
    

    ​ 上面两段代码运行结果是完全一样的。不同的是:makeFunc函数中,内部函数 displayName() 在执行前,被外部函数返回。在一些编程语言中,函数中的局部变量仅在函数的执行期间可用。一旦 makeFunc() 执行完毕,我们会认为 name 变量将不能被访问。然而,因为代码运行得没问题,所以很显然在 JavaScript 中并不是这样的。

    ​ JavaScript这样的原因是:JavaScript中的函数会形成闭包。 闭包是由函数以及创建该函数的词法环境组合而成,这个环境包括了这个闭包创建时所能访问的所有局部变量。在上面的例子中,myFunc 是执行 makeFunc 时创建的 displayName 函数实例的引用,而 displayName 实例仍可访问其词法作用域中的变量,即可以访问到 name 。由此,当 myFunc 被调用时,name 仍可被访问,其值 Mozilla 就被传递到alert中。

    ​ 下面看一个更加有趣的例子:

    function makeAdder(x) {
      return function(y) {
        return x + y;
      };
    }
    
    var add5 = makeAdder(5);
    var add10 = makeAdder(10);
    
    console.log(add5(2));  // 7
    console.log(add10(2)); // 12
    

    在这个示例中,我们定义了 makeAdder(x) 函数,它接受一个参数 x ,并返回一个新的函数。返回的函数接受一个参数 y,并返回x+y的值。从本质上讲,makeAdder 是一个函数工厂,他创建了将指定的值和它的参数相加求和的函数。在上面的示例中,我们使用函数工厂创建了两个新函数—— 一个将其参数和 5 求和,另一个和 10 求和。

    闭包的应用

    应用于面向对象编程

    ​ 闭包很有用,因为它允许将函数与其所操作的某些数据(环境)关联起来。这显然类似于面向对象编程。在面向对象编程中,对象允许我们将某些数据(对象的属性)与一个或者多个方法相关联。因此,通常你使用只有一个方法的对象的地方,都可以使用闭包。

    应用于Web开发

    ​ 在 Web 中,大部分我们所写的 JavaScript 代码都是基于事件定义某种行为,然后将其添加到用户触发的事件之上(比如点击或者按键)。我们的代码通常作为回调:为响应事件而执行的函数。

    ​ 假如,我们想在页面上添加一些可以调整字号的按钮。一种方法是以像素为单位指定 body 元素的 font-size,然后通过相对的 em 单位设置页面中其它元素(例如header)的字号:

    body {
      font-family: Helvetica, Arial, sans-serif;
      font-size: 12px;
    }
    
    h1 {
      font-size: 1.5em;
    }
    
    h2 {
      font-size: 1.2em;
    }
    

    我们的文本尺寸调整按钮可以修改 body 元素的 font-size 属性,由于我们使用相对单位,页面中的其它元素也会相应地调整。下面是JavaScript:

    function makeSizer(size) {
      return function() {
        document.body.style.fontSize = size + 'px';
      };
    }
    
    var size12 = makeSizer(12);
    var size14 = makeSizer(14);
    var size16 = makeSizer(16);
    

    size12size14size16 三个函数将分别把 body 文本调整为 12,14,16 像素。我们可以将它们分别添加到按钮的点击事件上。如下所示:

    document.getElementById('size-12').onclick = size12;
    document.getElementById('size-14').onclick = size14;
    document.getElementById('size-16').onclick = size16;
    
    
    <p>Some paragraph text</p>
    <h1>some heading 1 text</h1>
    <h2>some heading 2 text</h2>
    <a href="#" id="size-12">12</a>
    <a href="#" id="size-14">14</a>
    <a href="#" id="size-16">16</a>
    

    用闭包模拟私有方法

    ​ 编程语言中,比如 Java,是支持将方法声明为私有的,即它们只能被同一个类中的其它方法所调用。而 JavaScript 没有这种原生支持,但我们可以使用闭包来模拟私有方法。私有方法不仅仅有利于限制对代码的访问:还提供了管理全局命名空间的强大能力,避免非核心的方法弄乱了代码的公共接口部分。

    ​ 下面的示例展现了如何使用闭包来定义公共函数,并令其可以访问私有函数和变量。这个方式也称为 模块模式(module pattern),下面创建一个计数器可以实现数字加减和查看数字:

    var Counter = (function() {
      var privateCounter = 0;
      function changeBy(val) {
        privateCounter += val;
      }
      return {
        increment: function() {
          changeBy(1);
        },
        decrement: function() {
          changeBy(-1);
        },
        value: function() {
          return privateCounter;
        }
      }   
    })();
    
    console.log(Counter.value()); /* logs 0 */
    Counter.increment();
    Counter.increment();
    console.log(Counter.value()); /* logs 2 */
    Counter.decrement();
    console.log(Counter.value()); /* logs 1 */
    

    上面计数器代码只创建了一个词法环境,为三个函数所共享Counter.increment,Counter.decrementCounter.value。该共享环境创建于一个立即执行的匿名函数体内。这个环境中包含两个私有项:名为privateCounter 的变量和名为 changeBy 的函数。这两项都无法在这个匿名函数外部直接访问。必须通过匿名函数返回的三个公共函数访问。这三个公共函数是共享同一个环境的闭包。因为JavaScript 的词法作用域,它们都可以访问 privateCounter 变量和 changeBy 函数。

    ​ 下面改进一下,定义一个不立即执行的非匿名函数,用于创建计数器。这样可以创建多个计数器并且互相不影响:

    var makeCounter = function () {
        var privateCounter = 0;
        function changeBy(val) {
            privateCounter += val;
        }
        return {
            increment: function() {
                changeBy(1);
            },
            decrement: function() {
                changeBy(-1);
            },
            value: function() {
                return privateCounter;
            }
        }
    };
    var Counter1 = makeCounter();
    var Counter2 = makeCounter();
    console.log(Counter1.value()); /* logs 0 */
    Counter1.increment();
    Counter1.increment();
    console.log(Counter1.value()); /* logs 2 */
    Counter1.decrement();
    console.log(Counter1.value()); /* logs 1 */
    console.log(Counter2.value()); /* logs 0 */
    
    

    上面两个计数器,counter1counter2 是相互独立的,每个闭包都是引用自己词法作用域内的变量 privateCounter 。每次调用其中一个计数器时,通过改变这个变量的值,会改变这个闭包的词法环境。然而在一个闭包内对变量的修改,不会影响到另外一个闭包中的变量。

    闭包的问题

    问题来源

    ​ 问题例子引用自廖雪峰JavaScript教程之闭包,在应用闭包时,需要注意一个问题,返回的函数并没有立刻执行,而是直到调用了f()才执行。我们来看一个例子:

    function count() {
        var arr = [];
        for (var i=1; i<=3; i++) {
            arr.push(function () {
                return i * i;
            });
        }
        return arr;
    }
    
    var results = count();
    var f1 = results[0];
    var f2 = results[1];
    var f3 = results[2];
    

    在上面的例子中,每次循环,都创建了一个新的函数,然后,把创建的3个函数都添加到一个Array中返回了。你可能认为调用f1()f2()f3()结果应该是149,但实际结果是:

    f1(); // 16
    f2(); // 16
    f3(); // 16
    

    全部都是16!原因就在于返回的函数引用了变量i,但它并非立刻执行。等到3个函数都返回时,它们所引用的变量i已经变成了4,因此最终结果为16

    问题解析

    ​ 首先我们弄懂上面代码的运行流程:首先var results = count();之后,函数count已经被调用了,所以一次执行函数内的各段代码:var arr = [];for (var i=1; i<=3; i++),这个for循环尤其值得注意。因为此时循环体执行了push方法,将一个个函数function () { return i * i;}添加到数组内,但是这个函数并没有被调用,还只是一个变量,所以for循环依次执行,直到i = 4。因为闭包,内部函数function () { return i * i;}引用的i就是外部变量,for循环中的i = 4。所以,之后数组arr内的函数的i都是4。

    ​ 调用函数count后,变量results已经是数组arr了。数组里面元素依次是function f1() { return i * i;} function f2() { return i * i;} function f3() { return i * i;}。但是三个函数都没有被调用,直到var f1 = results[0];,此时function f1() { return i * i;}开始执行,如上段所写,此时的i = 4,所以,返回值就是16了。后面两个调用也是类似情况。

    问题启示

    ​ 返回闭包时牢记的一点就是:返回函数不要引用任何循环变量,或者后续会发生变化的变量。如果一定要引用循环变量,方法是再创建一个函数,用该函数的参数绑定循环变量当前的值,无论该循环变量后续如何更改,已绑定到函数参数的值不变:

    function count() {
        var arr = [];
        for (var i=1; i<=3; i++) {
            arr.push((function (n) {
                return function () {
                    return n * n;
                }
            })(i));
        }
        return arr;
    }
    
    var results = count();
    var f1 = results[0];
    var f2 = results[1];
    var f3 = results[2];
    
    f1(); // 1
    f2(); // 4
    f3(); // 9
    

    上述代码中,避免在arr.push方法中,实现了每次循环都立即将当前的参数i传送给函数,然后立即塞进数组。这样就避免了i最后才传给函数。注意,这里用了一个“创建一个匿名函数并立刻执行”的语法,才能及时绑定参数i

    (function (x) {
        return x * x;
    })(3); // 9
    

    参考文档:廖雪峰教程MDN教程

  • 相关阅读:
    腾讯的张小龙是一个怎样的人?
    wordpress 推荐几个主题
    heidisql 可以查看sql
    phpmyadmin 连接外部数据库
    两个效果不错胡时间轴网站
    phpstorm与Mint 快捷键冲突
    Android Action
    Tomcat server.xml
    awk
    makefile
  • 原文地址:https://www.cnblogs.com/cjvae/p/9786598.html
Copyright © 2020-2023  润新知