文章目录
  1. 1. 递归
  2. 2. 闭包
    1. 2.1. 闭包与变量
    2. 2.2. 关于this
    3. 2.3. 内存泄漏
  3. 3. 模仿块级作用域
  4. 4. 私有变量
    1. 4.1. 静态私有变量
    2. 4.2. 模块模式

递归

arguments.callee 是一个指向正在执行的函数的指针,因此可以用它来实现对函数的递归调用。

1
2
3
4
5
6
7
function factorial(num){
if(num <= 1){
return 1;
}else{
return num * arguments.callee(num-1);
}
}

通过使用 argument.callee 代替函数名,可以确保无论怎样调用函数都不会出问题。因此,在编写递归函数时,使用 arguments.callee 总比使用函数名更保险。
但在严格模式下,不能通过脚本访问 arguments.callee, 访问这个属性会导致错误。不过,可以使用函数表达式来达成相同的结果。

1
2
3
4
5
6
7
var factorial = (function f(num)) {
if (num <= 1) {
return 1;
} else{
return num * f(num-1);
}
}

即便把函数赋值给了另一个变量,函数的名字 f 仍然有效,所以递归调用照样能正确完成。这种方式在严格模式和非严格模式下都行得通。

闭包

闭包是指有权访问另一个函数作用域中的变量的函数。创建闭包的常见方式,就是在一个函数内部创建另一个函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;
}
}
}

内部函数的代码访问了外部函数中的变量 propertyName。 即使这个内部函数被返回了,而且是在其它地方被调用了,但它仍然可以访问变量propertyName。之所以还能访问这个变量,是因为内部函数的作用域中包含 createComparisonFunction() 的作用域。要彻底搞清楚其中的细节,必须从理解函数被调用的时候都会发生什么入手。
当某个函数被调用时,会创建一个执行环境及相应的作用域链。然后,使用 arguments 和其他命名参数的值来初始化函数的活动对象。但在作用域链中,外部函数的活动对象始终位于第二位,外部函数的外部函数的活动对象始终位于第三位,……直至作为作用域终点的全局执行环境。
在函数执行过程中,为读取和写入变量的值,就需要在作用域中查找变量。

1
2
3
4
5
6
7
8
9
10
11
function compare(value1, value2) {
if (value1 < value2) {
return -1;
} else if (value1 > value2) {
return 1;
} else {
return 0;
}
}
var result = compare(5, 10);

以上代码先定义了 compare() 函数, 然后又在全局作用域中调用了它。当调用 compare() 时,会创建一个包含 arguments、value1、value2 的活动对象。全局执行环境(包含 result 和 compare) 在 compare() 执行环境的作用域链中则处于第二位。

后台的每个执行环境都有一个表示变量的对象---变量对象。全局环境的变量对象始终存在,而像 compare() 函数这样的局部环境的变量对象,则只在函数的执行的过程中存在。
无论什么时候再函数中访问一个变量时,就会从作用域链中搜索具有响应名字的变量。一般来讲,当函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局作用域(全局执行环境的变量对象)。但是,闭包的情况又有所不同。
在另一个
在另一个函数内部定义的函数会将包含函数(即外部函数)的活动对象添加到它的作用域链中。因此,在 createComparisonFunction() 函数内部定义的匿名函数的作用域链中,实际上将会包含外部函数 createComparisonFunction() 的活动对象。

1
2
var compare = createComparisonFunction("name");
var result = compare({name: "Nicholas"}, {name: "Greg"});

在匿名函数从 createComparisonFunction() 中被返回后,它的作用域链被初始化为包含 createComparisonFunction() 函数的活动对象和全局变量对象。这样,匿名函数就可以访问在 createComparisonFunction() 中定义的所有变量。更为重要的是,createComparisonFunction() 函数在执行完毕后,其活动对象也不会被销毁,因为匿名函数的作用域链仍然在引用这个活动对象。换句话说,当 createComparisonFunction() 函数返回后,其执行环境的作用域链会被销毁,但它的活动对象仍然会留在内存中;直到匿名函数被销毁返回后,createComparisonFunction() 的活动对象才会被销毁。

1
2
3
4
5
6
7
8
//创建函数
var compareNames = createComparisonFunction("name");
//调用函数
var result = compareNames({ name: "Nicholas"}, { name, "Greg"});
//解除对匿名函数的引用(以便释放内存)
compareNames = null;

首先,创建的比较函数被保存在变量 compareNames 中。而通过将 compareNames 设置为等于null解除该函数的引用,就等于通知垃圾回收例程将其清除。随着匿名函数的作用域链被销毁,其它作用域(除了全局作用域)也都可以安全地销毁了。

闭包与变量

作用域链的这种配置机制引出了一个值得注意的副作用,即闭包只能取得包含函数中任何变量的最后一个值。因为闭包所保存的是整个变量对象,而不是某个特殊的变量。

1
2
3
4
5
6
7
8
9
function createFunctions(){
var result = new Array();
for (var i=0; i < 10; i++) {
result[i] = function(){
return i;
};
}
return result;
}

此时每个函数都引用着保存变量 i 的同一个变量对象,所以在每个函数内部 i 的值都是10.但是,可以通过创建另一个匿名函数强制让闭包的行为符合预期。

1
2
3
4
5
6
7
8
9
10
11
12
function createFunctions(){
var result = new Array();
for(var i=0; i < 10; i++) {
result[i] = function(num){
return function() {
return num;
};
}(i);
}
return result;
}

关于this

this 对象是在运行时基于函数的执行环境绑定的:在全局函数中,this 等于 window,而当函数被作为某个对象的方法调用时,this 等于那个对象。不过,匿名函数的执行环境具有全局性,因此其 this 对象通常指向window。

