定义函数的方式有两种:一种是函数声明,一种就是函数表达式。
关于函数声明,它的重要特征:函数声明提升(function declaration hosting)
<!DOCTYPE html> <html> <head> <title>Function Name Example</title> </head> <body> <script type="text/javascript"> //函数声明 function functionName(){ //noop } //works only in Firefox, Safari, Chrome, and Opera alert(functionName.name); //"functionName" //函数声明提升 sayHi(); function sayHi(){ alert("Hi!"); } var fname=function(arg0,arg1){ //函数体 这种情况下创建的函数叫做匿名函数(有时候也叫拉姆达函数),因为function后面没有标识符。匿名函数的name属性是空字符串。 } </script> </body> </html>
以下代码在ECMAScript中属于无效语法,如果是使用函数表达式就没有问题。
<!DOCTYPE html> <html> <head> <title>Function Declaration Error Example</title> </head> <body> <script type="text/javascript"> var condition = true; //never do this! if(condition){ function sayHi(){ alert("Hi!"); } } else { function sayHi(){ alert("Yo!"); } } sayHi(); </script> </body> </html>
把函数当成值来使用的情况下,都可以使用匿名函数。
7.1 递归
经典的递归阶乘函数,虽然表面上看没有问题,但下面的代码可能导致它出错。
<!DOCTYPE html> <html> <head> <title>Recursion Example</title> </head> <body> <script type="text/javascript"> function factorial(num){ if (num <= 1){ return 1; } else { return num * factorial(num-1); } } var anotherFactorial = factorial; factorial = null; alert(anotherFactorial(4)); //error! </script> </body> </html>
在严格模式下不能通过脚本访问 arguments.callee,访问这个属性会导致错误。
<!DOCTYPE html> <html> <head> <title>Recursion Example</title> </head> <body> <script type="text/javascript"> function factorial(num){ if (num <= 1){ return 1; } else { return num * arguments.callee(num-1); } } var anotherFactorial = factorial; factorial = null; alert(anotherFactorial(4)); //24 </script> </body> </html>
如下方式在严格模式和非严格模式都行得通
<!DOCTYPE html> <html> <head> <title>Recursion Example</title> </head> <body> <script type="text/javascript"> factorial=(function f(num){ if (num <= 1){ return 1; } else { return num * f(num-1); } }) var anotherFactorial = factorial; factorial = null; alert(anotherFactorial(4)); //24 </script> </body> </html>
7.2 闭包
如何创建作用域链以及作用域链有什么作用的细节,对彻底理解闭包至关重要。当某个函数第一次被调用时,会创建一个执行环境(execution context)及相应的作用域链,并把作用域链赋值给一个特殊的内部属性(即[[Scope]])。然后,使用this、argument和其它命名参数的值来初始化函数的活动对象(activetion object)。但在作用域链中,外部函数的活动对象始终处于第二位,外部函数的外部函数的活动对象处于第三位,......直至作为作用域链终点的全局执行环境。后台的每个执行环境都有一个表示变量的对象---变量对象。全局的变量对象始终存在。而局部环境的变量对象,则只在函数执行的过程中存在。作用域链本质上是一个指向变量对象的指针列表,它只引用但不实际包含变量对象。
闭包是指有权访问另一个函数作用域中的变量的函数(函数调用返回后一个没有释放资源的栈区),创建闭包的常见方式,就是在一个函数内部创建另一个函数。
由于闭包会携带包含它的函数的作用域,因此比其他函数占用更多的内存。建议只在绝对必要时考虑使用闭包。
闭包的作用:一是可以读取函数内部的变量;二是让这些变量的值始终保持在内存中。
7.2.1 闭包与变量
作用域链的这种配置机制引出了一个值得注意的副作用,即闭包只能取得包含函数中任何变量的最后一个值。别忘了闭包所保存的是整个变量对象,而不是某个特殊的变量,下例可以清淅的说明问题:
<!DOCTYPE html> <html> <head> <title>Closure Example</title> </head> <body> <script type="text/javascript"> function createFunctions(){ var result = new Array(); for (var i=0; i < 10; i++){ result[i] = function(){ return i; }; } return result; } var funcs = createFunctions();//此时变量i的值是10 //every function outputs 10 for (var i=0; i < funcs.length; i++){ document.write(funcs[i]() + "<br />"); } </script> 表面上看好像每个函数都应该返自己的索引值,但实际上每个函数都返回10,因为第个函数的作用域链中都保存着createFunctions()函数的活动对象,所以它们都引用的同一个变量i,当createFunctions()函数返回后,变量i的值是10,此时每个函数都引用着保存变量i的同一个变量对象,所以每个函数的内部i的值都是10。
</body> </html>
<!DOCTYPE html> <html> <head> <title>Closure Example 2</title> </head> <body> <script type="text/javascript"> function createFunctions(){ var result = new Array(); for (var i=0; i < 10; i++){ result[i] = function(num){ return function(){ return num; }; }(i);//这里的匿名函数有一个参数num,也就是最终的函数要返回的值。在调用每个匿名函数时,我们传入了变量i,由于函数参数是按值传递的,所以就会将变量i的当前值复制给参数num。而这个匿名函数内部又创建和并返回了一个访问num的闭包。这样一来result数组中的每个函数都有自己num变量的一个副本,因此就可以返回各自不同的数值了。 } return result; } var funcs = createFunctions(); //every function outputs 10 for (var i=0; i < funcs.length; i++){ document.write(funcs[i]() + "<br />"); } </script>
</body> </html>
7.2.2 关于this对象
每个函数在被调用时,其活动对象都会自动取得两个特殊变量:this和arguments。内部函数在搜索这两个变量时,只会搜索到其活动对象为止,因此永远不可能直接访问外部函数中的这两个变量。arguments和this一样,也存在同样的问题。如果想访问作用域中的this(arguments)对象,必须将对该对象的引用保存到另一个闭包能够访问的对象里。匿名函数的执行环境具有全局性,因此其this对象通常指向window。
<!DOCTYPE html> <html> <head> <title>This Object Example</title> </head> <body> <script type="text/javascript"> var name = "The Window"; var object = { name : "My Object", getNameFunc : function(){ return function(){ return this.name; }; } }; alert(object.getNameFunc()()); //"The Window" </script> </body> </html>
<!DOCTYPE html> <html> <head> <title>This Object Example 2</title> </head> <body> <script type="text/javascript"> var name = "The Window"; var object = { name : "My Object", getNameFunc : function(){ var that = this;// 把外部作用域中的this对象保存在一个闭包能够访问到的变量里,就可以让闭包访问该对象了。 return function(){ return that.name; }; } }; alert(object.getNameFunc()()); //"MyObject" </script> </body> </html>
<!DOCTYPE html> <html> <head> <title>This Object Example 3</title> </head> <body> <script type="text/javascript"> var name = "The Window"; var object = { name : "My Object", getName: function(){ return this.name; } }; alert(object.getName()); //"My Object" alert((object.getName)()); //"My Object" alert((object.getName = object.getName)()); //"The Window" in non-strict mode </script> </body> </html>
7.3 模仿块级作用域
用作块级作用域(通常称为私有作用域)的匿名函数的语法如下所示(这种技术经常在全局作用域中被用在函数外部,从而限制在全局作用域中添加过多的变量和函数):
(fuction(){
//这里是块级作用域
})()
定义并立即调用了一个匿名函数
fuction(){
//这里是块级作用域
}()//出错!导致语法错误的原因是因为javascript把fuction当作函数声明的开始,而函数声明后面不能跟圆括号。
无论在什么地方,只要临时需要一些变量,就可以使用私有作用域,例如:
<!DOCTYPE html> <html> <head> <title>Block Scope Example</title> </head> <body> <script type="text/javascript"> function outputNumbers(count){ (function () { for (var i=0; i < count; i++){ alert(i); } })(); alert(i); //causes an error 在匿名函数中定义的任何变量,都会在函数执行结束时被销毁 } outputNumbers(5); </script> </body> </html>
一般来说我们应该尽量少向全局作用域中添加变量和函数,在一个由很多开发人员共同参与的大型应用程序中,过多的全局变量和函数很容易导致命名冲突。而通过创建私有作用域,开发人员既可以使用自己的变量,又不怕搞乱全局作用域。
(function(){
var now=new Date();
if(now.getMonth()==0&&now.getDate==1){
alert("happy new year!");
}
})();
这种作法可以减少闭包占用的内存问题,因为没有指向匿名函数的引用。只要函数执行完毕,就可以立即销毁其作用域链了。
7.4 私有变量
严格来讲,javascript中没有私有成员的概念,所有对象属性都是公有的,不过倒是有一个私有变量的概念。私有变量包括:函数的参数、局部变量、在函数内部定义的其它函数(构造函数里定义的function,即为私有方法)。利用闭包可以创建用于访问私有变量的公有方法,把有权访问私有变量和私有函数的公有方法称为特权方法,有两种创建特权方法的方式:
var Person = function(name,sex){ this.name = name; this.sex = sex; var _privateVariable = "";//私有变量 //构造器中定义的方法,即为私有方法 function privateMethod(){ _privateVariable = "private value"; alert("私有方法被调用!私有成员值:" + _privateVariable); } privateMethod(); //构造器内部可以调用私有方法 } Person.prototype.sayHello = function(){ alert("姓名:" + this.name + ",性别:" + this.sex); } var p = new Person("Nicholas","男");
p.sayHello();
//p.privateMethod();//这里将报错,私成方法无法被实例调用
alert(p._privateVariable);//显示: undefined
说明:类的构造函数里定义的function,即为私有方法;而在构造函数里用var声明的变量,也相当于是私有变量。(不过类比于c#这类强类型语言中的私有成员概念还是有区别的,比如无法在非构造函数以外的其它方法中调用)
类似的,我们还能实现类似set,get属性的封装
<!DOCTYPE html> <html> <head> <title>Privileged Method Example</title> </head> <body> <script type="text/javascript"> function Person(name){ this.getName = function(){ return name; }; this.setName = function (value) { name = value; }; } var person = new Person("Nicholas"); alert(person.getName()); //"Nicholas" person.setName("Greg"); alert(person.getName()); //"Greg" </script> </body> </html>
在构造函数中定义特权方法也有一个缺点,那就是必须使用构造函数模式来达到这个目的。使用静态私有变量特权方法就能解决这个问题。
7.4.1 静态私有变量
<!DOCTYPE html> <html> <head> <title>Privileged Method Example 2</title> </head> <body> <script type="text/javascript"> (function(){ var name = ""; Person = function(value){ name = value; }; Person.prototype.getName = function(){ return name; }; Person.prototype.setName = function (value){ name = value; }; })(); var person1 = new Person("Nicholas"); alert(person1.getName()); //"Nicholas" person1.setName("Greg"); alert(person1.getName()); //"Greg" var person2 = new Person("Michael"); alert(person1.getName()); //"Michael" alert(person2.getName()); //"Michael" </script> </body> </html>
这个例子里name变成了一个静态的、由所有实例共享的属性。以这种方式创建静态私有变量会因为使用原型增进代码利用,但每个实例都没有自己的私有变量。到底是使用实例变量,还是静态私有变量,最终视具体需求而定。
7.4.2 模块模式
前面的模式是用于为自定义类型创建私有变量和特权方法的。模块模式则是为单例创建私有变量和特权方法。所谓单例,就是只有一个实例的对象。
<!DOCTYPE html> <html> <head> <title>Module Pattern Example</title> </head> <body> <script type="text/javascript"> function BaseComponent(){ } function OtherComponent(){ } var application = function(){ //private variables and functions var components = new Array(); //initialization components.push(new BaseComponent()); //public interface return { getComponentCount : function(){ return components.length; },//返回已注册的组件数目 registerComponent : function(component){ if (typeof component == "object"){ components.push(component); } }//后者用于注册新组件 }; }(); application.registerComponent(new OtherComponent()); alert(application.getComponentCount()); //2 </script> </body> </html>
在Web应用程序中,经常需要使用一个单例来管理应用程序级的信息。这个简单的例子创建了一个用于管理组件的application对象。简言之,如果必须要创建一个对象并以某些数据对其进行初始化,同时还要公开一些能够访问这些私有数据的方法,那么就可以使用模块模式。以这种模式创建的每个单例都是Object的实例,因为最终要通过一个对象字面量来表示它。事实上这也没什么,毕竟单例通常都是作为全局对象存在的,我们不会将它传递给一个函数,因此也没有必要用instansof操作符来检查它的对象类型。
7.4.3 增强的模块模式
<!DOCTYPE html> <html> <head> <title>Module Pattern Example</title> </head> <body> <script type="text/javascript"> function BaseComponent(){ } function OtherComponent(){ } var application = function(){ //private variables and functions var components = new Array(); //initialization components.push(new BaseComponent()); //create a local copy of application var app = new BaseComponent(); //public interface app.getComponentCount = function(){ return components.length; }; app.registerComponent = function(component){ if (typeof component == "object"){ components.push(component); } }; //return it return app; }(); alert(application instanceof BaseComponent); application.registerComponent(new OtherComponent()); alert(application.getComponentCount()); //2 </script> </body> </html>
这种模式适合那些单例必须是某种类型的实例,同时必须添加某些属性和(或)方法对其加以增强。
7.5 小结
javascript的函数表达式和闭包都是极其有用的特性,利用它们可以实现很多功能。