闭包再学习

之前写过博客,也看了无数博客,去学习闭包,理解闭包。后来,得出的结论是,闭包是一个函数,它能访问另一个函数定义的变量和方法。 前几天面试网易,面试官的一个反问让我有点懵,他说只是可以访问另一个函数的变量和方法吗?什么函数都可以?回来后,觉着我对于作用域,执行环境的理解太片面,那么对于闭包的理解就更不用说有多片面了。今天又看了很多博客,重新刷了《高级程序设计》,包括维基百科和MDN的英文原版都学习了一下,感觉到自己之前的理解是有问题的,先把新的理解记录一下。

1、几个重要概念

这里写图片描述

要想理解闭包,必须首先弄清楚执行环境、作用域链是什么。看了一下午,我个人理解,他们之间的关系可以画这么一个图。

  • 首先,每个函数都会有它的执行环境。所有 JavaScript 代码都是在一个执行环境中被执行的。当执行流进入到一个函数时,函数的环境就会被推入一个环境栈中。在函数执行完后,栈将其环境弹出。执行环境定义了变量或函数有权访问的其他数据,决定了他们各自的行为
  • 每个执行环境都有一个与之关联的对象变量。在这个对象中,保存了环境中定义的所有变量和函数但是对象变量无法依靠代码来访问
  • 当代码在一个环境中执行时,会创建变量对象的一个作用域链。作用域链的最前端始终是当前执行代码所在环境的变量对象。再下一个变量对象则来自包含环境,这样一直延续到全局执行环境。全局执行环境的变量对象始终是作用域链中的最后一个对象。

总结一下: 引自这篇博文

在一个页面中,第一次载入JS代码时创建一个全局执行环境,当调用一个 JavaScript函数时,该函数就会进入相应的执行环境。如果又调用了另外一个函数(或者递归地调用同一个函数),则又会创建一个新的执行环境,并且在函数调用期间执行过程都处于该环境中。当调用的函数返回后,执行过程会返回原始执行环境。因而,运行中的JavaScript 代码就构成了一个执行环境栈。
在访问一个变量的时候,搜索过程从作用域链前端开始,一级一级向后回溯,知道找到变量标识符,否则会导致发生错误。

例子,高级程序设计中的例子:

var color = "blue";
function changeColor () {
    var anotherColor = "red";
    function swapColors () {
        var tempColor = anotherColor;
        anotherColor = color;
        color = tempColor;      
    }
    swapColors();
}
changeColor();

这里写图片描述

上图展示了前面代码的作用域链,内层可以访问外层,而外层不可以访问内层。

有面试官问:执行环境和作用域有什么区别?
之前真的不太会答,如果再被问到就可以说出一些自己的理解了:

每个函数都会有它的执行环境,当执行到一个函数时,就会将其环境推入执行环境栈中,函数执行完后,再将其弹出,返回到之前的执行环境。而每个执行环境又有它自己的变量对象,里面保存着其中定义的变量和函数。当代码在一个环境中执行时,会形成一个变量对象的作用域链,当前环境的变量对象在最前端,其外部函数执行环境的变量对象在下一级,一直回溯到全局环境的变量对象。作用域链是保证了对执行环境有权访问的所有变量和函数的有序访问。当访问一个变量的时候,会从作用域链的最前端开始搜索,逐级向上。

比如:

    var num = 1;
    function showNum () {
        var num = 0;
        return function () {
            console.log(num);
        }
    }

    var f = showNum();
    f();//0

解析:
最后打印出来的是0,showNum返回了一个匿名函数,并将其赋给了f,f()执行时,console.log(num),这时候就要从作用域链的前端开始搜索num,一级一级搜索的时候,先搜索到的是showNum函数执行环境变量对象中的num,所以搜索就停止了,因此,打印出的是0;如果console.log(this.num)那么情况就变了,因为f()在全局环境中执行,this指向window,所以会打印出1。

参考博文:

  1. 理解Javascript_12_执行模型浅析:http://www.cnblogs.com/fool/archive/2010/10/16/1853326.html
  2. 《JavaScript高级程序设计第四章》

2、闭包

先贴我搜索到的几个关于闭包的解释,我不进行翻译了,总觉着翻译出来变味儿:

  • A closure is a special kind of object that combines two things: a function and the environmentin which that function was created;
  • In JavaScript, if you use the function keyword inside another function, you are creating a closure;
  • In JavaScript, if you declare a function within another function, then the local variables can remain accessible after returning from the function you called;
  • The magic is that in JavaScript a function reference also has a secret reference to the closure it was created in.
  • 闭包是词法闭包的简称,是引用了自由变量的函数,这个被引用的变量将和这个函数一起存在,即使已经离开了创造它的环境也不例外;
  • 闭包不是私有啊,闭的意思不是“封闭内部状态”,而是封闭外部状态,一个函数如何能够封闭外部状态呢,当外部状态的scope失效的时候,还有一份留在内部状态里面–@轮子哥

参考博文:
1. MDN:Closures https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures
2. JavaScript Closures for Dummies http://stackoverflow.com/questions/111102/how-do-javascript-closures-work
3. 维基百科: https://en.wikipedia.org/wiki/Closure_(computer_programming)
4. 到底什么是闭包? https://www.zhihu.com/question/34210214
5. 闭包的概念形式与应用 http://www.ibm.com/developerworks/cn/linux/l-cn-closure/index.html
6. JavaScript 里的闭包是什么?应用场景有哪些?http://www.zhihu.com/question/19554716

