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;
};

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

 

 

posted @ 2016-03-27 15:43  pikachuWorld  阅读(13446)  评论(0编辑  收藏  举报