定义函数的方式有两种:一种是函数声明,另一种就是函数表达式。
函数声明的语法:
function functionName(arg0, arg1, arg2) {
//函数体
}
关于函数声明的一个重要特征就是函数声明提升,意思是在执行代码之前会先读取函数声明。
sayHi();
function sayHi(){
alert("Hi!");
}
函数表达式有几种不同的语法形式。最常见的一种形式是:
var functionName = function(arg0, arg1, arg2){
//函数体
};
这种情况下创建的函数叫做匿名函数,因为 function
关键字后面没有标识符。(匿名函数有时候也叫拉姆达函数。)匿名函数的name
属性是空字符串。
函数表达式与其他表达式一样,在使用前必须先赋值。以下代码会导致错误。
sayHi(); //错误:函数还不存在
var sayHi = function(){
alert("Hi!");
};
一、递归
递归函数是在一个函数通过名字调用自身的情况下构成的。
function factorial(num) {
if (num <= 1) {
return 1;
} else {
return num * factorial(num - 1);
}
}
arguments.callee
是一个指向正在执行的函数的指针,可以用它来实现对函数的递归调用。
function factorial(num) {
if (num <= 1) {
return 1;
} else {
return num * arguments.callee(num - 1);
}
}
可以使用命名函数表达式来达成相同的结果:
var factorial = (function f(num) {
if (num <= 1) {
return 1;
} else {
return num * f(num - 1);
}
});
创建了一个名为 f()
的命名函数表达式,将它赋值给变量 factorial
。即便把函数赋值给另一个变量,函数的名字 f
仍然有效,所以递归调用照样能正确完成。这种方式在严格模式和非严格模式下都行得通。
二、闭包
闭包是指有权访问另一个函数作用域中的变量的函数。创建闭包的常见方式,就是在一个函数内部创建另一个函数。
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;
}
};
}
作用域链本质上是一个指向变量对象的指针列表,它只引用但不实际包含变量对象。
无论什么时候在函数中访问一个变量时,就会从作用域链中搜索具有相应名字的变量。一般来讲,当函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局作用域(全局执行环境的变量对象)。
在另一个函数内部定义的函数会将包含函数(即外部函数)的活动对象添加到它的作用域链中。因此,在 createComparisonFunction()
函数内部定义的匿名函数的作用域链中,实际上将会包含外部函数createComparisonFunction()
的活动对象。
在匿名函数从createComparisonFunction()
中被返回后,它的作用域链被初始化为包含createComparisonFunction()
函数的活动对象和全局变量对象。这样,匿名函数就可以访问在createComparisonFunction()
中定义的所有变量。更为重要的是,createComparisonFunction()
函数在执行完毕后,其活动对象也不会被销毁,因为匿名函数的作用域链仍然在引用这个活动对象。换句话说,当 createComparisonFunction()
函数返回后,其执行环境的作用域链会被销毁,但它的活动对象仍然会留在内存中;直到匿名函数被销毁后,createComparisonFunction()
的活动对象才会被销毁。
提示:由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存。过度使用闭包可能会导致内存占用过多,建议只在绝对必要时再考虑使用闭包。
1、闭包与变量
闭包只能取得包含函数中任何变量的最后一个值。别忘了闭包所保存的是整个变量对象,而不是某个特殊的变量。
function createFunctions() {
var result = new Array();
for (var i = 0; i < 10; i++) {
result[i] = function() {
return i;
};
}
return result;
}
实际上,每个函数都返回 10。因为每个函数的作用域链中都保存着 createFunctions()
函数的活动对象,所以它们引用的都是同一个变量 i 。当createFunctions()
函数返回后,变量 i 的值是 10,此时每个函数都引用着保存变量 i 的同一个变量对象,所以在每个函数内部 i 的值都是 10。
2、关于this对象
在闭包中使用 this 对象可能会导致一些问题。this
对象是在运行时基于函数的执行环境绑定的:在全局函数中,this
等于 window
,而当函数被作为某个对象的方法调用时,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
和 arguments
。内部函数在搜索这两个变量时,只会搜索到其活动对象为止,因此永远不可能直接访问外部函数中的这两个变量。
把外部作用域中的 this
对象保存在一个闭包能够访问到的变量里,就可以让闭包访问该对象了。
var name = "The Window";
var object = {
name: "My Object",
getNameFunc: function() {
var that = this;
return function() {
return that.name;
};
}
};
alert(object.getNameFunc()()); //"My Object"
提示:this
和 arguments
也存在同样的问题。如果想访问作用域中的 arguments 对象,必须将对该对象的引用保存到另一个闭包能够访问的变量中。
3、内存泄漏
如果闭包的作用域链中保存着一个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;
}
通过把element.id
的一个副本保存在一个变量中,并且在闭包中引用该变量消除了循环引用。把element
变量设置为 null
。这样就能够解除对 DOM
对象的引用,顺利地减少其引用数,确保正常回收其占用的内存。
三、模仿块级作用域
JavaScript
没有块级作用域的概念。这意味着在块语句中定义的变量,实际上是在包含函数中而非语句中创建的。
function outputNumbers(count) {
for (var i = 0; i < count; i++) {
alert(i);
}
alert(i); //计数
}
在 Java、C++
等语言中,变量 i
只会在for
循环的语句块中有定义,循环一旦结束,变量 i
就会被销毁。但在JavaScrip
中,变量 i
是定义在 ouputNumbers()
的活动对象中的,因此从它有定义开始,就可以在函数内部随处访问它。
用作块级作用域(通常称为私有作用域)的匿名函数的语法:
(function(){
//这里是块级作用域
})();
以上代码定义并立即调用了一个匿名函数。将函数声明包含在一对圆括号中,表示它实际上是一个函数表达式。而紧随其后的另一对圆括号会立即调用这个函数。
var someFunction = function(){
//这里是块级作用域
};
someFunction();
先定义了一个函数,然后立即调用了它。定义函数的方式是创建一个匿名函数,并把匿名函数赋值给变量 someFunction
。而调用函数的方式是在函数名称后面添加一对圆括号,即someFunction()
。
无论在什么地方,只要临时需要一些变量,就可以使用私有作用域:
function outputNumbers(count) {
(function() {
for (var i = 0; i < count; i++) {
alert(i);
}
})();
alert(i); //导致一个错误!
}
在这个重写后的 outputNumbers() 函数中,我们在 for 循环外部插入了一个私有作用域。在匿名函数中定义的任何变量,都会在执行结束时被销毁。
提示:这种做法可以减少闭包占用的内存问题,因为没有指向匿名函数的引用。只要函数执行完毕,就可以立即销毁其作用域链了。
四、私有变量
严格来讲,JavaScript
中没有私有成员的概念;所有对象属性都是公有的。不过,倒是有一个私有变量的概念。任何在函数中定义的变量,都可以认为是私有变量,因为不能在函数的外部访问这些变量。私有变量包括函数的参数、局部变量和在函数内部定义的其他函数。
把有权访问私有变量和私有函数的公有方法称为特权方法。
有两种在对象上创建特权方法的方式。第一种是在构造函数中定义特权方法。
function MyObject() {
//私有变量和私有函数
var privateVariable = 10;
function privateFunction() {
return false;
}
//特权方法
this.publicMethod = function() {
privateVariable++;
return privateFunction();
};
}
能够在构造函数中定义特权方法,是因为特权方法作为闭包有权访问在构造函数中定义的所有变量和函数。
变量 privateVariable
和函数privateFunction()
只能通过特权方法publicMethod()
来访问。
利用私有和特权成员,可以隐藏那些不应该被直接修改的数据:
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"
构造函数中定义了两个特权方法: getName()
和setName()
。由于这两个方法是在构造函数内部定义的,它们作为闭包能够通过作用域链访问name
。
1、静态私有变量
通过在私有作用域中定义私有变量或函数,同样也可以创建特权方法。
(function() {
//私有变量和私有函数
var privateVariable = 10;
function privateFunction() {
return false;
}
//构造函数
MyObject = function() {};
//公有/特权方法
MyObject.prototype.publicMethod = function() {
privateVariable++;
return privateFunction();
};
})();
这个模式创建了一个私有作用域,并在其中封装了一个构造函数及相应的方法。在私有作用域中,首先定义了私有变量和私有函数,然后又定义了构造函数及其公有方法。公有方法是在原型上定义的,这一点体现了典型的原型模式。需要注意的是,这个模式在定义构造函数时并没有使用函数声明,而是使用了函数表达式。
记住:初始化未经声明的变量,总是会创建一个全局变量。
没有在声明 MyObject
时使用 var
关键字。因此, MyObject
就成了一个全局变量,能够在私有作用域之外被访问到。在严格模式下给未经声明的变量赋值会导致错误。
这个模式与在构造函数中定义特权方法的主要区别,就在于私有变量和函数是由实例共享的。由于特权方法是在原型上定义的,因此所有实例都使用同一个函数。而这个特权方法,作为一个闭包,总是保存着对包含作用域的引用。
(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"
以这种方式创建静态私有变量会因为使用原型而增进代码复用,但每个实例都没有自己的私有变量。
提示:多查找作用域链中的一个层次,就会在一定程度上影响查找速度。而这正是使用闭包和私有变量的一个显明的不足之处。
2、模块模式
模块模式(module pattern)
是为单例创建私有变量和特权方法。所谓单例,指的就是只有一个实例的对象。
var singleton = function() {
//私有变量和私有函数
var privateVariable = 10;
function privateFunction() {
return false;
}
//特权/公有方法和属性
return {
publicProperty: true,
publicMethod: function() {
privateVariable++;
return privateFunction();
}
};
}();
这个模块模式使用了一个返回对象的匿名函数。在这个匿名函数内部,首先定义了私有变量和函数。然后,将一个对象字面量作为函数的值返回。返回的对象字面量中只包含可以公开的属性和方法。
这种模式在需要对单例进行某些初始化,同时又需要维护其私有变量时是非常有用的。
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);
}
}
};
}();
如果必须创建一个对象并以某些数据对其进行初始化,同时还要公开一些能够访问这些私有数据的方法,那么就可以使用模块模式。以这种模式创建的每个单例都是 Object 的实例,因为最终要通过一个对象字面量来表示它。
3、增强的模块模式
有人进一步改进了模块模式,即在返回对象之前加入对其增强的代码。这种增强的模块模式适合那些单例必须是某种类型的实例,同时还必须添加某些属性和(或)方法对其加以增强的情况。
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;
}();
小结
函数表达式的特点:
- 函数表达式不同于函数声明。函数声明要求有名字,但函数表达式不需要。没有名字的函数表达式也叫做匿名函数。
- 在无法确定如何引用函数的情况下,递归函数就会变得比较复杂;
- 递归函数应该始终使用
arguments.callee
来递归地调用自身,不要使用函数名——函数名可能会发生变化。
当在函数内部定义了其他函数时,就创建了闭包。闭包有权访问包含函数内部的所有变量,原理如下。
- 在后台执行环境中,闭包的作用域链包含着它自己的作用域、包含函数的作用域和全局作用域。
- 通常,函数的作用域及其所有变量都会在函数执行结束后被销毁。
- 但是,当函数返回了一个闭包时,这个函数的作用域将会一直在内存中保存到闭包不存在为止。
使用闭包可以在 JavaScript
中模仿块级作用域(JavaScript
本身没有块级作用域的概念),要点如下。
- 创建并立即调用一个函数,这样既可以执行其中的代码,又不会在内存中留下对该函数的引用。
- 结果就是函数内部的所有变量都会被立即销毁——除非将某些变量赋值给了包含作用域(即外部作用域)中的变量。
闭包还可以用于在对象中创建私有变量,相关概念和要点如下。
- 即使
JavaScript
中没有正式的私有对象属性的概念,但可以使用闭包来实现公有方法,而通过公有方法可以访问在包含作用域中定义的变量。- 有权访问私有变量的公有方法叫做特权方法。
- 可以使用构造函数模式、原型模式来实现自定义类型的特权方法,也可以使用模块模式、增强的模块模式来实现单例的特权方法。
创建闭包必须维护额外的作用域,过度使用它们可能会占用大量内存。