定义函数javascript
在 JS 中定义函数的方式有两种:函数声明
和 函数表达式
。java
函数声明
的语法为:数组
function functionName(arg0, arg1, arg2) {
// 函数体;
}
复制代码
函数声明的一个重要特征就是 函数声明提高 ,即在执行代码以前会先读取函数声明,这意味着能够把函数声明放在调用它的语句后面:浏览器
sayHi(); // 'hi';
sayHi() {
alert('hi');
}
复制代码
第二种建立函数的形式是 函数表达式
。函数表达式有建立几种不一样的语法形式,下面是最多见的一种:闭包
var functionName = function(arg0, arg1, arg2) {
// 函数体;
}
复制代码
这看来好像是常规的变量复制语句,即建立一个函数并将它赋值给变量 functionName,在这种状况下建立的函数叫作 匿名函数 ( 也称为 Lambda 拉姆达函数 ),在使用前必须先赋值。以下面的代码会报错:app
sayHi(); // 错误,函数还不存在;
var sayHi = function(){
alert('hi');
}
复制代码
理解函数提高的关键,就是理解函数声明与函数表达式之间的区别。例如,执行如下代码的结果可能会让人意想不到:函数
if(condition) {
function sayHi() {
alert('hi');
}
} else {
function sayHi() {
alert('Yo~');
}
}
复制代码
表面上看,上述代码会在 condition 为 true 时使用一个 sayHi() 的定义,不然就使用另外一个定义。实际上,这在 ECMAScript 中属于无效语法,JavaScript 引擎会尝试修正错误,将其转换为合理的状态,大多数浏览器会返回第二个声明,忽略 condition。ui
不过,若是是使用函数表达式,那就没什么问题:this
var sayHi;
if(condition) {
sayHi = function() {
alert('hi');
}
} else {
sayHi = function() {
alert('Yo~');
}
}
复制代码
这样不一样的函数将根据不一样的 condition 被赋值给 sayHi。spa
递归
递归函数是在一个函数经过名字调用自身的状况下构成的:
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
解决问题。
所以能够用它来实现对函数的递归调用:
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', ( 在非严格模式下 )
复制代码
不过,把外部做用域中的 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 中的函数表达式和闭包都是极其有用的特性,利用它们能够实现不少功能。
不过,由于建立闭包必须维护额外的做用域,过分使用它们可能会占用大量内存,因此不要为了闭包而闭包~
关于函数和闭包的浅薄知识就先讲到这里,若有不正确的地方,欢迎各位指正。【比心】