1
2
3
4
5
6
7
8
9
10
11
12
13
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
return function(){
return this.name;
}
}
}
alert(object.getNameFunc()()); //"The Window" (在非严格模式下)

每个函数在被调用时都会自动取得这两个特殊变量:this 和 arguments。内部函数在搜索这两个变量时,只会搜索到其活动对象为止,因此永远不可能直接访问外部函数中的这两个变量。

this 和 arguments 也存在同样的问题。如果想访问作用域中的 arguments 对象,必须将该对象的引用保存到另一个闭包能够访问的变量中。

内存泄漏

如果闭包的作用域链中保存着一个HTML元素,那么久意味着该元素无法被销毁。

1
2
3
4
5
6
function assignHandler(){
var element = document.getElementById("someElement");
element.onclick = function(){
alert(element.id);
}
}

这个闭包创建了一个循环引用。由于匿名函数保存了一个对 assignHandler() 的活动对象的引用,因此就会导致无法减少 element 的引用数。只要匿名函数存在,element 的引用数至少也是 1,因此它所占用的内存就永远不会被回收。不过,这个问题可以通过稍微改一下代码来解决。

1
2
3
4
5
6
7
8
9
10
function assignHandler(){
var element = document.getElementById("someElement");
var id = element.id;
element.onclick = function(){
alert(id);
}
element = null;
}

通过把 element.id 的一个副本保存在一个变量中,并且在闭包中引用该变量消除循环引用。但仅仅做到这一步,还是不能解决内存泄漏的问题。因为闭包会引用闭包包含函数的整个活动对象,而其中包含着 element。即使闭包不直接引用 element,包含函数的活动对象中也仍然会保存引用。因此,有必要把 element 变量设为 null。这样就能够解除对 DOM 对象的引用,顺利地减少其引用数,确保正常回收其占用的内存。

模仿块级作用域

无论在什么地方,只要临时需要一些变量,就可以使用私有作用域。

1
2
3
4
5
6
7
8
function outputNumbers(count) {
(function () {
for (var i=0; i < count; i++) {
alert(i);
}
})();
alert(i); //导致一个错误!
}

私有变量

有权访问私有变量和私有函数的公有方法称为特权方法。有两种在对象上创建特权方法的方式。第一种是在构造函数中定义特权方法,基本模式如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function MyObject() {
//私有变量和私有函数
var privateVariable = 10;
function privateFunction(){
return false;
}
//特权方法
this.publicMethod = function (){
privateVariable++;
return privateFunction();
}
}

利用私有和特权成员,可以隐藏那些不应该直接被修改的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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"

在函数中定义特权方法也有缺点,那就是必须使用构造函数模式来达到这个目的。构造函数的缺点是针对每个实例都会创建同样一组新方法,而使用静态私有变量来实现特权方法就可以避免这个问题。

静态私有变量

通过在私有作用域中定义私有变量或函数,同样也可以创建特权方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
(function(){
//私有变量和私有函数
var privateVariable = 10;
function privateFunction(){
return false;
}
//构造函数
MyObject = function(){
};
//公有/特权方法
MyObject.prototype.publicMethod = function(){
privateVariable++;
return privateFunction();
}
})();

这个模式在定义构造函数时并没有使用函数声明,而是使用了函数表达式。函数声明只能创建局部函数,但那并不是我们想要的。出于同样的原因,我们也没有在声明Object时使用 var 关键字。初始化未经声明的变量,总是会创建一个全局变量。因此,MyObject 就成了一个全局变量,能够在私有作用域外被访问到。但在严格模式下给未经生命的变量赋值会导致错误。
这个模式与在构造函数中定义特权方法的主要区别,就在于私有变量和函数是由实例共享的。由于特权方法是在原型上定义的,因此所有实例都使用同一个函数。而这个特权方法,作为一个闭包,总是保存着对包含作用域的引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
(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"

在这种模式下,变量name就变成了一个静态的、由所有实例共享的属性。也就是说,在一个实例上调用 setName() 会影响所有实例。而调用 seName() 或新建一个 Person 实例都会赋予 name 属性一个新值。
以这种方式创建静态私有变量会因为使用原型而增进代码复用,但每个实例都没有自己的私有变量。到底是使用实例变量,还是静态私有变量,最终还是要视具体需求而定。

多查找作用域链中的一个层次,就会在一定程度上影响查找速度。而这正是使用闭包和私有变量的一个明显的不足之处。

模块模式

前面的模式是用于为自定义类型创建私有变量和特权方法的。而模块模式则是为单例创建私有变量和特权方法。所谓单例,值得就是只有一个实例的对象。按照惯例,Javascript是以对象字面量的方式来创建单例对象的。

1
2
3
4
5
6
var singleton = {
name : value,
method : function(){
//这里是方法的代码
}
}

模块模式通过为单例添加私有变量和特权方法能够使其得到增强,其语法形式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var singleton = function(){
//私有变量和私有函数
var privateVariable = 10;
function privateFunction(){
return false;
}
//特权/公有方法和属性
return {
publicProperty: true,
publicMethod : function(){
privateVariable++;
return privateFunction();
}
}
}()

从本质上来讲,这个对象字面量定义的是单例的公共接口。这种模式在需要对单例进行某些初始化,同时又需要维护其私有变量时是非常有用的,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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 的实例,因为最终要通过一个对象字面量来表示它。

文章目录
  1. 1. 递归
  2. 2. 闭包
    1. 2.1. 闭包与变量
    2. 2.2. 关于this
    3. 2.3. 内存泄漏
  3. 3. 模仿块级作用域
  4. 4. 私有变量
    1. 4.1. 静态私有变量
    2. 4.2. 模块模式