
定义函数
在 JS 中定义函数的方式有两种:函数声明 和 函数表达式 。
函数声明 的语法为:
function functionName(arg0, arg1, arg2) {
// 函数体;
}
函数声明的一个重要特征就是 函数声明提升 ,即在执行代码之前会先读取函数声明,这意味着可以把函数声明放在调用它的语句后面:
sayHi(); // 'hi';
sayHi() {
alert('hi');
}
第二种创建函数的形式是 函数表达式 。函数表达式有创建几种不同的语法形式,下面是最常见的一种:
var functionName = function(arg0, arg1, arg2) {
// 函数体;
}
这看来好像是常规的变量复制语句,即创建一个函数并将它赋值给变量 functionName,在这种情况下创建的函数叫做 匿名函数 ( 也称为 Lambda 拉姆达函数 ),在使用前必须先赋值。如下面的代码会报错:
sayHi(); // 错误,函数还不存在;
var sayHi = function(){
alert('hi');
}
理解函数提升的关键,就是理解函数声明与函数表达式之间的区别。例如,执行以下代码的结果可能会让人意想不到:
if(condition) {
function sayHi() {
alert('hi');
}
} else {
function sayHi() {
alert('Yo~');
}
}
表面上看,上述代码会在 condition 为 true 时使用一个 sayHi() 的定义,否则就使用另一个定义。实际上,这在 ECMAScript 中属于无效语法,JavaScript 引擎会尝试修正错误,将其转换为合理的状态,大多数浏览器会返回第二个声明,忽略 condition。
不过,如果是使用函数表达式,那就没什么问题:
var sayHi;
if(condition) {
sayHi = function() {
alert('hi');
}
} else {
sayHi = function() {
alert('Yo~');
}
}
这样不同的函数将根据不同的 condition 被赋值给 sayHi。
递归
递归函数是在一个函数通过名字调用自身的情况下构成的:
function factorial(num) {
if (num <= 1) {
return 1;
} else {
return num * factorial(num - 1);
}
}
这是一个经典的递归阶乘函数。虽然这个函数表面上看起来没有问题,但下面的代码却可能导致它出错:
var anotherFactorial = factorial;
factorial = null;
alert(anotherFactorial(4)); // error!
在调用 anotherFactorial() 时,由于必须执行 factorial(),而此时 factorial 已不再是函数,所以会导致错误。
在这种情况下,可以使用 arguments.callee 解决问题。
- arguments 是函数内部对象,它是一个类数组对象,包含着传入函数中的所有参数。arguments 对象有一个 callee 属性,该属性是一个指针,指向 arguments 所在的函数。
因此可以用它来实现对函数的递归调用:
function factorial(num) {
if (num <= 1) {
return 1;
} else {
return num * arguments.callee(num - 1);
}
}
通过使用 arguments.callee 代替函数名,可以确保无论怎样调用函数都不会出问题。因此,在编写递归函数时,使用 arguments.callee 总比使用函数名更保险。
不过在严格模式下,不能通过脚本访问 arguments.callee,访问这个属性会导致错误。but~ 我们可以使用命名一个函数表达式来完成同样的效果:
var factorial = (function f(num) {
if (num <= 1) {
return 1;
} else {
return num * f(num - 1);
}
});
这样递归调用仍能正常完成。
闭包
不少童鞋总是会混淆 匿名函数 和 闭包 这两个概念。 匿名函数 是没有实际名字的函数;而 闭包 是指有权访问另一个函数作用域中的变量的函数。
而创建闭包的常见方式,就是在一个函数内部创建另一个函数:
function compare(name) {
return function(obj1, obj2) {
var value1 = obj1[name]; // 可以访问到外部函数中的变量 name;
var value2 = obj2[name];
if (value1 < value2) {
return -1;
} else {
return 1
}
};
}
之所以还能够访问这个变量,是因为这个内部函数的作用域链中包含 compare() 的作用域。
而了解作用域的细节,对彻底理解闭包至关重要:
当某个函数被调用时,会创建一个执行环境及相应的作用域链。然后,使用 arguments 和其它命名参数的值来初始化函数的活动对象。在作用域链中,外部函数的活动对象始终处于第二位,外部函数的外部函数的活动对象处于第三位,......直至作为作用域链终点的全局执行环境。
在函数执行过程中,为读取和写入变量的值,就需要在作用域链中查找变量,来看下面的例子:
function compare(value1, value2) {
if (value1 < value2) {
return -1;
} else {
return 1;
}
}
var result = compare(5, 10);
上述代码首先定义了 compare() 函数。然后又在全局作用域中调用了它。
当第一次调用 compare() 时,会创建一个包含 this、arguments、value1 和 value2 的活动对象。全局执行环境的变量对象(包含 this、result 和 compare)在 compare() 执行环境的作用域中则处于第二位。
下图展示了包含上述关系的 compare() 函数执行时的作用域链:

