JavaScript闭包中令人困惑的一个奇怪问题

例1:

var obj = new Object();
var events = {m1: 'clicked', m2: 'changed'};

for (var e in events) {
    (function() {
        var aValue = e;
        obj[e] = function() {
            //var aValue = e;
            console.log(events[aValue]);
        };
    }());
};

console.log(obj.m1 === obj.m2);  //false

obj.m1();  //clicked
obj.m2();  //changed

例2:

var obj = new Object();
var events = {m1: 'clicked', m2: 'changed'};

for (var e in events) {
    (function() {
        //var aValue = e;
        obj[e] = function() {
            var aValue = e;
            console.log(events[aValue]);
        };
    }());
};

console.log(obj.m1 === obj.m2);  //false

obj.m1();  //changed
obj.m2();  //changed

以上两个例子中,除了var aValue = e;这一句位置不同:例1位于外层匿名函数中、例2位于内层匿名函数中,其他部分完全相同。但结果令我困惑,为什么?哪位帮忙分析下?谢谢!

—————————————————————————————————————————
PS.目前我的理解,一般来讲,同一个外层函数中的不同闭包(内层函数)的作用域链(本质上是一个指向变量对象的指针列表,参见《JavaScript高级程序设计(第3版)》P.179正文倒数第2段及P.180图7-2)引用的是外层函数的同一个活动对象;但是在例1中,因为立即执行函数两次独立执行的缘故,导致创建了两个不同的外层函数的活动对象,从而每个闭包的作用域链各自引用了其中的一个活动对象。如果有误,还望指教。

阅读 5.5k
7 个回答

闭包在函数声明的时候生成,它确定的是变量的查找路径而不是变量的值;变量的值在函数执行时才能确定

var obj = new Object();
var events = {m1: 'clicked', m2: 'changed'};

for (var e in events) {
    (function() {
        var aValue = e;//A
        obj[e] = function() {//B
            //var aValue = e;//A1
            console.log(events[aValue]);//C
        };
    }());//D
};

console.log(obj.m1 === obj.m2);  //E false

obj.m1();  //clicked
obj.m2();  //changed

D行定义了一个立即执行函数,函数在执行时引用了当前循环的e值并赋值给立即执行函数的局部变量aValue,B行定义了匿名函数,并且为obj对象添加了e表示的字符串值为属性名的一个属性,并且绑定到匿名函数。匿名函数使用到了D行定义的立即执行函数的局部变量aValue,立即执行函数完毕后,B行匿名函数有对立即执行函数的闭包,确定匿名函数中需要的变量aValue到立即函数中查找

下一次循环时,e变量指向的值发生变化,但先前生成的B匿名函数引用aValue变量值是不会跟着变化的,aValue变量值是确定的,在立即执行函数中。
每次循环B行声明的匿名函数对象都是不同的,立即执行函数对象也是不同的

因为有变量应用关系的存在,匿名立即执行函数不会被释放掉,其局部变量也会得到保留~~~

var obj = new Object();
var events = {m1: 'clicked', m2: 'changed'};

for (var e in events) {
    (function() {
        //var aValue = e;//A
        obj[e] = function() {//B
            var aValue = e;//A1
            console.log(events[aValue]);//C
        };
    }());//D
};

console.log(obj.m1 === obj.m2);  //false

obj.m1();  //changed
obj.m2();  //changed

和上面不同的地方将A行移动到A1行,看着似乎差不多,但是结果却不同
原因就在于B处声明的匿名函数,引用了最外层的变量e,构成对对外层函数的闭包;只是确立了变量引用了关系(确切的说确定了变量查找路径),但是具体的值在函数被执行前是无法预知的
等整个循环结束后, e的值为m2(这个也不一定,看events属性定义的顺序及属性值的获取手段),当obj属性绑定的函数被执行时,A1行aValue都为m2~~~~

画了个图:展示了两种情况下,执行到console.log(events[aValue])时的作用域(链)状态。只标出了与aValue查找有关的变量即取值。

Image

