定义函数的方式有两种:(1)函数声明;(2)函数表达式。
(1)函数声明
function test() {}
(2)函数表达式分为:匿名函数表达式;命名函数表达式。
匿名函数表达式会省略函数名,具体如下所示:
var test = function() {
// 函数体
};
命名函数表达式如下所示:
var test = (function f() {
// 函数体
});
7.1、递归
函数的递归:
function factorial(num) {
if (num <= 1) {
return 1;
} else {
return num*arguments.callee(num-1);
}
}
console.log(factorial(5)); // 120
命名函数实现递归:
var factorial1 = (function f(num) {
if (num <= 1) {
return 1;
} else {
return num*f(num-1);
}
});
f = null;
console.log(factorial1(5)); // 120
7.2、闭包
闭包:一个函数,且该函数有权访问另一个函数作用域中的变量。
当函数被调用时发生了什么?
当某个函数被调用时,会创建一个执行环境及相应的作用域链。然后,使用 arguments 和其他命名参数的值来初始化函数的活动对象。但在作用域链中,外部函数的活动对象始终处于第二位,外部函数的外部函数的活动对象处于第三位,......直至作为作用域链终点的全局执行环境。
function compare(value1, value2) {
if (value1 < value2) {
return -1;
} else if (value1 > value2) {
return 1;
} else {
return 0;
}
}
var result = compare(5, 10); // 当执行函数调用时,其作用域链如下所示。
当执行上述的函数调用表达式时,其作用域链如图1所示:
图1
后台的每个执行环境都有一个表示变量的对象——变量对象。全局环境的变量对象始终存在,而像 compare() 函数这样的局部环境的变量对象,则只在函数的执行过程中存在。
在创建 compare() 函数时,会创建一个预先包含全局变量对象的作用域链,这个作用域链被保存在内部的 [[Scope]] 属性中。当调用 compare() 函数时,会为函数创建一个执行环境,然后通过复制函数的 [[Scope]] 属性中的对象构建起执行环境的作用域链。
此后,又有一个活动对象(在此作为变量对象使用)被创建并被推入执行环境作用域链的前端。
作用域链本质上是一个指向变量对象的指针列表,它引用但不实际包含变量对象。
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 compare = createComparisonFunction("name");
var result = compare({name: "Nicholas"}, {name: "Greg"});
上述代码最后两行执行结果的作用域链如图2所示。
图2
由图2可知,createComparisonFunction() 函数执行完毕后,其活动对象也不会销毁,因为匿名函数的作用域链任然在引用这个活动对象。即当 createComparisonFunction() 函数返回后,其执行环境的作用域链会被销毁,但它的活动对象任然留存在内存中;直到匿名函数被销毁后, createComparisonFunction() 的活动对象才会被销毁。
注意:由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存。
7.2.1、闭包与变量
注意:闭包只能取得包含函数中任何变量的最后一个值。
function createFunction() {
var result = new Array();
for (var i = 0; i < 10; i++) {
result[i] = function() {
return i;
};
}
return result;
}
var result2 = createFunction();
for (var i = 0; i < result2.length; i++) {
console.log(result2[i]()); // 输出10个 10
}
因为每个函数的作用域链中都保存着 createFunctions() 函数的活动对象,所以它们引用的都是同一个变量 i 。
可以进行如下修改:
function createFunction() {
var result = new Array();
for (var i = 0; i < 10; i++) {
result[i] = function(num) {
return function() {
return num;
};
}(i);
}
return result;
}
var result2 = createFunction();
for (var i = 0; i < result2.length; i++) {
console.log(result2[i]()); // 输出1 2 3 4 5 6 7 8 9
}
7.2.2、关于 this 对象
this 对象在运行时基于函数的执行环境绑定:在全局函数中,this 等于 window,而当函数被作为某个对象的方法调用时,this 等于那个对象。不过,匿名函数的执行环境具有全局性,因此其 this 对象通常指向 window。
var name = 'The Window';
var object = {
name: 'My Object',
getNameFunc: function() {
return function() {
return this.name;
};
}
};
console.log(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;
};
}
};
console.log(object.getNameFunc()()); // "The Object"
如果想访问作用域中的 arguments 对象,必须将对该对象的引用保存到另一个闭包能访问的变量中。
7.2.3、内存泄露
注意:闭包会引用包含函数的整个活动对象。
7.3、模仿块级作用域
JS从来不会告诉你是否多次声明了同一个变量;当遇到变量重新声明,它只会对后续的声明视而不见(不过,它会执行后续声明中的变量初始化)。
function outputNumbers(count) {
for (var i = 0; i < count; i++) {
console.log(i);
}
var i;
console.log(i);
}
outputNumbers(5); // 0 1 2 3 4 5
var b = 20;
var b;
console.log(b); // 20
将函数声明包含在一对圆括号中,表示它实际上是一个函数表达式。而紧随其后的另一对圆括号会立即调用这个函数。
注意:在 JS 中将 function 关键字当作一个函数声明的开始,而函数声明后面不能跟圆括号。
7.4、私有变量
在 JS 中有私有变量的概念:任何函数定义的变量都可以认为是私有变量,因为不能在函数的外部访问这些变量。私有变量包括函数的参数、局部变量和在函数内部定义的其他变量。
利用闭包可以创建用于访问私有变量的公有方法。
特权方法:有权访问私有变量和私有函数的公有方法。
在对象上创建特权方法的两种方式:(1)在构造函数中定义特权方法;(2)在私有作用域中定义私有变量或函数。
(1)在构造函数中定义特权方法
缺点:在构造函数中定义特权方法也有一个缺点,那就是你必须使用构造函数模式来达到这个目的。而构造函数的模式的缺点在第6章已有介绍。
function MyObject() {
// 私有变量和函数
var privateVariable = 10;
function privateFunction() {
return false;
}
// 特权方法
this.publicMethod = function() {
privateVariable++;
return privateFunction();
}
}
var myObject = new MyObject();
console.log(myObject.publicMethod()); // false
利用私有和特权成员,可以隐藏那些不应该被直接修改的数据。
function Person(name) {
this.getName = function() {
return name;
};
this.setName = function(value) {
name = value;
};
}
var person = new Person('Nicholas');
console.log(person.getName()); // 'Nicholas'
person.setName('Greg');
console.log(person.getName()); // 'Greg'
7.4.1、静态私有变量
(2)在私有作用域中定义私有变量或函数,同样也可以创建特权方法。
(function() {
// 私有变量和私有函数
var privateVariable = 10;
function privateFunction() {
return false;
}
// 构造函数
// 初始化未经声明的变量,总会创建一个全局变量,但在严格模式会导致错误。
MyObject = function() {
};
// 公有/特权方法
MyObject.prototype.publicMethod = function() {
privateVariable++;
return privateFunction();
};
})();
在私有作用域中创建一个全局构造函数,并在该构造函数的原型中创建公有方法来访问私有作用域中的私有变量和私有函数。
(function() {
// 私有变量
var name = "";
Person = function(value) {
name = value;
};
Person.prototype.getName = function() {
return name;
};
Person.prototype.setName = function(value) {
name = value;
};
})();
var person = new Person('Nicholas');
console.log(person.getName()); // 'Nicholas'
person.setName('Greg');
console.log(person.getName()); // 'Nicholas'
var person1 = new Person('Michel');
console.log(person.getName()); // 'Michel'
console.log(person1.getName()) // 'Michel'
这个例子中的 Person 构造函数与 getName() 和 setName() 方法一样,都有权访问私有变量 name。在这种模式下,变量 name 就变成了一个静态的、由所有实例共享的属性。
7.4.2、模块模式
模块模式则是为单例创建私有变量和特权方法。
单例:指的就是只有一个实例的对象。按照惯例,JS 是以对象字面量的方式来创建单例对象的。
function BaseComponent() {
this.c = 1;
};
var application = function() {
// 私有变量和私有函数
var components = new Array();
// 初始化
components.push(new BaseComponent);
// 公共
return {
getComponentCout : function() {
return components.length;
},
registerComponent : function(component) {
if (typeof component == 'object') {
components.push(component);
}
}
};
}();
console.log(application.getComponentCout()); // 1
application.registerComponent(new BaseComponent());
console.log(application.getComponentCout()); // 2
如果必须创建一个对象并以某些数据对其进行初始化,同时还要公开一些能够访问这些私有数据的方法,那么就可以使用模块模式。以这种模式创建的每个单例都是 Object 的实例,因为最终要通过一个对象字面量来表示它。
单例通常都是作为全局对象存在的,我们不会将它传递一个函数。
7.4.3、增强的模块模式
有人进一步改进了模块模式,即在返回对象之前加入对其增强的代码。这种增强的模块模式适合那些单例必须是某种类型的实例,同时还必须添加某些属性和(或)方法对其加以增强的情况。
function BaseComponent2() {
this.name = 2;
}
var application2 = function() {
// 私有变量
var components = new Array();
components.push(new BaseComponent2());
// 创建一个 application 的一个局部副本
var app = new BaseComponent2();
// 公共接口
app.getComponentCount2 = function() {
return components.length;
};
app.registerComponent2 = function(component) {
if (typeof component == 'object') {
components.push(component);
}
};
// 返回这个副本
return app;
}();
console.log(application2.getComponentCount2()); // 1
application2.registerComponent2(new BaseComponent2());
console.log(application2.getComponentCount2()); // 2
参考文献
[1]《JavaScript高级程序设计(第3版)》