目录

第七章 - 函数表达式

本章内容

  • 函数表达式的特征
  • 使用函数实现递归
  • 使用闭包定义私有变量

定义函数的方式有两种:一种是函数声明,另一种就是函数表达式。函数声明的语法:

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

关于函数声明,它的一个重要特征就是函数声明提升(function declaration hoisting),在执行代码之前会先读取函数声明。 第二种创建函数的方式是使用函数表达式,常见的形式:

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

这种情况下创建的函数叫做匿名函数(anonymous function),因为 function 关键字后面没有标识符。在使用前必须先赋值。 理解函数提升的关键,就是理解函数声明与函数表达式之间的区别。

//不要这样做!  
if(condition){ 
    function sayHi(){ 
        alert("Hi!"); 
    } 
} else { 
    function sayHi(){ 
        alert("Yo!"); 
    } 
}  

大多数浏览器会返回第二个声明,忽略condition;使用函数表达式可以避免此问题。

7.1 递归

使用 arguments.callee来代替一个函数通过名字调用自身。在严格模式下可使用命名函数表达式来达到相同效果:

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

7.2 闭包

闭包是指有权访问另一个函数作用域中的变量的函数(因为内部函数的作用域链包含了外部函数的作用域,详见第四章)。创建闭包的常见方式,就是在一个函数内部创建另一个函数。

createComparisonFunction()函数为例注意加粗的代码 
 
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; 
        } 
    }; 
}

下面简要回顾关于作用域的概念:

function compare(value1, value2){ 
    if (value1 < value2){ 
        return -1; 
    } else if (value1 > value2){ 
        return 1; 
    } else { 
        return 0; 
    } 
} 
 
var result = compare(5, 10);

下图展示了作用域链: http://q7l5moftw.bkt.clouddn.com/20200322184914.png 在创建 compare()函数时,会创建一个预先包含全局变量对象的作用域链,这个作用域链被保存在内部的[[Scope]]属性中。显然,作用域链本质上是一个指向变量对象的指针列表,它只引用但不实际包含变量对象。 一般来讲,当函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局作用域(全局执行环境的变量对象)。但是,闭包的情况又有所不同
createComparisonFunction()函数在执行完毕后,其活动对象也不会被销毁,因为匿名函数的作用域链仍然在引用这个活动对象。换句话说,当 createComparisonFunction()函数返回后,其执行环境的作用域链会被销毁,但它的活动对象仍然会留在内存中;直到匿名函数被销毁后,createComparisonFunction()的活动对象才会被销毁。

//创建函数 
var compareNames = createComparisonFunction("name"); 
 
//调用函数 
var result = compareNames({ name: "Nicholas" }, { name: "Greg" }); 
 
//解除对匿名函数的引用(以便释放内存) 
compareNames = null; 

下图展示了调用compareNames()过程中产生的作用域链之间的关系: http://q7l5moftw.bkt.clouddn.com/20200322185839.png

由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存。所以要慎重使用闭包。

7.2.1 闭包与变量

作用域链的这种配置机制引出了一个值得注意的副作用:

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

表面上看,似乎每个函数都应该返自己的索引值,即位置 0 的函数返回 0,位置 1 的函数返回 1,以此类推。但实际上,每个函数都返回 10。因为当createFunctions()函数返回后,变量 i 的值是 10,此时每个函数都引用着保存变量 i 的同一个变量对象。 我们可以通过创建另一个匿名函数强制让闭包的行为符合预期。

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

7.2.2 关于this对象

在闭包中使用 this 对象也可能会导致一些问题。我们知道,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 对象保存在一个闭包能够访问到的变量里(比如下面的that),就可以让闭包访问该对象了:

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

arguments 也存在同样的问题。如果想访问作用域中的 arguments 对象,必须将对该对象的引用保存到另一个闭包能够访问的变量中。

在几种特殊情况下,this 的值可能会意外地改变:

var name = "The Window"; 
 
var object = { 
    name : "My Object", 
 
    getName: function(){ 
        return this.name; 
    } 
};
object.getName();   //"My Object" 
(object.getName)(); //"My Object" 
(object.getName = object.getName)(); //"The Window",在非严格模式下

