目录
一、函数表达式 和 函数声明 的区别
- 函数的创建有两种方式:函数声明 和 函数表达式。
- 函数声明具有函数声明提升,函数表达式不具有函数声明提升。
(1)、函数声明
函数声明有一个重要的特点就是 函数声明提升——在执行代码之前会先读取函数声明,这就意味着可以把函数声明放在调用它的语句之后。
fn(); // hello
function fn(){
alert("hello");
}
(2)、函数表达式
函数表达式就是常规的变量赋值——创建一个函数(一般为匿名函数,当然也有命名函数),并将它赋值给一个变量,使用前必须先赋值,不存在函数声明提升。
// sayHello(); // error
var sayHello = function(){
alert("hello");
};
sayHello(); // hello
二、递归
递归函数,是在一个函数内部,通过名字调用自身的情况下形成的。
举个栗子:这是一个经典的递归实现阶乘的函数。
function factorial(num){
if(num <= 1){
return 1;
} else {
return num * factorial(num - 1);
}
}
递归有时会面临一个问题:如上代码,在函数有名字,而且名字以后也不会变得情况下,这样定义没有问题。但问题是这个函数的执行与函数名 factorial 仅仅耦合在了一起。为了消除这种紧密的耦合现象,就可以用 argument.callee 来指向正在执行的函数的指针。这样,无论引用函数时使用的是什么名字,都可以保证正常完成递归调用。
function factorial (num) {
if (num <= 1) {
return 1;
} else {
return num * arguments.callee (num - 1);
}
}
在严格模式下,不能访问 arguments.callee,否则报错。不过,可以使用 命名函数表达式 来达到相同的结果。
var factorial = (function f(num){
if(num <= 1){
return 1;
} else {
return num * factorial(num - 1);
}
});
上述代码,创建了一个名为 f() 的命名函数表达式,然后将她赋值给变量 factorial。即便把函数赋值给了另一个变量,函数的名字 f 仍然有效,所以递归调用照样能正确完成。
三、闭包
1、函数被调用时都发生了什么?
当某个函数被调用时,会创建一个执行环境及其相应的作用域链。然后,使用 arguments 和其他命名参数的值来初始化函数的活动对象。但在作用域链中,外部函数的活动对象始终处于第二位,外部函数的外部函数的活动对象始终处于第三位,……直至作为作用域链终点的全局执行环境。在函数执行的过程中,为了读取和写入变量的值,就需要在作用域链中查找变量。
后台的每个执行环境都有一个表示变量的对象——变量对象。全局环境的变量对象始终存在,而像函数中的局部环境的变量对象,则只能在函数执行的过程中存在。
function compare(value1, value2){
if(value1 < value2){
return -1;
} else if(value1 > value2){
return 1;
} else {
return 0;
}
}
var result = compare(5, 10);
一般来讲,当函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局执行环境的变量对象。但是,闭包的情况例外。例如:
function createComparisionFunction(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 compareNames = createComparisionFunction("name");
var result = compare({ name: "marry" }, { name: "lily" });
上述代码,在匿名函数从 createComparisionFunction() 函数中被返回后,它的作用域链被初始化为包含 createComparisionFunction() 函数的活动对象 和 全局变量对象。这样,匿名函数就能访问在 createComparisionFunction() 函数中定义的所有变量了。
2、闭包
- 闭包是指有权访问另一个函数作用域中的变量的函数。
- 创建闭包的常见方式,就是在一个函数内部创建一个匿名函数。
- 建议只在必要的时候使用闭包,因为闭包存在内存泄漏的问题,比其他函数更占内存。
对于什么是闭包,更直白的理解是:A函数中嵌套着B函数,B程序中有用到A的变量,当外部函数C调用函数A时,虽然A已经执行完毕,理论上函数执行完毕,它就要被弹出栈,但是由于B要用到A,所以A的变量被保存到内存中不被销毁,我们称函数B是闭包。
(1)、函数作为返回值
function sum(arr) {
return arr.reduce(function (x, y) {
return x + y;
});
}
sum([1, 2, 3, 4, 5]); // 15
如果不需要立刻求和,而是在后面的代码中,根据需要再计算怎么办?可以不返回求和的结果,而是返回求和的函数。
function lazy_sum(arr) {
var sum = function () {
return arr.reduce(function (x, y) {
return x + y;
});
}
return sum;
}
var f = lazy_sum([1, 2, 3, 4, 5]);
f(); // 15
当我们调用 lazy_sum() 时,返回的并不是求和结果,而是求和函数 f()。只有调用函数 f() 时,才真正计算求和的结果。
闭包可以实现:返回一个函数并延迟执行。
(2)、每次循环都会创建一个新函数,即使传入相同的参数
function lazy_sum(arr) {
var sum = function () {
return arr.reduce(function (x, y) {
return x + y;
});
}
return sum;
}
var f1 = lazy_sum([1, 2, 3, 4, 5]);
var f2 = lazy_sum([1, 2, 3, 4, 5]);
console.log(f1 === f2); // false
(3)、闭包只能取得包含函数中任何变量的最后一个值
function count() {
var arr = [];
for (var i=1; i<=3; i++) {
arr.push(function () {
return i * i;
});
}
return arr;
}
var results = count();
var f1 = results[0];
var f2 = results[1];
var f3 = results[2];
f1(); // 16
f2(); // 16
f3(); // 16
在上述代码中,把创建的3个函数都添加到一个 Array 中返回了。你可能认为当我们调用 f1(),f2() 和 f3() 结果应该是 1,4,9,但实际结果是:全部都是 16。原因就在于返回的函数引用了变量 i,但它并非立刻执行。等到 3 个函数都返回时,它们所引用的变量 i 已经变成了 4,因此最终结果为 16。
返回闭包时牢记的一点就是:返回函数尽量不要引用任何循环变量,或者后续会发生变化的变量。
(4)、如果一定要引用循环变量怎么办?
方法是再创建一个函数,用该函数的参数绑定循环变量当前的值,无论该循环变量后续如何更改,已绑定到函数参数的值不变。
function count() {
var arr = [];
for (var i=1; i<=3; i++) {
arr.push((function (n) {
return function () {
return n * n;
}
})(i));
}
return arr;
}
var results = count();
var f1 = results[0];
var f2 = results[1];
var f3 = results[2];
f1(); // 1
f2(); // 4
f3(); // 9
注意这里用了一个“创建一个匿名函数并立刻执行”的语法:
(function (x) {
return x * x;
})(3); // 9
(5)、闭包与 this 对象
this 对象是基于函数的执行环境绑定的:
- 在全局函数中,this 指向 window 对象;
- 当函数被作为某个对象的方法调用时,this 指向这个调用对象;
- 通过 call()、apply() 改变函数执行环境的情况下,就会改变 this 指向(详见:https://blog.csdn.net/mChales_Liu/article/details/102497060);
- 匿名函数的执行环境通常具有全局性,因此 this 在非严格模式下默认指向 window 对象;
- 闭包可以通过将其外部函数作用域中的 this 保存在一个闭包能访问到的变量里,就可以间接的让闭包访问 this 对象了。
匿名函数的执行环境通常具有全局性,因此 this 在非严格模式下默认指向 window 对象:
var name = "The Window";
var object = {
name: "My Object",
getNameFunction: function(){
return function(){
return this.name;
};
}
};
var f1 = object.getNameFunction();
console.log(f1()); // The Window
那么在闭包里如何访问其外部函数的 this 对象呢?闭包可以通过将其外部函数作用域中的 this 保存在一个闭包能访问到的变量里,就可以间接的让闭包访问 this 对象了。
var name = "The Window";
var object = {
name: "My Object",
getNameFunction: function(){
var that = this;
return function(){
return that.name;
};
}
};
var f1 = object.getNameFunction();
console.log(f1()); // My Object
闭包中 this 对象存在这个问题,arguments 对象也同样存在此问题。通过用与上述代码中类似的方法就能解决。
(6)、闭包与内存泄漏
- 闭包存在内存泄漏的问题。
- 如何解决闭包带来的内存泄漏问题?
- 需要手动将匿名函数设置为 null 来销毁匿名函数,释放内存。
例如:
function createFunction(name){
return function(){
return name;
};
}
var f1 = createFunction("marry");
f1 = null; // 解除对匿名函数的引用(以便释放内存)
上述代码,在 createFunction() 函数执行完毕后,其执行环境的作用域链会被销毁,但它的活动对象仍会留在内存中,直到匿名函数被手动销毁后,createFunction() 函数的活动对象才会被销毁。这里的手动销毁指的是,将该匿名函数设置为 null。
3、闭包与匿名函数
闭包和匿名函数的区别是:
- 匿名函数是没有名字的函数。
- 闭包不但没有函数名,而且还具有一个特性:在一个函数执行完毕后,还在使用该函数的变量。
常见的匿名函数:
var fn = function(){ };
(function (x,y){ })(2, 1);
function() { };
闭包——请看本文“2、闭包”的内容,恕不赘述。
【推荐阅读】函数式编程与面向对象编程https://blog.csdn.net/mChales_Liu/article/details/106530145