这样就明白了:第一种情况下,aValue分别取的是外部的“立即执行函数”形成的两个不同闭包中的aValue,两者值不同(因为它们的值是在两次循环期间确定的)。第二种情况下,aValue都取的是Global中的e的值,它的值在循环完成后,变成了'changed'

因为闭包额 兄弟

例2中,你在执行m1和m2的时候,e已经迭代到了最后一个,所以输出都是最后一个,
例1中,通过闭包保存了e的值,所以输出是对应的值

答案写在注释中

var obj = new Object();
var events = {m1: 'clicked', m2: 'changed'};

for (var e in events) {
    (function() {
        var aValue = e;
        obj[e] = function() {
            //这里的aValue是在函数体外赋值的,并不受方法自身影响,同时由于闭包的原因,aValue在赋值后并不会被释放,所以此处的aValue是在for.in 循环时被赋的值
            console.log(events[aValue]);
        };
    }());
};

console.log(obj.m1 === obj.m2);  //false

obj.m1();  //clicked
obj.m2();  //changed
var obj = new Object();
var events = {m1: 'clicked', m2: 'changed'};

for (var e in events) {
    (function() {
        obj[e] = function() {
            var aValue = e;
            //这里的aValue是在方法运行时在函数体内部被赋值的,其值=e,而e是在for.in循环时赋值的,循环结束后e=m2 , 所以在后面执行的过程中,每次会创建一个新aValue变量,变量值为e,e=m2, 所以两次都是changed
            console.log(events[aValue]);
        };
    }());
};

console.log(obj.m1 === obj.m2);  //false  


obj.m1();  //changed
obj.m2();  //changed

关键是aValue在那里定义

var obj = new Object();
var events = {m1: 'clicked', m2: 'changed'};
var aValue;//如果aValue放在这里定义,结果才会是一样的,关键是aValue在哪里定义,而不是 e 的引用是哪一个;
for (var e in events) {
    (function() {
        aValue = e;
        obj[e] = function() {
            console.log(events[aValue]);
        };
    }());
};

console.log(obj.m1 === obj.m2);  //false  


obj.m1();  //changed
obj.m2();  //changed
新手上路,请多包涵

例1的赋值在函数外,执行函数之前aValue的具体值就已经确定;
例2的赋值在函数内,执行函数时取aValue的值;
这就是最根本的原因,js 函数表达式 延迟解析的机制。

1.作用域链在函数创建时便存在,作用域链是为函数执行时函数所需变量(变量对象)的有序访问而存在的指针;
2.作用域链一般不会发生变化(不delate),变量对象内的变量值会随时发生变化;
3.闭包会保留父级函数的变量对象,而父级函数再执行完毕会销毁作用域链和执行环境;
4.循环内的父级函数每次运行都会创建一次变量对象,并被闭包保存,换句话讲,闭包每次访问的父级变量对象均不相同;
5.因此,第一个的情况满足4,既每次循环时,avalue的值都不一样,闭包内的父级函数的变量对象均不相同,所引用的数值也不一样;第二个是因为在循环结束时,e是循环时的变量,只能读到结束时e的数值,所以最后都一样;

图片描述

如图,(function(){ // some code ...})()这个自执行的匿名函数与上一层的父函数形成链接,当执行m1时,会将值传递给该函数。这就是闭包中子函数可以获得父函数变量的特征。(另一个特征是闭包调用的变量一直在内存中)

当执行obj[e] = function() {}
此时如果是例一:aValue = e;放在该函数前,仍然由于闭包(function(){ // some code ...})()的特性,每次为aValue赋值时,都会由于作用域链,其值会发生改变;
此时如果是例二:aValue = e;放在该函数内,该函数由于作用域链,其obj[e]的值e发生变化,但是关联的匿名函数中的变量仅获取变量,其值会按照for执行的最后结果返回给该函数。

如果简化函数:

var obj = new Object();
var events = {m1: 'clicked', m2: 'changed'};

for (var e in events) {
    (function() {
        // console.log(e);
        obj[e] = (function() {
            console.log(e);
        })(e);
    }());
};

obj.m1();  // m1
obj.m2();  // m2

如果将第一层的匿名函数的e值传递给第二次的匿名函数,其值会同时变化。
图片描述

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题
宣传栏