7.2.3 内存泄漏

IE9 之前的版本导致闭包在 IE 的这些版本中会导致一些特殊的问题。

7.3 模仿块级作用域

匿名函数可以用来模仿块级作用域:

(function(){ 
    //这里是块级作用域 
})();

将函数声明包含在一对圆括号中,表示它实际上是一个函数表达式。而紧随其后的另一对圆括号会立即调用这个函数。

无论在什么地方,只要临时需要一些变量,就可以使用私有作用域:

function outputNumbers(count){ 
    (function () { 
        for (var i=0; i < count; i++){ 
            alert(i); 
        } 
    })(); 
     
    alert(i);   //导致一个错误! 
} 

在私有作用域中能够访问变量 count,是因为这个匿名函数是一个闭包,它能够访问包含作用域中的所有变量。这种技术经常在全局作用域中被用在函数外部,从而限制向全局作用域中添加过多的变量和函数。(JS的确是挺麻烦的语言,ES6的let是不是改善了这个?)

7.4 私有变量 (和上一节的内容混起来有点乱。。。)

严格来讲,JavaScript 中没有私有成员的概念;所有对象属性都是公有的。但有私有变量的概念:任何在函数中定义的变量,都可以认为是私有变量,因为不能在函数的外部访问这些变量。 我们把有权访问私有变量和私有函数的公有方法称为特权方法(privileged method)。两种在对象上创建特权方法的方式。第一种是在构造函数中定义特权方法,基本模式如下。

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

能够在构造函数中定义特权方法,是因为特权方法作为闭包有权访问(因为作用域链)在构造函数中定义的所有变量和函数。 不过,在构造函数中定义特权方法也有一个缺点,那就是你必须使用构造函数模式来达到这个目的。而构造函数模式的缺点是针对每个实例都会创建同样一组新方法,而使用静态私有变量来实现特权方法就可以避免这个问题。

7.4.1 静态私有变量

7.4.2 模块模式

7.4.3 增强的模块模式

7.5 小结

在 JavaScript 编程中,函数表达式是一种非常有用的技术。使用函数表达式可以无须对函数命名,从而实现动态编程。匿名函数,也称为拉姆达函数,是一种使用 JavaScript 函数的强大方式。以下总结了函数表达式的特点。

  • 函数表达式不同于函数声明。函数声明要求有名字,但函数表达式不需要。没有名字的函数表达式也叫做匿名函数。
  • 在无法确定如何引用函数的情况下,递归函数就会变得比较复杂;
  • 递归函数应该始终使用 arguments.callee 来递归地调用自身,不要使用函数名——函数名可能会发生变化。

当在函数内部定义了其他函数时,就创建了闭包。闭包有权访问包含函数内部的所有变量,原理如下。

  • 在后台执行环境中,闭包的作用域链包含着它自己的作用域、包含函数的作用域和全局作用域。
  • 通常,函数的作用域及其所有变量都会在函数执行结束后被销毁。
  • 但是,当函数返回了一个闭包时,这个函数的作用域将会一直在内存中保存到闭包不存在为止。

使用闭包可以在 JavaScript 中模仿块级作用域(JavaScript 本身没有块级作用域的概念),要点如下。

  • 创建并立即调用一个函数,这样既可以执行其中的代码,又不会在内存中留下对该函数的引用。
  • 结果就是函数内部的所有变量都会被立即销毁——除非将某些变量赋值给了包含作用域(即外部作用域)中的变量。

闭包还可以用于在对象中创建私有变量,相关概念和要点如下。

  • 即使 JavaScript 中没有正式的私有对象属性的概念,但可以使用闭包来实现公有方法,而通过公有方法可以访问在包含作用域中定义的变量。
  • 有权访问私有变量的公有方法叫做特权方法。
  • 可以使用构造函数模式、原型模式来实现自定义类型的特权方法,也可以使用模块模式、增强的模块模式来实现单例的特权方法。

JavaScript 中的函数表达式和闭包都是极其有用的特性,利用它们可以实现很多功能。不过,因为 创建闭包必须维护额外的作用域,所以过度使用它们可能会占用大量内存