参考书籍:《JavaScript高级程序设计(第3版)》
函数表达式
函数表达式是JavaScript中的一个即强大又容易令人困惑的特性。
function functionName(arg0, arg1, arg2) { // 函数体 }
Firefox,Safari,Chrome和Opera都给函数定义了一个非标准的name属性
// 只在Firefox,Safari,Chrome,Opera有效 console.log(functionName.name); // functionName
关于函数声明,它的一个重要特征就是函数声明提升
函数表达式
var functionName = function (arg0, arg1, arg2) { // 函数体 }
function关键字后面没有标识符,这叫匿名函数。
理解函数提升的关键,就是lijie 函数声明与函数表达式之间的区别。
请看一个复杂的函数createComparisonFunction
function createComparisonFunction (propertyName) { return function (object1, object2) { var value1 = object1[propertyName]; var value2 = object2[propertyName]; if (value1 < value2) { return -1; } else if (value > value2) { return 1; } else { return 0; } } }
createComparisonFunction()返回了一个匿名函数。返回的函数可能会被赋值给一个变量,或者以其他方式被调用。
在把函数当成值来使用的情况下,都可以使用匿名函数。
递归
递归函数是在一个函数通过名字调用自身的情况下构成的。
function factorial (num) { if (num <= 1) { return 1; } else { return num * factorial(num - 1); } }
这是一个经典的递归阶乘函数。
优化这个函数
var factorial = (function f(num) { if (num <= 1) { return 1; } else { return num * f(num - 1) } })
闭包
闭包是指有权访问另一个函数作用域中的变量的函数。
创建闭包的常见方式,就是在一个函数内部创建另一个函数。以前面的createComparisonFunction()函数为例。
function createComparisonFunction (propertyName) { return function (object1, object2) { var value1 = object1[propertyName]; var value2 = object2[propertyName]; if (value1 < value2) { return -1; } else if (value > value2) { return 1; } else { return 0; } } }
在这个例子中,突出的那两行代码是内部函数(一个匿名函数)中的代码,这两行代码访问了外部函数中的变量propertyName。即使这个内部函数被返回了,而且是在其他地方被调用了,但它仍然可以访问变量propertyName。之所以还能够访问这个变量,是因为内部函数的作用域链中包含createComparisonFunction()的作用域。
要彻底搞清楚其中的细节,必须从理解函数被调用的时候都会发生什么入手。
当某个函数被调用时,会创建一个执行环境及相应的作用域链。然后,使用arguments和其他命名参数的值来初始化函数的活动对象。但在作用域链中,外部函数的活动对象始终处于第二位,外部函数的外部函数的活动对象处于第三位,……直至作为作用域链终点的全局执行环境。
在函数执行过程中,为读取和写入变量的值,就需要在作用域链中查找变量。
来看下面的例子
function compare (value1, value2) { if (value1 < value2) { return -1; } else if (value1 > value2) { return 1; } else { return 0; } } var result = compare(5, 10);
以上代码先定义了compare()函数,然后又在全局作用域中调用了它。当调用compare()时,会创建一个包含arguments,value1,value2的活动对象。全局执行环境的变量对象(包含result和compare)在compare()执行环境的作用域链中则处于第二位。
上图展示了包含上述关系的compare()函数执行时的作用域链。
后台的每个执行环境都有一个表示变量的对象——变量对象。
全局环境的变量对象始终存在,而像compare()函数这样的局部环境的变量对象,则只在函数执行的过程中存在。
作用域链本质上是一个指向变量对象的指针列表,它只引用但不实际包含对象。
一般来讲,当函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局作用域。但是,闭包的情况又有所不同。
在另一个函数内部定义的函数会将包含函数(即外部函数)的活动对象添加到它的作用域链中。因此,在createComparisonFunction()函数内部定义的匿名函数的作用域链中,实际上将会包含外部函数createComparisonFunction()的活动对象。在createComparisonFunction()执行结束的时候,这个活动对象不会被销毁,因为匿名函数的作用域链仍然在引用这个活动对象。
匿名函数被销毁后,这个外部函数的活动对象才会被销毁。
// 创建函数 var compareNames = createComparisonFunction("name"); // 调用函数 var result = compareNames({ name: "Nicholas"}, { name: "Greg" }); // 解除对匿名函数的引用(以便释放内存) compareNames = null;
注意:由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存。过度使用闭包可能会导致内存占用过多。
闭包与变量
请看一个例子
function createFunctions () { var result = new Array(); for (var i = 0; i < 10; i++) { result[i] = function () { return i; } } return result; } var fns = createFunctions(); console.log(fns[1]());
这个函数会生成一个函数数组。表面上看,似乎每个函数都应该返回自己的索引值,即fns[0]() 返回0,fns[1]() 返回1,但实际上,每个函数都返回10.
因为每个函数的作用域链中都保存着createFunctions()函数的活动对象,所以他们引用的都是同一个变量i。当createFunctions()函数返回后,变量i的值是10,此时每个函数都引用着保存变量i的同一个变量对象,所以在每个函数内部的i的值都是10.
如何优化这个函数?
通过创建另一个匿名函数强制让闭包的行为符合预期
function createFunctions () { var result = new Array(); for (var i = 0; i < 10; i++) { result[i] = function(num) { return function() { return num; } }(i) } return result; } var fns = createFunctions(); console.log(fns[1]());
关于this对象
在闭包中使用this对象可能会导致一些问题。
var name = "The Window"; var object = { name: "My Object", getNameFunc: function () { return function () { return this.name } } } var getName = object.getNameFunc(); console.log(getName()); // The Window
把外部作用域中的this对象保存在一个闭包能够访问到的变量里,就可以让闭包访问该对象了,如下所示。
var name = "The Window"; var object = { name: "My Object", getNameFunc: function () { var that = this; return function () { return that.name } } } var getName = object.getNameFunc(); console.log(getName()); // My Object
内存泄漏
由于IE9之前的版本对JScript对象和COM对象使用不同的垃圾收集例程,因此闭包在IE的这些版本中会导致一些特殊的问题。
具体来说,如果闭包的作用域链中保存着一个HTML元素,那么久意味着该元素将无法被销毁。来看下面的例子。
function assignHandler () { var element = document.getElementById("someElement"); element.onclick = function () { alert(element.id); } }
以上代码创建了一个作为element元素事件处理程序的闭包,而这个闭包则又创建了一个循环引用。由于匿名函数保存了一个对assignHandler()的活动对象的引用,因此就会导致无法减少element的引用数。只要匿名函数存在,element的引用数至少也是1,因此它所占用的内存就用于不会被回收。优化代码如下。
function assignHandler () { var element = document.getElementById("someElement"); var id = element.id; element.onclick = function () { alert(id); } element = null; }
模仿块级作用域
通常称为私有作用域
(function(){ // 这里是块级作用域 })()
请看下面一段代码:
function outputNumbers(count) { (function(){ for (var i = 0; i < count; i++) { console.log(i) } })(); console.log(i); // 导致一个错误 } outputNumbers(3);
一般来说,我们都应该尽量少向全局作用域中添加变量和函数。
另一个例子:
(function () { var now = new Date(); if (now.getMonth() == 0 && now.getDate() == 1) { console.log('Happy New Year!') } else { console.log("It's a normal day") } })()
私有变量
任何在函数中定义的变量,都可以认为是私有变量,因为不能在函数外部访问这些变量。
私有变量包括函数的参数、局部变量和在函数内部定义的其他函数。来看下面的例子。
function add (num1, num2) { var sum = num1 + num2; return sum; }
在这个函数内部,有3个私有变量:num1, num2,sum.在函数内部可以访问这几个变量,但在函数外部则不能访问它们。如果在这个函数内部创建一个闭包,那么闭包通过自己的作用域链也可以访问这些变量。而利用这一点,就可以创建用于访问私有变量的公有方法。
我们把有权访问私有变量和私有函数的公有方法称为特权方法。
有两种在对象上创建特权方法的方式。第一种是在构造函数中定义特权方法,基本模式如下:
function myObject () { var privateVariable = 10; function privateFunction () { return false; } this.publicMethod = function () { privateVariable++; return privateFunction(); } } var obj = new myObject(); obj.publicMethod();
利用私有和特权成员,可以隐藏那么不应该被直接修改的数据,例如:
1 function Person (name) { 2 this.getName = function () { 3 return name; 4 } 5 this.setName = function (value) { 6 name = value 7 } 8 } 9 10 var a = new Person('cathy'); 11 console.log(a.getName()); 12 a.setName('nick'); 13 console.log(a.getName());
构造函数模式的缺点是针对每个实例都会创建同样一组新方法。
多查找作用域链中的一个层次,就会在一定程度上影响查找速度。而这正是使用闭包和私有变量的一个明显的不足之处。
模块模式
为单例创建私有变量和特权方法。所谓单例,指的就是只有一个实例的对象。
var singleton = { name: value, method: function () { // 这里是方法的代码 } }
模块模式通过为单例添加私有变量和特权方法能够使其得到增强,其语法形式如下:
1 var singleton = function () { 2 // 私有变量和私有函数 3 var privateVariable = 10; 4 5 function privateFunction () { 6 return false; 7 } 8 9 return { 10 publicProperty: true, 11 publicMethod: function () { 12 privateVariable++; 13 return privateFunction(); 14 } 15 } 16 }
这个模块模式使用了一个返回对象的匿名函数。在这个匿名函数内部,首先定义了私有变量和函数。然后将一个对象字面量作为函数的值返回。返回的对象字面量中只包含可以公开的属性和方法。由于这个对象是在匿名函数内部定义的,因此它的公有方法有权访问私有变量和函数。
从本质上来讲,这个对象字面量定义的是单例的公共接口。
这种模式在需要对单例进行某些初始化,同时又需要维护其私有变量时是非常有用的。例如:
var application = function () { // 私有变量和函数 var components = new Array(); // 初始化 components.push(new BaseComponent()); // 公共 return { getComponentCount: function () { return components.length; }, registerComponent: function(component) { if (typeof component == 'object') { components.push(component); } } } }
在Web应用程序中,经常需要使用一个单例来管理应用程序级的信息。这个简单的例子创建了一个用于管理组件的application对象。在创建这个对象的过程中,首先声明了一个私有的components数组,并向数组中添加了一个BaseComponent的新实例。而返回对象的getComponentsCount和registerComponent方法都是有权访问数组components的特权方法。
增强的模块模式
有人进一步改进了模块模式,即在返回对象之前加入对其增强的代码。
var singleton = function () { // 私有变量和私有函数 var privateVariable = 10; function privateFunction () { return false; } // 创建对象 var object = new CustomType(); // 添加特权/公有属性和方法 object.publicProperty = true; object.publicMethod = function () { privateVariable++; return privateFunction(); } // 返回这个对象 return object; } ();
如果前面演示模块模式的例子中的application对象必须是BaseComponent的实例,那么就可以使用以下代码。
var application = function () { // 私有变量和函数 var components = new Array(); // 初始化 components.push(new BaseComponent()); // 创建application的一个局部副本 var app = new BaseComponent(); // 公共接口 app.getComponentCount = function () { return components.length; } app.registerComponent = function () { if (typeof component == 'object') { components.push(component) } } // 返回这个副本 return app; }();
在这个重写后的应用程序单例中,首先也像前面例子中一样定义了私有变量。主要的不同之处在于命名变量app的创建过程,因为它必须是BaseComponent的实例。这个实例实际上是application对象的局部变量版。此后,我们又为app对象添加了能够访问私有变量的公有方法。最后一步是返回app对象,结果仍然是将它赋值给全局变量application.
小结
在JavaScript编程中,函数表达式是一种非常有用的技术。使用函数表达式可以无须对函数命名,从而实现动态编程。匿名函数,也称为拉姆达函数,是一种使用JavaScript函数的强大方式。以下总结了函数表达式的特点。
在无法确定如何引用函数的情况下,递归函数就会变得比较复杂。
当在函数内部定义了其他函数时,就创建了闭包。闭包有权访问包含函数内部的所有变量。原理如下:
1. 在后台执行环境中,闭包的作用域链包含着它自己的作用域、包含函数的作用域和全局作用域。
2. 通常,函数的作用域及其所有变量都会在函数执行结束后被销毁。
3. 但是,当函数返回了一个闭包时,这个函数的作用域将会一直在内存中保存到闭包不存在为止。
使用闭包可以在JavaScript中模仿块级作用域,要点如下:
1. 创建并立即调用一个函数,这样既可以执行其中的代码,又不会在内存中留下对该函数的引用。
2. 结果就是函数内部的所有变量都会被立即销毁——除非将某些变量赋值给了包含作用域(即外部作用域)中的变量。
闭包还可以用于在对象中创建私有变量,相关概念和要点如下:
1. 即使JavaScript中没有正式的私有变量对象属性的概念,但可以使用闭包来实现公有方法,而通过公有方法可以访问在包含作用域中定义的变量。
2. 有权访问私有变量的公有方法叫做特权方法。
3. 可以使用构造函数模式、原型模式来实现自定义类型的特权方法,也可以使用模块模式、增强的模块模式来实现单例的特权方法。
JavaScript中的函数表达式和闭包都是及其有用的特性,利用它们可以实现很多功能。不过因为创建闭包必须维护额外的作用域,所以过度使用它们可能会占用大量内存。