js 函数声明与函数表达式、递归与闭包

目录

一、函数表达式 和 函数声明 的区别

(1)、函数声明

(2)、函数表达式

二、递归

三、闭包

1、函数被调用时都发生了什么?

2、闭包

(1)、函数作为返回值

(2)、每次循环都会创建一个新函数,即使传入相同的参数

(3)、闭包只能取得包含函数中任何变量的最后一个值

(4)、如果一定要引用循环变量怎么办?

(5)、闭包与 this 对象

(6)、闭包与内存泄漏

3、闭包与匿名函数


一、函数表达式 和 函数声明 的区别

  • 函数的创建有两种方式:函数声明 和 函数表达式。
  • 函数声明具有函数声明提升,函数表达式不具有函数声明提升。 

(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、闭包”的内容,恕不赘述。

【推荐阅读】函数式编程与面向对象编程icon-default.png?t=M276https://blog.csdn.net/mChales_Liu/article/details/106530145

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值