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

作用域链与闭包(二)

作者: 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