前文说过定义函数的方式有两种,一种是函数声明、一种是函数表达式。两者最大的区别是函数声明提升,即函数的声明在执行代码前会先被读取。
递归
递归函数是在一个函数中通过名字调用自身的情况。前面我们讲过的一个计算乘阶的函数:
function factorial(num){ if(num <= 1){ return 1; }else{ return num*factorial(num - 1); } } console.log(factorial(5)); //120
我们知道,函数名只是一个引用,所以也可以进行赋值,当factorial 被赋值为null或者其他函数引用时,就会发生错误,如下:
var anotherFactorial = factorial; factorial = null; anotherFactorial(5); //TypeError: factorial is not a function
前文讲过,使用arguments.callee 可以解决问题。
function factorial(num){ if(num <= 1){ return 1; }else{ return num*arguments.callee(num - 1); } } console.log(factorial(5)); //120 var anotherFactorial = factorial; factorial = null; console.log(anotherFactorial(5)); //120
但是在严格模式下,使用arguments.callee 会导致错误。不过可以使用命名函数表达式来达到相同的效果。
"use strict" var factorial = (function f(num){ if(num <= 1){ return 1; }else{ return num*f(num - 1); } }); console.log(factorial(5)); //120 var anotherFactorial = factorial; factorial = null; console.log(anotherFactorial(5)); //120
这种方式在严格模式和非严格模式下都能行得通。
闭包
闭包是指有权访问另一个函数作用域中的变量的函数。创建闭包的常见方式,就是在一个函数内部创建另一个函数。
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 obj1 = { name: "Lilei", age: 18 }; var obj2 = { name: "HanMeimei", age: 17 }; console.log(compareNames(obj1, obj2)); compareNames = null;
这里需要理解compareNames 函数的作用域链,函数的作用域链保存在内部的[[Scope]] 属性中,当函数被调用的时候就为函数创建一个执行环境,然后通过复制函数的 [[Scope]] 属性中的对象构建其执行环境的作用域链。作用域链本质上是一个指向变量对象的指针列表,它只引用但不实际包含变量对象。
createComparisonFunction 函数作用域链包括自身的变量 和 全局变量,compareNames 函数的作用域链包括 自身变量 和 createComparisionFunction 的变量 和 全局变量。
当createComparisonFunction 退出时,返回一个compareNames 函数,自身的作用域链销毁, 但是自身的活动对像由于被compareNames 引用,所以仍然会留在内存中,知道compareNames 函数被销毁( compareNames = null; )
1.闭包与变量
作用域链的这种配置机制引出了一个值得注意的副作用,即闭包只能取得包含函数中任何变量的最后一个值。
function createFunctions(){ var result = new Array(); for(var i = 0; i < 10; i++){ result[i] = function(){ return i; }; } return result; } var res = createFunctions(); res.forEach(function(value, index, array){ console.log(value.call()); // 10 10 10 ... 10 });
函数返回十个函数组成的数组,数组中的十个函数的作用域链都是自身和createFunctions 的对象,读取变量 i 值时,找到作用域链的 createFunctions 对象,在数组中的函数调用的时候,createFunctions 对象中的 i 已经变成了10, 所以返回的函数返回值都是 10。
解决办法是再创建一个匿名函数,在作用域链中隔离开自身和 createFunctions 的对象。
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 res = createFunctions(); res.forEach(function(value, index, array){ console.log(value.call()); // 0 1 2 ... 9 });
本例中,我们没有直接把闭包赋值给数组,而是定义了一个匿名函数,并将立即执行该匿名函数的结果赋给了属猪。这里的匿名函数有一个参数 num, 也就是最终的函数要返回的值。在调用每个匿名函数时,我们传入了变量 i 。 由于函数参数是按值传递的,所以就会将变量i的当前值复制给 num。而在这个匿名函数内部,有创建并返回了一个访问num 的闭包。这样一来,result 数组中每一个函数都有自己 num 变量的一个副本,因此就可以返回各自不同的数值了。
2.闭包的this对象
在闭包中使用 this 翠系那个也可能会导致一些问题。匿名函数的执行环境具有全局性,因此其this对象通常指向 global。
var global = function(){ return this; }(); global.name = "The global"; var object = { name: "My Object", getNameFunc: function(){ return function(){ return this.name; }; } } console.log(object.getNameFunc()()); //The global
前面说过,每个函数在被调用时都会自动取得两个特殊变量:this 和 arguments。 内部函数在搜索这两个变量时,只会搜索到其活动对象位置,因此永远不可能直接访问外部函数中的者两个变量。
模仿块级作用域
如前所述,JavaScript 中没有块级作用域的概念,这意味着在块语句中定义的变量,实际上是在包含函数中而非语句中创建的。即使后面重新声明变量,也于事无补。
for(var i = 0; i<10; i++){ } var i; console.log(i); //10
匿名函数可以解决这个问题,用作块级作用域(通常被称为私有作用域)的匿名函数的语法如下:
(function(){ //todo code 块级作用域 })();
以上代码定义并立即调用了一个匿名函数。将函数声明包含在一对全括号中,表示它实际上是一个函数表达式。而金穗气候的另一对圆括号会立即调用这个函数。
(function(){ for(var i =0; i<10; i++){ } })(); console.log(i); //ReferenceError: i is not defined
这种做法可以减少闭包占用内存的问题,因为没有指向匿名函数的引用。只要函数执行完毕,就可以立即销毁其作用域链了。
私有变量
任何在函数中定义的变量,都可以认为是私有变量,因为不能在函数的外部访问这些变量。私有变量包括函数的参数、局部变量 和 在函数内部定义的其他函数。
我们把有权访问私有变量和私有函数的共有方法称为特权方法(privileged method)。
有两种在对象上创建特权方法的方式:
第一种:在构造函数中定义特权方法。
function MyObject(){ //私有变量 var privateVar = 10; //私有方法 function privateFunc(){ return false; } //特权方法 this.publicMethod = function(){ privateVar++; return privateFunc(); } }
定义特却方法有一个缺点,就是你必须使用构造函数模式来达到这个目的。构造函数构建对象的缺点是对每个实例都会创建同样一组方法。
第二种:静态私有变量
(function(){ //私有变量 私有方法 var privateVar = 10; function privateFunc(){ return false; } //构造函数 MyObject = function(){}; //公有/特权方法 MyObject.prototype.publicMethod = function(){ privateVar++; return privateFunc(); } })();
这个模式创建了一个私有作用域,兵在其中封装了一个构造函数及相应的方法。在私有作用域中首先定义了私有变量和私有函数,然后又定义了构造函数和公有方法。公有方法是在原型上定义的,这一点体现了典型的原型模式。需要注意的是,这个模式在定义构造函数时没有使用函数声明,而是使用了函数表达式。函数声明只能创建局部函数,但那不是我们想要的。出于同样的原因,我们也没有在声明MyObject 时使用var 关键字。记住:初始化未经声明的变量,总是会创建一个全局变量。因此MyObject就i成了一个全局变量,能够在私有作用域之外被访问到。但是在严格模式下,给未经声明的变量赋值会导致错误。
多查找作用域链中的一个层次,就会在一定程度上影响查找的速度。这正是使用闭包和私有变量的一个明显的不足之处。
模块模式
前面的模式用于为自定义类型创建私有的变量和特权方法。而道格拉斯所说的模块模式则是为单利创建私有变量和特权方法。所谓单例(singleton),指的就是只有一个实例的对象。按照惯例,JavaScript是以对象字面量的方式来创建单例对象的。
var singleton = { name: value, method: function(){ } };
模块模式通过为单例添加私有变量和特权方法能够使其得到增强。
var singleton = function(){ var privateVar = 10; function privateFunc(){ return false; } return { publicProperty: true, publicMethod: function(){ privateVar++; return privateFunc(); } }; }();
这个模块模式使用了一个返回对象的匿名函数。在这个匿名函数的内部,首先定义了私有变量和函数。然后,将一个对象字面量作为函数的值返回。返回的对象字面量中质保含可以公开的属性和方法。
增强的模块模式
改进模块模式,就是在返回对象之前加入对其增强的代码。这种增强的模块模式适合那些单例必须是某种类型的实例,同事还必须添加某些属性和(或)方法对其甲乙增强的情况。
var singleton = function(){ var privateVar = 10; function privateFunc(){ return false; } //创建对象 var object = new CustomType(); object.publicProperty = true; object.publicMethod = function(){ privateVariable++; return privateFunction(); }; return object; }();
小结
在JavaScript 编程中,函数表达式是一种非常有用的技术。使用函数表达式可以无需对函数命名,从而实现动态编程。匿名函数,也称为拉姆达函数,是一种使用JavaScript 函数的强大方式。
- 函数表达式不同于函数声明。函数声明要求有名字,但函数表达式不需要。没有名字的函数表达式也叫匿名函数。
- 在无法确定如何引用函数的情况下,递归函数就会变得比较复杂。
- 地柜函数应该始终使用 arguments.callee 来递归调用自身,不要使用函数名----函数名可能会发生变化。
当在函数内部定义了其他函数是,就创建了闭包。闭包有权访问包含函数内部的所有变量。
- 在后台执行环境中,闭包的作用域链包含着它自己的作用域、包含函数的作用域 和全局作用域。
- 通常,函数的作用域及其所有变量都会在函数执行结束后销毁。
- 但是,当函数返回了一个闭包是,这个函数的作用域将会一直在内存中保存到闭包不存在为止。
使用闭包可以在JavaScript 中模仿块级作用域(JavaScript中本省没有块级作用域的概念)。
- 创建并立即调用一个函数,这样既可以执行其中的代码,有不会在内存中留下对该函数的引用。
- 结果就是函数内部的所有变量都会被立即销毁----除非将某些变量赋值给了包含作用域(即外部作用域)中的变量。
闭包还可以用户与在对象中创建私有变量。
- 即使JavaScript 中没有正式的私有变量属性的概念,但可以使用闭包来实现公有方法,而通过公有方法可以访问包含作用域中定义的变量。
- 有权访问私有变量的公有方法叫特权方法。
- 可以使用构造函数模式、原型模式来实现自定义类型的特却方法,也可以使用模块模式、增强的模块模式来实现单例的特权方法。
JavaScript 中的函数表达式和闭包都是及其有用的特性,利用它们可以实现很多功能。不过因为创建闭包必须维护额外的作用域,所以过度使用它们可能会占用大量内存。