好,开始说我的理解,坦白讲,我自己感觉我现在也并没有对闭包理解的很透彻,还是有什么地方没捋出来,但又不知道自己是卡在哪里……先记录下看了一天之后,自己理解的部分。
前面第一部分讲了作用域链和执行环境,一般来讲,当函数执行完毕后,其执行环境就会从执行环境栈弹出,变量对象也会被销毁,内存中仅保存全局作用域。但是,闭包不是这样子的:
看个例子:

    var name = "The window";//全局变量
    function myObject () {
        var name = "my Object";
        return function () {
            return name;
        }
    }
    var result = myObject();//result就是那个匿名函数
    result();//"my Object"//访问到了myObject()的局部变量name

这里写图片描述

尝试着画一下作用域链会感觉好很多。当匿名函数从myObject()中被返回后,它的作用域链被初始化为包含其自身变量对象、myObject()变量对象和全局变量对象。这样,匿名函数就可以访问myObject()和全局环境中定义的变量。**最重要的是,**myObject()函数执行完毕后,其变量对象也不会被销毁,因为匿名函数的作用域链仍然引用着这个活动对象。也就是说,虽然myObject()函数return之后,它执行环境弹出了执行环境栈,作用域链被销毁,但是它的变量对象仍然存在内存中,直到匿名函数被销毁。

所以写到这里,我好像又明白了一些:

所谓闭包就是,当你在函数的内部又声明了另一个函数,内部函数的作用域链中就会包含外部函数的变量对象,所以内部函数就有权访问外部函数定义的变量,而当外部函数执行完后,其变量对象仍存在于内存之中,这样就像是封闭了外部状态。

再来看几个例子好了,以下例子全部来源于stackoverflow

(1)Example1

这个例子说明了局部变量没有被复制,它们是通过引用来保持的。这就好像是在函数执行完毕后,仍然有内存记忆一样。

        function showNum1(){
        // Local variable that ends up within closure           
            var num = 42;
            var show = functio() {
                console.log(num);
            }
            num ++;
            return show;
        }
        var showNum = showNum1();
        showNum(); // 43
(2)Example2

这个例子就有意思了,如果我不自己试一下,我真的说不对会打印出什么。

/*-----Example2-----*/
    var gLogNumber, gIncreaseNumber, gSetNumber;
    function setupSomeGlobals() {
        // Local variable that ends up within closure
        var num = 42;
        // Store some references to functions as global variables
        gLogNumber = function() { 
            console.log(num); 
        }
        gIncreaseNumber = function() { 
            num++; 
        }
        gSetNumber = function(x) { 
            num = x; 
        }
    }

    setupSomeGlobals();
    gIncreaseNumber();
    gLogNumber(); // 43
    gSetNumber(5);
    gLogNumber(); // 5

    var oldLog = gLogNumber;
    setupSomeGlobals();
    gLogNumber(); // 42
    oldLog() // 5

结果是不是很有意思。解释一下:
这三个函数gLogNumber, gIncreaseNumber, gSetNumber 共享访问同一个闭包,即setupSomeGlobals()的局部变量。
特别注意:在上面的例子中,当第二次调用setupSomeGlobals()时,一个新的闭包(stack-frame堆栈框架)就被创建了。原来的 gLogNumber, gIncreaseNumber, gSetNumber被新的匿名函数给重写。在JavaScript中,只要你在一个函数内部声明了另一个函数,只要外部函数被调用,那么内部函数就会被重新创建。

(3)Example3

这个例子类似于《高级程序设计》中for循环的那个例子,所以,在循环中使用闭包,要格外小心,因为它常常出现让我们意想不到的结果,要记住,闭包保存的是整个变量对象:

/*-----Example3-----*/
    function buildList(list) {
        var result = [];
        len = list.length;
        for (var i = 0; i < len; i++) {
            var item = 'item' + i;
            result.push( function() {
                console.log(item + ' ' + list[i])
            } );
        }
        return result;
    }

    function testList() {
        var fnList = buildList([1,2,3]);
        var lenF = fnList.length; 
        // Using j only to help prevent confusion -- could use i.
        for (var j = 0; j < lenF; j++) {
            fnList[j]();
        }
    }
    testList();//"item2 undefined" 3 times

最终打印出来了3次 item2 undefined。这是因为result.push( function() {console.log(item + ’ ’ + list[i])} 向result数组中添加了3次匿名函数的引用。由于闭包保存的是整个变量对象,所以,这三次引用的i都是同一个变量。当匿名函数被 fnListj调用时,他们使用的是同一个闭包,i值变成了3,因为循环结束后,i变成了数组的长度3。console.log(item + ’ ’ + list[i]) 这行代码中,需要用到变量i,所以就从匿名函数的作用域链中开始找,这时候i已经变成3了,所以list[i]=list[3], 是undefined,所以会返回3次 item2 undefined。注意,因为下标是从0开始,所以item 值变成了item2;而i++ 使得i的值增加到了3.
当然,这个例子跟181页红宝书的例子是类似的。本意都是 console出来 1,2,3.改进方法就是,push的时候加一个匿名函数,让匿名函数,将立即执行的匿名函数的结果返回给result。代码如下:

function buildList(list) {
        var result = [];
        len = list.length;
        for (var i = 0; i < len; i++) {

            result.push( 
                (function (num){
                return function() {
                    var item = 'item' + (num+1);
                    console.log(item + '-' + list[num])
                }})(i)
            );
        }
        return result;
    }

结果:
这里写图片描述

好了,写到这里,想继续了解的话,请参考 参考博文再去研究吧~~

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值