js学习之函数表达式及闭包

  来自《javascript高级程序设计 第三版:作者Nicholas C. Zakas》的学习笔记(七)

   

  直接切入主题~

  定义函数的方式有两种:

  • 函数声明
    function functionName(arg0,arg1,arg2) {
      //函数体
    }

    函数声明有一个重要的特征——函数声明提升,意思是在执行代码之前会先读取函数声明,允许把函数声明放在调用它的语句后面

    sayHi();
    
    function sayHi() {
      alert("hi");
    }
  • 函数表达式
    var functionName = function(arg0,arg1,arg2) {
      //函数体
    };

    如上创建的其实就是匿名函数。这种情况不支持函数声明提升,即

    sayHi();   //错误,函数不存在
    var sayHi = function() {
      alert("hi");
    };

    还有执行下列代码会产生意想不到的结果:

    //不要这样
    if (condition) {
      function sayHi() {
        alert("hi!");
      }else {
        function sayHi() {
          alert("hello");
       }
      }
    }

    因为不同的浏览器处理方式不同,大多数浏览器会返回第二个声明,忽略condition;Firefox会在condition为true时返回第一个声明。所以这种使用方式很危险。不过,你可以这么改进:

    var sayHi;
    
    if (condition) {
      sayHi = function() {
        alert("hi");
      };
    }esle {
      sayHi = function() {
        alert("hello");
      }
    }

    当然,这时应该好好想想为什么可以这么改进~~因为函数表达式不存在函数声明提前,所以sayHi在任何条件下只会执行一次(if条件的互斥性)。

  • 你可能很想知道匿名函数的用途,在这里就先介绍一点(当然匿名函数的作用可不止这一点)
    function createComparisonFunction(propetyName) {
      return function(object1,object2) {
        var value1 = object1[propertyName];
        var value2 = object2[propertyName];
        if (value1 < value2) {
          return -1;
        }else if (value1 > value2) {
          return 1
        }else {
          return 0;
        }
      };
    }
  • 递归
    function factorial(num) {
      if (num <= 1) {
        return 1;
      }else {
        return num*factorial(num -1);
      }
    }
    
    //上面是一个很简单的递归阶乘函数
    
    var anotherFactorial = factorial;
    factorial = null;
    alert(anotherFactorial(4));     //出错了,原因很明显,函数定义中用了自身的函数名(就是指针啦),当factorial置为null时,整个函数失效了

    这么改进:使用arguments.callee(),它指向正在执行的函数的指针。

    function factorial(num) {
      if (num <= 1) {
        return 1;
      }else {
        return num*arguments.callee(num - 1);
      }
    }
    
    //但是在严格模式下,不能通过脚本访问arguments.callee(),又有了如下改进
    
    var factorial = (function f(num) {    //这种情况下,即便是将factorial赋给另一个变量函数名字f仍然是有效的
      if (num <= 1) {
        return 1;
      }else {
        return num*f(num - 1);
      }
    });

 

  闭包

  如果有人叫你解释一下闭包,你该怎么说。~如果你知道怎么说了,说明你已经完全掌握了它。

  凡事都是有一个起源的,一种技术的产生往往就是为了解决一个问题(装B了)。=。=我总是觉得,所谓的代码简洁是有不同level的,对于个人来说,只要你能把程序中每一条语句存在意义说出来和不可删除的存在在,对你来说就是简洁(当然,你的功力决定了你主观上的简洁的大众level),扯远了。。。。

  闭包作为js语言的特点和难点,要理解它,首先要理解js特殊的变量作用域。

  一、变量作用域

  js变量的作用域无非就是两种:全局变量和局部变量。

 

  js语言的特殊之处就在于函数内部可以直接读取全局变量(其实很多语言,如C++也是)。如:

var n = 999;
function f1() {
  alert(n);
}

f1(); //999

  而另一方面,在函数外部自然无法读取函数内部分局部变量:

function f1() {
  var n = 999;
}

alert(n);   //error

  这里有一个地方需要注意,函数内部声明变量的时候,一定要使用命令var命令。如果不用的话,你是实际上声明了一个全局变量:

function f1() {
  n = 999;
}

f1();

alert(n);  //999

  那如何从外部读取局部变量呢?

  出于种种原因,我们有时候需要得到函数内部的局部变量。怎么变通?~.~

  那就在函数的内部在定义一个函数。

function f1() {
  var n = 999;
  function f2() {
    alert(n);   //999
  }
}

  在上面的代码中,函数f2就被包括在函数f1内部,这时f1内部的所有局部变量,对f2都是可见的。但是反过来就不行,f2内部的局部变量,对f1就是不可见的。这就是js语言特有的“链式作用域”结构(chain scope)。子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的。反之则不成立。

  可是呢,灵感就是这么来的,既然f2可以读取f1中局部变量,那么只要把f2作为返回值,我们就可以在f1外部读取它们内部变量了:

