美文网首页
作用域链与闭包(二)

作用域链与闭包(二)

作者: DHFE | 来源:发表于2018-04-04 23:08 被阅读3次

    作用域链的这种配置机制引出了一个值得注意的副作用,即闭包只能取得包含书函数中任何变量的最后一个值。别忘了闭包所保存的是真个变量对象,而不是某个特殊的变量。

    栗子

    function createFunctions() {
    
        var result = [];
    
        for (var i=0;i<10;i++) {
            result[i] = function() {
                console.log(i);
            }
        }
    
        return result; 
    }
    
    var test = createFunctions();
    
    test[1]();      // 10
    test[9]();      // 10
    
    

    这是一个闭包吗?
    回想之前的概念:闭包是一个函数,这个函数可以访问另一个函数的作用域。
    对的,它是闭包,每一个新建的匿名函数赋值到数组内,并返回数组。

    这里我们要清除的几个点,哪些是createFunctions()的变量,哪些是匿名函数的,有何关系。

    createFunctions

    • 变量:i
    • 数组:result

    匿名函数

    • 无自有变量。

    现在,第一个问题,匿名函数里的i是从哪里来的?是从createFunctions()函数的变量对象来的。那么,在for循环中,每一个新建的匿名函数里的i都来自外部函数的变量对象里的i,那么i的值为什么都是10?

    因为,保存的是变量对象的值!而不是变量对象的副本,抽象的说,所有人都公用这个i,而变量对象的值会随着for循环而变化。

    解决方法很简单,我们在数组匿名函数和createFunctions()函数之间加一个作用域,使得匿名函数的值不直接来自createFunctions()函数不就行了吗。

    function createFunctions() {
    
        var result = [];
        var divide = function(i) {
            return function() {
                console.log(i);
            }
        }
    
        for (var i=0;i<10;i++) {
            result[i] = divide(i);
        }
    
        return result; 
    }
    
    var test = createFunctions();
    
    test[0]();      // 0
    test[9]();      // 9
    

    解析

    同样的,我们还可以使用立即执行函数,只要是函数,就创建了一个新的作用域,隔绝了闭包函数与createFunction()函数的作用域链,使得每次的递增的变量 i 先放在新函数(新作用域)内,而且是不会在变化的。

    function createFunctions() {
    
        var result = [];
    
        for (var i = 0; i < 10; i++) {
            result[i] = function(num) {
    
                return function() {
                    console.log(num);
                }
                
            }(i);
        }
    
        return result;
    }
    
    var test = createFunctions();
    test[5]();      // 5
    

    在这个版本中,我们没有把闭包赋值给数组,而是定义了一个匿名函数,并将立即执行该匿名函数的结果赋给数组。这里的匿名函数有一个参数num,也就是最终的函数要返回的值。在调用每个匿名函数时,我们传入了变量i,。由于函数参数是按值传递的,所以就会将变量i的当前值复制给参数num。而在这个匿名函数内部,又创建了一个访问num的闭包。这样一来,test数组中的每个函数都有自己num变量的一个副本,因此就可以返回各自不同的值了。

    另外一个变形:

    function createFunctions() {
    
        var result = [];
        
        var fn1 = function(num) {
            return fn2;
        }
        var fn2 = function() {
            console.log(num);
        }
        for (var i = 0; i < 10; i++) {
            result[i] = fn1(i);
        }
    
        return result;
    }
    
    var test = createFunctions();
    test[3]();      // num is not defined   
    

    还行吗?
    当然不行,fn1函数内,i是传进入给num了,但是fn2怎么访问num?fn2函数并不在fn1作用域内呀。


    好的,我们来看一个经典面试题,来自Excuse me?这个前端面试在搞事!
    下列源代码将会输出什么?

    1.

    for (var i = 0; i < 5; i++) {
      console.log(i);
    }
    

    这没什么好说的,循环打印出0~4,最后i的值为5。

    2.

    for (var i = 0; i < 5; i++) {
      setTimeout(function() {
        console.log(i);
      }, 1000 * i);
    }
    

    看代码后,首先明白,JS是单线程的,而setTimeout()这个方法在JS中并不是同步执行。



    setTimeout(function() {
        console.log("1");
    });
    console.log("2");
    
    // "2"
    // "1"
    
    for (var i = 0; i < 5; i++) {
        setTimeout(function () {
            console.log(i);
        }, 1000 * i);
    }
    

    这道题对于新手是会有一点违背直觉的感觉,我们一步一步来分析。首先setTimeout里的匿名函数并不是立即执行函数,这个匿名函数的执行和延迟时间有关,也就是说,它不是同步的,不信?我们加上立即执行函数试试。

    for (var i = 0; i < 5; i++) {
        setTimeout( !function () {
            console.log(i);
        }(i), 1000*i);
    }
    /*
    0
    1
    2
    3
    4
    */
    

    运行后,立即弹出01234,延迟执行也不会生效,因为匿名函数定义后就立即执行了。(这里反而是同步的了)

    既然原代码中的匿名函数不是同步的,那么谁是同步的?当然是for循环里的i,它在5个匿名函数执行前就会计算完毕,最后i的值变为5。
    对于1000*i,当i为0时,整个表达式也为0,但是,这个0不代表马上就执行函数的意思。

    for (var i = 0; i < 5; i++) {
        setTimeout( function () {
            console.log(i);
        }, 1000*i);
    }
    console.log("test");
    //test
    //5
    //5
    //5
    //5
    //5
    

    还是要先将同步的代码执行完毕后,才会执行函数,而又i是主线程的,早已递增为5,所以弹出5。

    • i :0,匿名函数0秒后执行。
    • i : 1,匿名函数1秒后执行。
    • i :2,匿名函数2秒后执行。
    • i :3,匿名函数3秒后执行。
    • ...... , 匿名函数4秒后执行。
    • ...... , 匿名函数5秒后执行。

    那么总结下来,就是每隔一秒,执行一个函数,函数打印出5。(可以抽象一下,在每一次for循环后,就有一个待执行函数推入一个队列)


    3.刚才的代码,怎么改才能输出 0 到 4 ?

    for (var i = 0; i < 5; i++) {
      setTimeout(function() {
        console.log(i);
      }, 1000 * i);
    }
    

    闭包呗~

    for (var i = 0; i < 5; i++) {
        setTimeout((function (num) {
            return function() {
                console.log(num);
            }
        })(i), 1000*i);
    }
    

    每隔一秒输出0~4。


    4.

    这里我使用原作者的代码,和上面无关。

    for (var i = 0; i < 5; i++) {
        (function (i) {
            setTimeout(function () {
                console.log(i);
            }, i * 1000);
        })(i);
    }
    // 这是作者给出的第三题解决方案
    

    问题:删掉这个 i 会发生什么?

    for (var i = 0; i < 5; i++) {
        (function () {
            setTimeout(function () {
                console.log(i);
            }, i * 1000);
        })(i);
    }
    

    分析一下,当形参i被删掉后,内部的闭包函数console的i将会来自哪?当然是全局作用域中的i了,所以情况就和第二题是一样的。

    • i为0时,将第一个闭包函数推入队列待执行(延迟时间:0s),i来自全局作用域。
    • i为1时,将第二个闭包函数推入队列待执行(延迟时间:1s),i来自全局作用域。
    • i为2时,将第三个闭包函数推入队列待执行(延迟时间:2s),i来自全局作用域。
    • i为3时,将第四个闭包函数推入队列待执行(延迟时间:3s),i来自全局作用域。
    • i为4时,将第五个闭包函数推入队列待执行(延迟时间:4s),i来自全局作用域。
      主线程执行完毕后按推入顺序和延迟时间来执行队列内任务。

    每隔一秒,打印一个5。


    那如果是我的代码,删掉i会发生什么?

    for (var i = 0; i < 5; i++) {
        setTimeout((function (num) {
            return function() {
                console.log(num);
            }
        })(i), 1000*i);
    }
    

    变为

    for (var i = 0; i < 5; i++) {
        setTimeout((function (num) {
            return function () {
                console.log(num);
            }
        })(i/*删掉*/), 1000 * i);
    }
    

    分析一下,删掉i之后,arguments数组为空,那么自调用函数内的参数num就为undefined,那么返回的闭包函数届时也会返回undefined。

    • i为0时,将第一个闭包函数推入队列待执行(延迟时间:0s),num为undefined。
    • i为1时,将第二个闭包函数推入队列待执行(延迟时间:1s),num为undefined。
    • i为2时,将第三个闭包函数推入队列待执行(延迟时间:2s),num为undefined。
    • i为3时,将第四个闭包函数推入队列待执行(延迟时间:3s),num为undefined。
    • i为4时,将第五个闭包函数推入队列待执行(延迟时间:4s),num为undefined。
      主线程执行完毕后按推入顺序和延迟时间来执行队列内任务。

    每隔一秒,打印一个undefined。


    5.

    for (var i = 0; i < 5; i++) {
      setTimeout((function(i) {
        console.log(i);
      })(i), i * 1000);
    }
    

    如果是这样会输出什么?

    setTimeout内部定义的函数是一个立即执行函数,也就是每一次定义后,就立即调用,那么等价于:

    for (var i = 0; i < 5; i++) {
      setTimeout(/*立即执行函数调用后*/undefined, i * 1000);
    }
    

    整个for循环过程中,总共定义了5次函数,由于定义后立即调用,相当于又调用了5次函数,每一次函数内部的i值等于每一次循环的i值。这5次调用都是同步的,setTimeout里则啥都没有。

    结果:会立即打出0~4。


    《JS高程》上还有一个类似的问题。

    function createFuncitons() {
        var result = [];
        for (var i=0;i<10;i++) {
            result[i] = function() {
                return i;
            };
        }
    
        return result;
    }
    

    这个函数会返回一个数组。表面上看,似乎每个函数都应该返回自己的索引值,即位置0的函数返回0,位置1的函数返回1,以此类推。但实际上,每个函数都返回10,。因为每个函数的作用域链中都保存着createFunctions()函数的活动对象,所以它们引用的都是同一个变量i,当createFunctions()函数返回后,变量i的值是10,此时每个函数都引用着保存着变量i的同一个变量对象,我们可以创建一个匿名函数强制让闭包的行为符合预期。

    function createFuncitons() {
        var result = [];
        for (var i=0;i<10;i++) {
            result[i] = function(num) {
                return function() {
                    return num;
                }
            }(i);
        }
    
        return result;
    }
    

    或者这样

    function createFuncitons() {
        var result = [];
        for (var i=0;i<10;i++) {
            (function(num) {
                result[i] = function() {
                    return num
                };
            })(i)
        }
    
        return result;
    }
    

    又或者这样

    function createFuncitons() {
        var result = [];
        var closure = function(num) {
            return function() {
                return num
            }
        }
        
        for (var i=0;i<10;i++) {
            result[i] = closure(i);
        }
    
        return result;
    }
    
    over

    相关文章

      网友评论

          本文标题:作用域链与闭包(二)

          本文链接:https://www.haomeiwen.com/subject/zemlhftx.html