全局环境的变量对象始终存在,而像 compare() 函数这样的局部环境的变量对象,则只在函数执行的过程中存在。
显然,作用域链本质上是一个指向变量对象的指针列表,它只引用但不实际包含变量对象。
无论什么时候在函数中访问一个变量时,都会从作用域链中搜索具有相应名字的变量。一般来讲,当函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局作用域。
但,闭包的情况又有所不同:
function compare(name) {
return function(obj1, obj2) {
var value1 = obj1[name]; // 可以访问到外部函数中的变量 name;
var value2 = obj2[name];
if (value1 < value2) {
return -1;
} else {
return 1
}
};
}
在匿名函数从 compare() 中被返回后,它的作用域链被初始化为包含 compare() 函数的活动对象和全局对象。
更为重要的是,compare() 函数在执行完毕后,其活动对象也不会被销毁,因为匿名函数的作用域链仍在引用这个活动对象,直到匿名函数被销毁后,compare() 的活动对象才会被销毁。
// 创建函数;
var compareNames = compare('name');
// 调用函数;
var result = compareNames({name: 'Fly_001'}, { name: 'juejin' });
// 解除对匿名函数的引用,以便释放内存;
compareNmaes = null;
Tips: 由于闭包会携带包含它的函数的作用域,因此会比其它函数占用更多的内存,所以过度使用闭包可能会导致内存占用过多。
闭包与变量
作用域链的这种配置机制引出了一个值得注意的副作用,即闭包只能取得包含函数中任何变量的最后一个值。
别忘了闭包保存的是整个变量对象,而不是某个特殊的变量。
下面是一个经典的例子:
function createFunctions() {
var result = [];
for (var i = 0; i < 10; i ++) {
result[i] = function() {
return i;
};
}
return result;
}
表面上看,似乎每个函数都应该返回自己的索引值,但实际上,每个函数都返回 10。
因为每个函数的作用域链中都保存着 createFunctions() 函数的活动对象,所以它们引用的都是同一个变量 i。
当 createFunctions() 函数返回时,变量 i 的值是 10,此时每个函数都引用着变量 i 的同一个变量对象,所以在每个函数内部 i 的值都是 10。
不过,我们可以通过创建另一个匿名函数强制让闭包的行为符合预期:
function createFunctions() {
var result = [];
for (var i = 0; i < 10; i ++) {
result[i] = function(num) {
return function() {
return num;
};
}(i);
}
return result;
}
在这个版本中,我们没有直接把闭包赋值给数组,而是定义了一个匿名函数,并将立即执行该匿名函数的结果赋给数组。
这里的匿名函数有一个参数 num,也就是最终要返回的值。
在调用每个匿名函数时,我们传入了变量 i,并会将变量 i 的当前值复制给参数 num,而在这个匿名函数内部,又创建并返回了一个访问 num 的闭包。
所以 result 数组中的每个函数都有自己 num 变量的一个副本,因此就可以返回各自不同的数值了。
另外,我们现在可以用 ES6 中的 let 命令实现上述效果:
function createFunctions() {
var result = [];
for (let i = 0; i < 10; i ++) {
result[i] = function() {
return i;
};
}
return result;
}
Tips: 因为 let 声明的变量只在所在的块级作用域有效,所以每一次循环的变量 i 都是一个新的变量。
闭包中的 this 对象
我们知道,this 对象是在运行时基于函数的执行环境绑定的:在全局函数中,this 等于 window;而当函数被作为某个对象的方法调用时,this 等于那个对象。
不过, 匿名函数的执行环境具有全局性, 因此其 this 对象通常指向 window (在通过 call() 或 apply() 改变函数执行环境的情况下,this 就会指向其它对象)。
但有时由于编写闭包的方式不同,这一点可能不会那么明显:
var name = 'The Window';
var object = {
name: 'My Object',
getName: function() {
return function() {
return this.name;
};
}
};
alert(object.getName()()); // 'The Window', ( 在非严格模式下 )
- 由于 getName() 会返回一个函数,所以调用 object.getName()() 就会立即调用它返回的函数。
不过,把外部作用域中的 this 对象保存在一个闭包能够访问到的变量里,就可以让闭包访问该对象了:
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 也仍然引用着 object,所以调用 object.getName()() 就返回了 'My Object'。
JavaScript 中的函数表达式和闭包都是极其有用的特性,利用它们可以实现很多功能。
不过,因为创建闭包必须维护额外的作用域,过度使用它们可能会占用大量内存,所以不要为了闭包而闭包~
关于函数和闭包的浅薄知识就先讲到这里,如有不正确的地方,欢迎各位指正。【比心】