zoukankan      html  css  js  c++  java
  • js的嵌套函数与闭包函数

    js的嵌套函数与闭包函数

    先看一下代码示例:

    function f(){
    var cnt=0;
    return function(){ return ++cnt;}
    }
    var fa=f();//将函数f的的返回值给变量fa
    // fa();    //对fa的函数调用
    console.log(fa());//1
    console.log(fa());//2
    console.log(fa());//3

    函数的返回值是函数(对象的引用),这里将其赋值给变量fn。在调用fn时,其输出结果每次都会自增加1

    从表面看,闭包(closure)具有状态的函数,或者也可以将闭包的特征理解为:其相关的局部变量在函数调用结束后会继续存在

    一、闭包的原理

    1.1 嵌套的函数声明:

    闭包的前提条件是需要在函数声明的内部声明另一个函数(即嵌套的函数声明)贴一下函数函数声明的simple example:

    function f(){
        function g(){
           console.log('g is called');
        }
       g();
    }
    f()// g is called

    在函数f的声明中包含函数g的声明以及调用语句。再调用函数f时,就间接地调用了函数g。为了更好理解该过程,在此对其内部机制进行说明。

    在javaScript中,调用函数时将会隐式地生成call对象。为了方便起见,我们将调用函数f生成的call对象称作call-f对象。在函数调用完成之后,call对象将被销毁。

    函数f内的函数g的声明将会生成一个与函数的g相对应function对象。其名称g是call-f对象的属性。由 于每一次调用函数都会独立生成call对象,因此在调用函数g时将会隐式地生成另一个call对象。为了方便起见,我们将该call对象称作call-g对象。

    离开函数g之后,call-g对象将被自动销毁。类似的,离开函数f之后,call-f对象也就自动销毁。此时,由于属性g将与call-g对象一起被销毁,所以由g所引用的function对象将会失去其引用,而最终(通过垃圾回收机制)被销毁。

    1.2嵌套函数与作用域

    对上面代码稍稍修改:

    function f(){
        var n=123;
        function g(){
            console.log("n is"+n);
            console.log('g is called');
        }
        g();
    }
    f();
    
    运行结果:
    
    js>f();
    
    n is 123
    
    g is called'

    在内层进行声明函数g可以访问外层的函数f的局部变量(在这里指变量n),对于嵌套声明的函数,内部的函数将会首先查找被调用时所生成的call对象的属性,之后之后在查找外层函数的call对象的属性。这一机制被称为作用链。

    1.3嵌套函数的返回

    上面的代码稍稍修改

    function f(){
        var n=123;
        function g(){
            console.log("n is"+n);
            console.log('g is called');
        }
        return g;
    }
    js> f();
    
    function g(){
            console.log("n is"+n);
            console.log('g is called');
        }

    由于return语句,函数将会返回一个function对象(的引用)。调用函数f的结果是一个function对象。这时,虽然会生成与函数f相对应的call对象(call-f对象)(并在离开函数f后被销毁),但由于不会调用函数g,所以此时还不会生成与之相对应的call对象(call-g对象),请对此多加注意。

    二、闭包

      2.1、作用域

        待更新

      2.2、闭包示例:

    比如:
    init() 创建了一个局部变量 name 和一个名为 displayName() 的函数。displayName() 是定义在 init() 里的内部函数,并且仅在 init() 函数体内可用。请注意,displayName() 没有自己的局部变量。然而,因为它可以访问到外部函数的变量,所以 displayName() 可以使用父函数 init() 中声明的变量 name 。
    function init() {
        var name = "Mozilla"; // name 是一个被 init 创建的局部变量
        function displayName() { // displayName() 是内部函数,一个闭包
            console.log(name); // 使用了父函数中声明的变量
        }
        displayName();
    }
    init();

    运行这段代码的效果和之前 init() 函数的示例完全一样。其中不同的地方(也是有意思的地方)在于内部函数 displayName() 在执行前,从外部函数返回。。在本例子中,myFunc 是执行 makeFunc 时创建的 displayName 函数实例的引用。displayName 的实例维持了一个对它的词法环境(变量 name 存在于其中)的引用。因此,当 myFunc 被调用时,变量 name 仍然可用,其值 Mozilla 就被传递到alert中

    function makeFunc() {
        var name = "Mozilla";
        function displayName() {
         console.log(22, name);
        }
        return displayName;
    }
    
    var myFunc = makeFunc();
    myFunc(); //22 "Mozilla"

    下面是一个更有意思的示例 — 一个 makeAdder 函数

    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 求和。

    add5 和 add10 都是闭包。它们共享相同的函数定义,但是保存了不同的词法环境。在 add5 的环境中,x 为 5。而在 add10 中,x 则为 10。

    三、闭包的实际应用

    1、return 一个函数

    2、传以一个函数的形似传参数

    3、IIF立即执行函数

    css:

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

    html:

    <hr>
    <a href="#" id="size-12">12</a>
    <a href="#" id="size-14">14</a>
    <a href="#" id="size-16">16</a> 

    js:

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

    用闭包模拟私有方法:

    //*****
    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.decrement 和 Counter.value。

    该共享环境创建于一个立即执行的匿名函数体内。这个环境中包含两个私有项:名为 privateCounter 的变量和名为 changeBy 的函数。这两项都无法在这个匿名函数外部直接访问。必须通过匿名函数返回的三个公共函数访问。

    这三个公共函数是共享同一个环境的闭包。多亏 JavaScript 的词法作用域,它们都可以访问 privateCounter 变量和 changeBy 函数。

    四、在循环中创建闭包常见误区

    htm代码片段:

    <p id="help">Helpful notes will appear here</p>
    <p>E-mail: <input type="text" id="email" name="email"></p>
    <p>Name: <input type="text" id="name" name="name"></p>
    <p>Age: <input type="text" id="age" name="age"></p>


    js代码片段:


    function
    showHelp(help) { document.getElementById('help').innerHTML = help; } function setupHelp() { var helpText = [ {'id': 'email', 'help': 'Your e-mail address'}, {'id': 'name', 'help': 'Your full name'}, {'id': 'age', 'help': 'Your age (you must be over 16)'} ]; for (var i = 0; i < helpText.length; i++) { var item = helpText[i]; document.getElementById(item.id).onfocus = function() { showHelp(item.help); } } } setupHelp();

    运行这段代码后,您会发现它没有达到想要的效果。无论焦点在哪个input上,显示的都是关于年龄的信息。

    原因是赋值给 onfocus 的是闭包。这些闭包是由他们的函数定义和在 setupHelp 作用域中捕获的环境所组成的。这三个闭包在循环中被创建,但他们共享了同一个词法作用域,在这个作用域中存在一个变量item。这是因为变量item使用var进行声明,由于变量提升,所以具有函数作用域。当onfocus的回调执行时,item.help的值被决定。由于循环在事件触发之前早已执行完毕,变量对象item(被三个闭包所共享)已经指向了helpText的最后一项。

    改进方法一:

    function showHelp(help) {
      document.getElementById('help').innerHTML = help;
    }
    
    function makeHelpCallback(help) {
      return function() {
        showHelp(help);
      };
    }
    
    function setupHelp() {
      var helpText = [
          {'id': 'email', 'help': 'Your e-mail address'},
          {'id': 'name', 'help': 'Your full name'},
          {'id': 'age', 'help': 'Your age (you must be over 16)'}
        ];
    
      for (var i = 0; i < helpText.length; i++) {
        var item = helpText[i];
        document.getElementById(item.id).onfocus = makeHelpCallback(item.help);
      }
    }
    
    setupHelp(); 

    改进方法二:

    function showHelp(help) {
      document.getElementById('help').innerHTML = help;
    }
    
    function setupHelp() {
      var helpText = [
          {'id': 'email', 'help': 'Your e-mail address'},
          {'id': 'name', 'help': 'Your full name'},
          {'id': 'age', 'help': 'Your age (you must be over 16)'}
        ];
    
      for (var i = 0; i < helpText.length; i++) {
        (function() {
           var item = helpText[i];
           document.getElementById(item.id).onfocus = function() {
             showHelp(item.help);
           }
        })(); // 马上把当前循环项的item与事件回调相关联起来
      }
    }
    
    setupHelp();

    避免使用过多的闭包,可以用let关键词:

    function showHelp(help) {

    document.getElementById('help').innerHTML = help; } function setupHelp() { var helpText = [ {'id': 'email', 'help': 'Your e-mail address'}, {'id': 'name', 'help': 'Your full name'}, {'id': 'age', 'help': 'Your age (you must be over 16)'} ]; for (var i = 0; i < helpText.length; i++) { let item = helpText[i]; document.getElementById(item.id).onfocus = function() { showHelp(item.help); } } } setupHelp();

    例如,在创建新的对象或者类时,方法通常应该关联于对象的原型,而不是定义到对象的构造器中。原因是这将导致每次构造器被调用时,方法都会被重新赋值一次(也就是,每个对象的创建)。

    考虑以下示例:

    function MyObject(name, message) {
      this.name = name.toString();
      this.message = message.toString();
      this.getName = function() {
        return this.name;
      };
    
      this.getMessage = function() {
        return this.message;
      };
    }

    在上面的代码中,我们并没有利用到闭包的好处,因此可以避免使用闭包。修改成如下:

    function MyObject(name, message) {
      this.name = name.toString();
      this.message = message.toString();
    }
    MyObject.prototype = {
      getName: function() {
        return this.name;
      },
      getMessage: function() {
        return this.message;
      }
    };

    但我们不建议重新定义原型。可改成如下例子:

    function MyObject(name, message) {
      this.name = name.toString();
      this.message = message.toString();
    }
    MyObject.prototype.getName = function() {
      return this.name;
    };
    MyObject.prototype.getMessage = function() {
      return this.message;
    };

    在前面的两个示例中,继承的原型可以为所有对象共享,不必在每一次创建对象时定义方法。

  • 相关阅读:
    jquery两个滚动条样式
    js双层动画幻灯
    漂浮QQ
    js物理弹性窗口
    js抽奖跑马灯程序
    经典算法
    判断手机浏览器终端设备
    javascript判断手机旋转横屏竖屏
    【转】处理百万级以上的数据提高查询速度的方法
    Linux -- Centos 下配置LNAMP 服务器环境
  • 原文地址:https://www.cnblogs.com/pikachuworld/p/5325868.html
Copyright © 2011-2022 走看看