function f1() {
  var n = 999;
  function f2() {
    alert(n);
  }
  return f2;
}

var result = f1();
result();     //999 相当于f2()

  现在将是引入闭包概念的好时候:上述f2函数就是闭包。闭包的官方描述:闭包是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因为这些变量也是表达式的一部分。其实完全可以这么理解:闭包就是能够读取其他函数内部变量的函数。由于在js中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成“定义在函数内部分函数”。闭包就是将函数内部和函数外部连接起来的一座桥梁。

  二、闭包的用途

  它的最大的两个用处就是:

  • 可以读取函数内部的变量
  • 并且可以让这些变量的值始终保存在内存中
    function f1() {
      var n=999;
      nAdd=function() { 
        n+=1;
      }
      function f2() {
        alert(n);
      }
      return f2;
    }
    var result=f1();
    result(); // 999
    nAdd();
    result(); // 1000

     

  在这段代码中,result实际上就是闭包f2函数。它一共运行了两次,第一次的值是999,第二次的值是1000。这证明了,函数f1中的局部变量n一直保存在内存中,并没有在f1调用后被自动清除。

  为什么会这样呢?原因就在于f1是f2的父函数,而f2被赋给了一个全局变量,这导致f2始终在内存中,而f2的存在依赖于f1,因此f1也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。

  这段代码中另一个值得注意的地方,就是“nAdd=function(){n+=1}”这一行,首先在nAdd前面没有使用var关键字,因此 nAdd是一个全局变量,而不是局部变量。其次,nAdd的值是一个匿名函数(anonymous function),而这个匿名函数本身也是一个闭包,所以nAdd相当于是一个setter,可以在函数外部对函数内部的局部变量进行操作。

  三、使用闭包的注意点

  1)由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。

  2)闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。

  四、思考题

var name = "The Window";   
var object = {   
  name : "My Object",   
  getNameFunc : function(){   
    return function(){   
      return this.name;   
    };   
  }   
};   
alert(object.getNameFunc()());  //The Window
function outerFun() {
 var a=0;
 function innerFun() {
   a++;
   alert(a);
  }    
 }
innerFun();

//上面的代码是错误的.innerFun()的作用域在outerFun()内部,所在outerFun()外部调用它是错误的.
改成如下,也就是闭包:

function outerFun() {
  var a=0;
   function innerFun() {
     a++;
     alert(a);
   }
   return innerFun;  //注意这里
}
var obj=outerFun();
obj();  //结果为1
obj();  //结果为2
var obj2=outerFun();
obj2();  //结果为1
obj2();  //结果为2

  再来重新声明一遍什么是闭包:当内部函数 在定义它的作用域 的外部 被引用时,就创建了该内部函数的闭包 ,如果内部函数引用了位于外部函数的变量,当外部函数调用完毕后,这些变量在内存不会被 释放,因为闭包需要它们。

function outerFun() {
 var a =0;
 alert(a);  
}
var a=4;
outerFun();   //0
alert(a);    //4    因为在函数内部使用了var关键字 维护a的作用域在outFun()内部
function outerFun() {
 //没有var 
 a =0;
 alert(a);  
}
var a=4;
outerFun();   //0
alert(a);     //0

  作用域链是描述一种路径的术语,沿着该路径可以确定变量的值 .当执行a=0时,因为没有使用var关键字,因此赋值操作会沿着作用域链到var a=4;  并改变其值。其实,刚看到这种解释的时候我是有疑问的,函数声明具有提前的作用,而且作用域的方向是向上的,直到搜索到同名变量就应该停止。=。=其实是脑子短路了。应该是只要看outerFunn()的调用就可以了。因为当调用outerFun()的时候,函数内部的a = 0,是全局变量定义,覆盖了前面的var a = 4。当然,可能会问,如果outerFun()在前,var a = 4在后面,结果会是怎么样的?~动手试试不就知道了==

<!doctype html>
<html>
    <head></head>
    <script>
    window.onload = (function() {
        function outerFun()
        {
         //没有var 
         a =0;
         alert(a);  
        }
        outerFun();
        var a=4;
        //outerFun();
        alert(a);
    });
    </script>
    <body>
        
    </body>
</html>

  运行结果即为0和4,佐证了我的解释,bingo~

  补充一点js的垃圾回收的知识:在Javascript中,如果一个对象不再被引用,那么这个对象就会被GC回收。如果两个对象互相引用,而不再被第3者所引用,那么这两个互相引用的对象也会被回收。因为函数a被b引用,b又被a外的c引用,这就是为什么函数a执行后不会被回收的原因。

 

  争取把js基础打扎实,然后动手实践=。=

js学习之函数表达式及闭包,古老的榕树,5-wow.com

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。