JavaScript中的函数表达式与闭包

函数

函数是JavaScript中最有特色同时又容易让人困扰的特性。定义函数的方式有两种:函数声明和函数表达式。

1
2
3
4
5
6
7
8
9
//函数声明
function foo (arg0, arg1) {
//函数体
}
//函数表达式
var bar = function () {
//函数体
}

在非IE浏览器中,function都有非标准的name属性,属性值为function后的标识符或表达式的接收变量名。在函数声明里有一个重要特征——函数声明提升(function declaration hoisting)。这意味着函数声明可以放在调用它的语句后。

1
2
3
4
sayHi();
function sayHi () {
alert("Hi!"); // "Hi!"
}

而函数表达式则不能这样使用,因为变量声明提升会将函数名提升,下面的代码将导致错误。

1
2
3
4
sayHi();
var sayHi = function () {
alert("Hi!"); // Error!
}

正确理解函数声明提升将会避免很多潜在的错误,或者干脆养成好习惯——定义在前,调用在后

递归

递归函数是一个函数通过调用自身得到的。如

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

这是一个经典的递归阶乘函数。不过当我们不知道函数名或者函数是匿名函数时,可以通过arguments.callee来调用自身。
argument.callee是一个指向正在执行的函数的指针。

在ES5的严格模式下,arguments.callee特性是禁止使用的。它将影响解释器的优化和运行效率。

闭包

闭包几乎是前端面试必考的一个知识点。它的存在是JavaScript中作用域链带来的特性。闭包是指有权访问另一个函数
作用域中变量的函数。创建闭包最常用的方式就是在函数内部创建另一个函数。就像下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
function fatherFunction (propertyName, value) {
var sum = value;
return function (object1, object2) {
var a = object1[propertyName],
b = object2[propertyName];
if (a + b > value) {
return 1
} else {
return 0;
}
}
}

可以看到,在返回的函数中引用了外部函数的变量propertyNamesum。即使这个函数已经返回,只要内部函数还存在,那么这两个变量就仍然可以访问。这就是闭包的直观体现。

解释闭包就要先理解JS中的作用域链。执行环境是JS中的一个关键概念。它定义了变量或函数可以访问的数据。全局执行环境是最外层的执行环境。根据ECMAScript实现宿主的不同,全局执行环境也各不相同。某个执行环境中的代码执行完毕后,环境被销毁,包括其中的所有变量和函数定义。JS中的每个函数都有自己的执行环境。执行流进入一个函数时,函数环境就被推入到环境栈中,待执行完毕后出栈。在执行环境中的代码时,会创建变量对象的作用域链,由当前的活动对象到外部环境变量对象一直到全局执行环境中的变量对象。内部环境可以通过作用域链访问所有外部环境,但是外部环境不能访问内部环境中的变量和函数。

现在回过头看闭包。在函数执行过程中,为了读取和写入变量值,需要保存整个作用域链。因此,在fatherFunction中创建的匿名函数的作用域链实际上包含了fantherFunction()的活动对象(输入参数和变量)以及全局变量对象。在fatherFunction()返回时,匿名函数的作用域链仍然引用着它的活动对象,使其并不会被销毁,直到匿名函数被销毁。

1
2
3
4
5
6
// 创建函数
var compare = fatherFunction("value", 0);
// 调用函数
var res = compare({value: 2}, {value: -1});
// 解除匿名函数的引用,释放内存
compare = null;

由于闭包会携带包含它的函数作用域,过度使用闭包会导致内存占用过多。忘记解除匿名函数引用还会导致内存泄漏。

闭包与变量

闭包可以取得父级函数的变量的最终值,因此配合for循环这样的结构就容易发生意外,就像下面的函数:

1
2
3
4
5
6
7
8
9
10
function bindClickFunctions () {
var buttons = document.getElementsByTagName('button');
// 让我们假设length是20
for (var i = 0, len = buttons.length; i < len; i++) {
buttons[i].onclick = function () {
alert(i);
}
}
return;
}

上面的函数会为所有的按钮绑定点击事件,不过效果却并不像预想中那样,不同的button弹出不同的值。结果是所有的button在点击后弹出的都是20。这是因为所有的匿名函数都使用着同一个外部函数的活动对象。可以通过在创建一层闭包来达到预期的目的。

1
2
3
4
5
6
7
8
9
10
11
function bindClickFunctions () {
var buttons = document.getElementsByTagName('button');
for (var i = 0, len = buttons.length; i < len; i++) {
buttons[i].onclick = function (i) {
return function () {
alert(i);
}
}(i);
}
return;
}

我们在每层循环中创建了一个匿名函数,匿名函数包含一个输入参数i,再分别保存在内部函数的作用域链中,就可以使闭包间引用的i互不干扰了。

块级作用域

JavaScript中是没有块级作用域的。不过可以利用匿名函数的作用域模拟一个块级作用域出来。在其中定义的私有变量也不必担心与其他作用域的变量名相冲突。这种用法很常用于最外层的封装,用于隐藏代码中的变量,在一定程度上保证安全。

1
2
3
4
(function(){
var foo = "You can see me, but you cannot touch me."
alert(foo);
})()

私有变量

同样的,JavaScript中是没有私有成员的概念的。但是,利用闭包可以制造出私有变量。原理是,利用函数作用域隐藏其中的变量甚至输入参数,通过返回的闭包操作这些“私有”变量。如下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
function Stu (name) {
this.getName = function () {
return name;
};
this.setName = function (value) {
name = value;
}
}
var stu = new Stu("Shenlvmeng");
alert(stu.getName());
stu.setName("Weii");
alert(stu.getName());

这里只是一个很简单的展示,红宝书中还介绍了模块模式和增强模块模式,利用闭包的特点实现了单例的构造和特权方法。下面对上面的Stu函数进行改造,可以使得所有通过Stu()构造的对象都有相同的公有变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
(function(){
var name = '';
Stu = function (value) {
name = value;
};
Stu.prototype.getName = function () {
return name;
};
Stu.prototype.setName = function (value) {
name = value;
};
})();
var stu1 = new Stu('shenlvmeng');
alert(stu1.getName());
var stu2 = new Stu('weii');
alert(stu2.getName()); // "weii"
alert(stu1.getName()); // "weii"