0%

JavaScript学习笔记(7)

《JavaScript高级程序设计》第七章,函数表达式。


以前我们提到过定义函数的两种方式:函数声明和函数表达式。我们先谈谈它们的一些区别和特点。

  • 函数有一个非标准的name属性。
1
2
3
4
5
6
7
function functionName(arg0, arg1) {...}
alert(functionName.name); // "functionName"

var functionName = function() {...};
alert(functionName.name); // "functionName"

alert((function(){...}).name);// ""
  • 函数声明提升(function declaration hoisting),即在执行代码前会先读取函数声明。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 可以这样做
sayHi();
function sayHi() {...}

if(condition) {
sayHi = function() {
alert("Hi");
};
} else {
sayHi = function() {
alert("Yo");
};
}

// 不可以这样做
sayHi();
var sayHi = function() {...};

if(condition) {
function sayHi() {
alert("Hi");
}
} else {
function sayHi() {
alert("Yo");
}
}
  • function关键字后无标识符的函数称为匿名函数或${\lambda}$函数。
    匿名函数有许多作用,以下会展开讲。

递归

递归函数没什么好讲的,就是自己调用自己,但之前第五章也谈到它的一个问题:递归函数和函数名的耦合问题。

1
2
3
4
5
6
7
8
9
10
11
function factorial(num) {
if(num <= 1) {
return 1;
} else {
return num * factorial(n - 1);
}
}

var anotherFactorial = factorial;
factorial = null;
anotherFactorial(4); // 出错

解决思路:

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

不过在严格模式下不能通过脚本访问arguments.callee。

  • 使用匿名函数
1
2
3
4
5
6
7
var factorial = (function f(num) {
if(num <= 1) {
return 1;
} else {
return num * f(n - 1);
}
});

这里创建了一个名为f()的命名函数表达式,再将其赋值给变量factorial,即便把函数赋值给了另一个变量,函数的名字f依然有效,因此递归调用可以正确完成。

闭包

闭包是指有权访问另一个函数作用域的变量的函数。内部函数之所以能访问外部函数作用域中的变量,是因为第四章讲过的作用域链。

作用域链构建过程

当一个函数被创建,首先会创建一个预先包含全局变量对象的作用域链,这个作用域链被保存在函数内部属性[[Scope]]中。
当一个函数被调用,首先会创建一个执行环境,然后将函数的[[Scope]]属性复制到执行环境的(scope chain)中。然后使用this、arguments等参数初始化活动对象,并将其作为变量对象推入执行环境的(scope chain)的前端。
作用域链的本质,是一个指向变量对象的指针列表,它只引用但不实际包含变量对象。

举例

普通函数

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);

一般来说,当函数执行完毕,它执行环境的作用域链和局部活动对象就会被销毁,内存中仅保存全局作用域(全局执行环境的变量对象)。

返回函数的闭包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function createCompFunc(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 = createCompFunc("name");
var result = compare({name: "Nicholas"}, {name: "Greg"});

这种情况下,当函数执行完毕,它执行环境的作用域链会被销毁,但局部活动对象不会被销毁,而是继续保存在内存中。原因在于有变量仍在引用它返回的匿名函数,而该匿名函数的作用域链仍在引用该活动对象。
若想将该函数的活动对象销毁,必须要销毁匿名函数,也就是解除对匿名函数的引用,来通知垃圾回收例程将所有清除,只留下全局作用域。

1
2
// 在调用结束解除引用
compare = 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;
}

它会返回包含10个函数的数值,表面看上去每一个函数都应返回自己的索引值,但实际上每个函数都会返回10。因为每个函数的作用域链中都保存了createFunctions()的活动对象,它们引用的都是同一个变量i。当createFunctions()返回时,变量i为10,因此每个函数顺着作用域链找到的i就是10。
如果想要让它的结果符合预期,可以把它包在一个立即调用的函数中。

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

这里没有直接把闭包赋值给数组,而是定义了一个匿名函数,并将立即执行该匿名函数的结果赋值给数组。该匿名函数接收参数num,返回一个返回参数num的函数,这个函数就是一个能够访问外层num的闭包。由于函数参数是按值传递的,因此每个当前的num都会被赋值为当前的i,也就是result数组中每个函数都有自己的num副本,它也就能返回不同的值了。

闭包与this对象

this对象是运行时基于函数的执行环境绑定的,全局环境中this等于window,某个对象调用某函数时this等于该对象。而匿名函数的执行环境具有全局性,因此它的this通常指向window。

1
2
3
4
5
6
7
8
9
10
var name = "The Window";
var object = {
name: "My Object",
getName: function() {
return function() {
return this.name;
};
}
};
alert(object.getName()()); // "The Window"

要注意,this的指代也是要顺着作用域链查找的。每个函数在被调用时,其活动对象都会自动取得两个特殊变量:this和arguments。这里它发现自己的执行环境是全局环境,this自然取得window,因此也不用再向外层查找了。
如果想访问外部作用域的this对象,可以将该this对象保存在一个闭包能够访问到的地方,再用闭包把它带出来。

1
2
3
4
5
6
7
8
9
10
11
var name = "The Window";
var object = {
name: "My Object",
getName: function() {
var that = this;
return function() {
return that.name;
};
}
};
alert(object.getName()()); // "My Object"

这里我们在定义匿名函数之前,将this对象赋值给了that变量,我们可以用闭包把that带出来。因为that一直是引用着object的,因此我们可以访问到object.name。
下面开动一下脑筋:

