JavaScript高级程序设计 函数表达式

函数表达式

函数定义的两种方式:

  • 函数声明(函数声明提升,非标准name属性可访问给函数指定的名字)

          函数声明提升:执行代码前先读取函数声明

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

 

  • 函数表达式(name属性为空字符串,匿名函数)
 var functionName = function(arg0, arg1, arg2){
     //函数体
 };  //注意这个分号

与if···else···语句结合使用只能用函数表达式(理解函数声明提升的关键--函数声明和函数表达式的区别)

7.1 递归

 递归函数:一个函数通过名字调用自身

 递归函数调用容易出问题:这里用一个最简单的阶乘函数来表示

 function factorial(num){
     if (num <= 1){
         return 1;
     }else{
         return num * factorial(num-1);
     }
 }

 使用下面的代码会让他出错:

 var anotherFactorial = factorial;
 factorial = null;
 alert(anotherFactorial(4));  //出错!

这里的anotherFactorial是factorial的一个副本,但factorial函数内仍在调用factorial函数,但此时的factorial函数已经被置空。所以这里会出现因为命名问题而产生的错误。

解决方法:

  1. arguments.callee 指向正在执行函数的指针(避免直接写明函数名带来的问题)  但严格模式下不能通过脚本访问
  2. 命名函数表达式        
 var factorial = (function f(num){
     if(num <= 1){
         return 1;
     }else{  
         return num * f(num-1);
     }
 });

这种方法在严格模式下也能使用

 7.2 闭包

闭包:有权访问另一个函数作用域中变量的函数 (常见情况就是在一个函数内部创建另一个函数)

之所以内部函数可以访问到外部函数的变量,是因为内部函数的作用域链中包含了外部函数的作用域    =>   函数第一次被调用的时候发生了什么? 

  当某个函数第一次被调用时,会创建一个执行环境和相应的作用域链,并把作用域链赋值给一个特殊的内部属性([[Scope]])。然后再用this,arguments和其他命名参数来初始化函数的活动对象。在作用域链中,外部函数的活动对象始终处于第二位。

  执行环境有一个与之关联的变量对象,环境中有权访问的所有变量和函数都在这个变量对象中。为了保证对这些变量对象的有序访问,出现了作用域链。作用域链从自身的环境的活动对象开始,一直链接到全局环境的活动对象。

  当一个函数创建时,会创建一个预先包含全局变量对象的作用域链保存在[[Scope]]属性中。在调用函数时,会为函数创建一个执行环境,通过复制[[Scope]]中的对象构建起执行环境的作用域链。之后每有一个活动对象被创建就被推入作用域链的前端。(是引用而不是变量对象本身)

  函数执行完毕,局部变量就会被销毁,内存中仅保留全局作用域。

闭包的特殊性:对于闭包,内部函数被单独调用时,作用域链会包括外部函数的执行环境。所以外部函数执行结束后,其作用域链会被销毁,但其活动对象仍留在内存中。直到其内部函数被销毁后才会被销毁。

 //闭包
 function createComparisonFunction(propertyName){
     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;
         }
     };
 }
 //创建函数
 var compareNames = createComparisonFunction("name"); 
 //调用函数
 var result = compareNames({name: "Nicholas" }, {name: "Grey"});
 //删除对函数的引用,以释放内存
 compareNames = null;

过度使用闭包会造成内存占用过多,慎重使用。

7.2.1 闭包与变量

作用域链配置机制的副作用:闭包只能取得包含函数中任何变量的最后一个值。

 function createFunctions(){
     var result = new Array();
 
     for (var i = 0; i < 10; i++){
         result[i] = function(){
             return i;
         };
     }
 
     return result;
 }

 

 function createFunctions(){
     var result = new Array();
 
     for (var i = 0; i < 10; i++){
         result[i] = function(num){
             return function(){
                 return num;
             }
         }(i);
     }
 
     return result;
 }        

第一种情况下,数组每一次都返回10。因为每个函数的作用域链都保存着外部函数的活动对象,所以引用的都是同一个变量i,外部函数返回后,变量i的值是10,所以每一个函数内部的i值都是10。

