作用域链的这种配置机制引出了一个值得注意的副作用,即闭包只能取得包含书函数中任何变量的最后一个值。别忘了闭包所保存的是真个变量对象,而不是某个特殊的变量。
栗子
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;
}
网友评论