定义函数的方式有两种:一种是函数声明,一种就是函数表达式。
关于函数声明,它的重要特征:函数声明提升(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的函数表达式和闭包都是极其有用的特性,利用它们可以实现很多功能。