1
2
3
4
5
6
7
8
9
10
11
var name = "The Window";
var object = {
name: "My Object",
getName: function() {
return this.name;
}
};

object.getName(); // "My Object"
(object.getName)(); // "My Object"
(object.getName = object.getName)(); // "The Window"

第二行因为object.getName和(object.getName)的定义是相同的,因此this的值得到了维持;第三行先执行了赋值语句,然后再调用赋值后的结果,因为赋值表达式的值是函数本身,因此this的值没有得到维持。我觉得可能第三个和这个类似:

1
2
var func = object.getName;
func(); // "The Window"

内存泄漏

第四章曾讲过引用计数方法,匿名函数由于可以引用外部的内容,而我们又很少显式地解除对匿名函数的引用,因此容易造成内存得不到释放的问题。

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
function assignHandler() {
var element = document.getElementById('someElement');
var id = element.id;
element.onclick = function() {
alert(id);
};
element = null;
}

这里减少了两处对element的引用:一是使用一个副本id,二是显式地将element设置为null;前者避免了显式的引用,后者避免了通过活动对象的引用。

利用匿名函数

模仿块级作用域

JavaScript中没有块级作用域,因此如下操作是可行的:

1
2
3
4
5
6
7
function outputNumbers(count) {
for(var i = 0; i < count; i++) {
alert(i);
}
var i; // 可以重新声明变量
alert(i); // 可以访问for中的变量
}

如果想要创建一个块级作用域,可以将所有内容塞进一个匿名函数中并立即调用:

1
2
3
4
5
6
7
8
9
(function() {
// 块级作用域
})();

// 因为它等价于
var someFunction = function() {
// 块级作用域
};
someFunction();

因此我们可以将最初的例子进行重写,在for循环上添加块级作用域:

1
2
3
4
5
6
7
8
function outputNumbers(count) {
(function() {
for(var i = 0; i < count; i++) {
alert(i);
}
})();
alert(i); // 出错
}

在这里,一方面,匿名函数中定义的任何变量都会在执行结束时被销毁,因此其内的变量无法被外部访问;另一方面,匿名函数是一个闭包,因此能够访问包含作用域中的所有变量。
我们时常在全局作用域中使用这种方法,来限制向全局作用域中添加过多的变量和函数,以防止发生命名冲突和全局作用域的混乱。同时,这种做法因为没有指向匿名函数的引用,函数执行完毕其作用域链就会被销毁,减少了闭包对内存的占用。

实现特权方法

严格来讲JavaScript中无私有成员的概念,所有对象属性都是公有的。但有一个私有变量的概念,即任何在函数中定义的变量,都可以认为是私有变量,因为无法在函数的外部访问到它们。
但如果我想在外部访问它们,有什么方法吗?答案是闭包。我们可以通过在函数内创建闭包来将私有变量带出来,我们将这类能访问私有变量和函数的公有方法称为特权方法(privileged method)。如何创建特权方法呢?

构造函数模式

模式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
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
function Person(name) {
this.getName = function() {
return name;
};
this.setName = function(value) {
name = value;
};
}

var person = Person("Niciholas");
alert(person.getName()); // "Nicholas"
person.setName("Greg");
alert(person.getName()); // "Gerg"

这里只有getName()setName()两个特权方法能在构造函数外部访问name。但在构造函数中定义特权方法有缺点:只能用构造函数不灵活;每个实例都会创建同样的一组新方法。为了避免这个问题,可以用静态私有变量实现特权方法。

原型模式

模式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(function() {
// 私有变量和函数
var privateVariable = 10;
function privateFunction() {
return false;
}

// 构造函数
MyObject = function() {};

// 特权方法
MyObject.prototype.publicMethod = function() {
privateVariable++;
return privateFunction();
};
})();

要注意:定义构造函数时候使用的是函数表达式而非函数声明,也没有使用var关键字,目的是为了创建全局构造函数而非局部函数。
这里的特权方法定义在原型上,因此所有实例都使用同一个函数,而由于特权方法作为闭包,总是保存着对包含作用域的引用,因此它访问的私有变量和函数都是由实例共享的。比如下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
(function() {
var name = "";

Person = function(value) {
name = value;
};

Perosn.prototype.getName = function() {
return name;
};

Person.prototype.setName = function(value) {
name = value;
};
})();

var person1 = new Person("Nicholas");
alert(person1.getName()); // "Nicholas"
var person2 = new Person("Greg");
alert(person1.getName()); // "Greg"

模块模式

前两个模式都是为自定义类型创建私有变量和特权方法,Douglas提出为单例创建私有变量和特权方法,称为模块模式。单例(singleton)指只有一个实例的对象,JavaScript是以对象字面量的方式来创建单例对象的。模块模式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var singleton = function() {
// 私有变量和函数
var privateVariable = 10;
function privateFunction() {
return false;
}

// 特权方法
return {
publicProperty: true,
publicMethod: function() {
privateVariable++;
return privateFunction();
}
}
}();

这里的单例设置为一个匿名函数的立即调用,该匿名函数内包含私有的变量和函数,返回一个对象字面量。该对象字面量一方面能够访问函数内部的私有内容,另一方面可以给外部使用自己的方法,它也就相当于是单例的公共接口了。这种模式适合需要对单例进行初始化,又需要在维护其私有变量的同时公开一些能够访问私有数据的方法的情况。

增强的模块模式

模块模式存在一个问题:返回的始终是原生的Object对象而不能指定类型,因此引入了这种新模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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;
}();

* 这篇写得没什么条理…理解深刻点再回来改吧。