JavaScript中的匿名函数与闭包

Javascript中,闭包无疑是重要的一个环节,入门时多半会被弄晕。

下面先从一个例子入手:

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]);
alert(f1 === f2); // false

本以为,f1和f2完全是两个完全一样的函数,但是实际上lazy_sum返回给f1和f2的两个匿名函数(参考本文第三部分)并不一样,即使看到“字面的表达式”是一样的,其实这涉及到更关键的一点是对于Javascript的“对象”的理解,这一点或许将来值得单独拿出来写篇文章。

一、闭包的定义
以上的lazy_sum中又定义了函数sum,并且,内部函数sum可以引用外部函数lazy_sum的参数和局部变量,当lazy_sum返回函数sum时,相关参数和变量都保存在返回的函数中,这种程序结构就被称为“闭包(Closure)”。(廖雪峰的定义)

在Javascript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成"定义在一个函数内部的函数"。所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。(阮一峰的的定义)

二、变量的作用域
要理解闭包,首先必须要明确的是Javascript特殊的变量作用域,也即全局变量和局部变量,这一点不难,可以简单地按照是否使用var声明变量来加以区分。

与变量的作用域相关的还有一个this方法,以及that指向。下面两段代码,在看完本文后不妨回来思考下如何解释两段代码的结果。

第一段代码:

var name = "The Window";
var object = {
    name : "My Object",
    getNameFunc : function(){
        return function(){
            return this.name;
        };
    }
};
alert(object.getNameFunc()());

第二段代码:

var name = "The Window";
var object = {
    name : "My Object",
    getNameFunc : function(){
        var that = this;
        return function(){
            return that.name;
        };
    }
};
alert(object.getNameFunc()());

三、匿名函数
另一个需要明确的,就是匿名函数,举几个栗子:

//普通函数 
function box() {               //函数名是 box 
       return'Lee'; 
}
 
 
//匿名函数
 function(){               //简单的匿名函数,单独使用会报错
     return'Lee'; 
}
 
 
//通过表达式自我执行
 (functionbox() {            //封装成表达式,单独使用会报错
        alert('Lee'); 
})();                  //()表示执行函数,并且传参
 
 
//把匿名函数赋值给变量 
var box=function(){            //将匿名函数赋给变量 
       return'Lee'; 
};
 alert(box());            //调用方式和函数调用相似
 
 
//函数里的匿名函数
 function box() { 
       return function(){                  //函数里的匿名函数,产生闭包 
            return'Lee';
    } 
   } 
alert(box()());               //调用匿名函数

四、闭包与内存

接下来就是闭包最让人疑惑的地方。

闭包最大用处有两个,一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。

如何理解呢?从一个简单计数器入手:

function getCounter(){
    var counter = 1;
 
    var add = function() {
        counter++;
    }
 
    var desc = function() {
        counter--;
    }
 
    var get = function() {
        return counter;
    }
 
    return {
        add: add,
        desc: desc,
        get: get
    }
};
 
var counter1 = getCounter();
var counter2 = getCounter();
 
console.log(counter1 === counter2)    //false
 
console.log(counter1.get())    //1
console.log(counter2.get())    //1
 
counter1.add()
counter1.add()
counter1.add()
 
counter2.desc()
counter2.desc()
 
console.log(counter1.get())    //4
console.log(counter2.get())    //-1

counter1 = null;
counter2 = null;

以上的例子可以看到,闭包内的函数add、desc可以反复调用,而父函数getCounter里的变量值则是在不断累加的。也就是说函数counter1及counter2中定义的父函数getCounter中的,由变量及子函数等一系要素构成该父函数的“运行环境”,一直停留在内存中,因而出现了累加的效果。

在JS的垃圾回收机制(garbage collection)中,如果一个变量发生过一次及以上的引用,那么这个变量就将停留在内存中,如以上代码,console.log(counter1.get())即是一次对counter1引用,所以这个getCounter()将会作为counter1的值一直停留在内存中,直到重新给counter1赋值,例如counter1 = null;就是一次清理内存的操作。在目前的小型脚本中,是不太会显示出内存占用问题的,而且现在新型的浏览器都有一定的内存控制机制,但是在古董级的IE浏览器中,可能就会面对严重的内存泄漏问题。所以及时清理无用内存是一个很好的习惯。

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];
 
console.log(f1()); // 16
console.log(f2()); // 16
console.log(f3()); // 16

五、正确地使用闭包

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];
 
console.log(f1()); // 16
console.log(f2()); // 16
console.log(f3()); // 16

这样一个程序中,我们期望得到的是[1,4,9],但是最终得到的却是[16,16,16]。为什么呢?

因为内存停留的原因啊,f1、f2、f3每一次赋值,返回的函数都引用了i,但它并非立刻执行。等到3个函数都返回时,它们所引用的变量i已经变成了4,因此最终结果为16。

所以返回闭包时牢记的一点就是:返回函数不要引用任何循环变量,或者后续会发生变化的变量。

来源(Source):樱花庄的白猫

匿名函数,也称为拉姆达函数,是一种使用JavaScript函数的强大方式。以下总结了匿名函数的特点: 任何函数表达式从技术上说都是匿名函数,因为没有引用它们的确定的方式; 在无法确定如何引用函数的情况下,递归函数就会变得比较复杂; 递归函数应该始终使用arguments.callee来递归地调用自身,不要使用函数名--函数名可能会发生变化。 当在函数内部定义了其他函数时,就创建了闭包闭包有权访问包含函数内部的所有变量,原理如下: 在后台执行环境闭包的作用域链包含着它自己的作用域、包含函数的作用域和全局作用域; 通常,函数的作用域及其所有变量都会在函数执行结束后被销毁; 但是,当函数返回了一个闭包时,这个函数的作用域将会一直在内存保存到闭包不存在为止;使用闭包可以在JavaScript模仿块级作用域(JavaScript本身没有块级作用域的概念),要点如下: 创建并立即调用一个函数,这样既可以执行其的代码,又不会在内存留下对该函数的引用; 结果就是函数内部的所有变量都会被立即销毁--除非将某些变量赋值给了包含作用域(即外部作用域)的变量。 闭包还可以用于在对象创建私有变量,相关概念和要点如下: 即使JavaScript没有正式的私有对象属性的概念,但可以使用闭包来实现公有方法,而通过公有方法可以访问在包含作用域定义的变量; 有权访问私有变量的公有方法叫做特权方法; 可以使用构造函数模式、原型模式来实现自定义类型的特权方法,也可以使用模块模式、增强的模块模式来实现单例的特权方法。 JavaScript匿名函数闭包都是非常有用的特性,利用它们可以实现很多功能。不过,因为创建闭包必须维护额外的作用域,所以过度使用它们可能会占用大量内存。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值