第二种情况可以符合预期。这里没有直接把闭包赋值给数组,而是定义了一个匿名函数,把实时的i值赋给num。

7.2.2 关于this对象

匿名函数的执行环境具有全局性,其this对象通常指向window。

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

内部函数搜索this,arguments(调用时会自动获取)时会到其活动对象为止,不可能访问外部函数中的这两个变量。

解决方法:

 var name = "The Window";
 
 var object = {    
     name: "My Object",
 
     getNameFunc: function(){
         var that = this;
         return function(){
             return that.name;
         };
     }
 };
 
 alert(object.getNameFunc()()):  //"My Object"

7.2.3 内存泄漏

 如果闭包的作用域链中保存了一个HTML元素,那么该元素无法被销毁。

 function assignHandler(){
     var element = document.getElementById("someElement");
     element.onclick = function(){
         alert(element.id);
     };
 }

由于在匿名函数中调用了包含函数的活动变量element,所以只要匿名函数还存在,element的引用数至少是1(参考垃圾清理机制),内存永远不会被回收。

解决方法:

 function assignHandler(){
     var element = document.getElementById("someElement");
     var id = element.id;    
     element.onclick = function(){
         alert(id);
     };
     element = null;
 }

 

 在闭包中删除了对HTML元素的循环引用(id)。但还要element变量置为null,因为闭包会引用整个包含函数的活动变量,包括element(即使不直接引用)。

7.3 模仿块级作用域

JavaScript没有块级作用域的概念(比如for循环中的i,在for循环之外也可以被访问到)。对已声明的变量重复声明会被忽略,但初始化会改变值。

解决方法:通过匿名函数来模拟块级作用域

(function(){
    //块级作用域
})():

实际上是一个函数表达式(不能去掉function外层的圆括号,因为JavaScript将function关键字当作一个函数声明的开始,然而函数声明后是不可以跟圆括号的,加这层圆括号可以将函数声明转换为函数表达式)

作用:临时需要一些变量,限制向全局作用域添加过多的变量和函数。

这样可以减少闭包占用的内存问题,因为没有指向匿名函数引用。只要函数执行完毕,作用域链就可以被销毁了。

7.4 私有变量

私有变量:在函数中定义的变量(不能在函数外部访问)

创建访问私有变量的公有方法:通过闭包(可以通过自己的作用域链访问这些私有变量)

特权方法:有权访问私有变量和私有函数的公有方法

在对象上创建特权方法的方式:构造函数中定义特权方法

function MyObject(){
    //私有变量和私有函数
    var privateVariable = 10;
    function privateFunction(){
        return false;
    }
    //特权方法
    this.publicMethod = function(){
        privateVariable++;
        return privateFunction();
    };
}

除了使用publicMethod()之外无法直接访问私有变量。

还可以利用这个特性来隐藏那些不应该被直接修改的数据

7.4.1 静态私有变量

在私有作用域中定义变量或函数,从而创建特权方法。

(function(){
    //私有变量和私有函数
    var privateVariable = 10;
    function privateFunction(){
        return false;
    }

    //构造函数,全局变量(没有用var)
    MyObject = function(){
    };

    //公有/特权方法
    MyObject.prototype.publicMethod = function(){
        privateVariable++;
        return privateFunction();
    };
})();

公有方法是在原型上定义的(原型模式),使用的函数表达式而不是函数声明(函数声明只能创建局部函数)

与构造函数法的区别是,私有变量和函数是由实例共享的(在原型上定义的)。

这种情况下,私有变量就成为了静态的、所有实例共享的。不足之处在于,多查找作用域链的一个层次就会在一定程度上影响查找速度

7.4.2 模块模式

模块模式:为单例创建私有变量和特权方法(单例:只有一个实例的对象)  =>  用对象字面量的方法来创建

使用返回对象的匿名函数的方法。可以应用在需要对单例进行某些初始化,但是又要维护其私有变量的时候。

7.4.3 增强的模块模式

单例必须是某种类型的实例,必须添加某些属性和方法对其增强的情况。

       

posted @ 2020-06-05 16:50  HermionePeng  阅读(157)  评论(0编辑  收藏  举报