nodejs异步的一个问题

在express中
可以正常写入文件,写入文件名也是正确的

但是
console.log(imgName)
每次输出的结果都是一样的 都是最后的文件名
为什么呢,
要怎么解决比较好

router.post('/uploadImages', function(req, res){
    var form = new multiparty.Form();
    form.parse(req, function(err, fields, files){
        //获得浏览器提交的图片数据
        var imgDatas = fields.editImg;
        //遍历图片数据,然后写入到后台
        for(let i = 0; i < imgDatas.length; i++){
            elem = imgDatas[i].replace(/^data:image\/\w+;base64,/, '');
            var dataBuffer = new Buffer(elem, 'base64');
            var imgName = path.join(__dirname,'../public/tempImg/') +'img' + Date.now() + i + '.png';
            //写入文件
            fs.writeFile(imgName, dataBuffer, function(err){
                if (err) {
                    console.log(err);
                }else{
                    console.log(imgName);
                }
            })
        }
    })
})
阅读 3.5k
4 个回答

michael_cai的回答很准确了,我这里补充一下代码,如果你是不理解闭包的意思的话,实际是这样的

fs.writeFile(
    imgName, 
    dataBuffer, 
    /*
     * 这里主要分析一下这个回调函数,这个
     * 回调函数执行时也许for循环已经执行
     * 完毕并且退出了这时的imgName参数就
     * 锁定为最后一次执行时的样子
     */
    function(err){
    if (err) {
        console.log(err);
    }else{
        // 这里读取的上下文是当回调开始执行时的上下文而非声明时的上下文
        console.log(imgName);
    }
})

那么如何保存声明回调时的上下文能,最直接的办法就是将整个方法放入闭包中
因为闭包可以"保存"调用时的参数,将这个参数"私有化",注意,我这里的解释都是比较通俗的
准确的解释请参阅更多详细资料,这个不是闭包的定义,那么代码应该改为

for(let i = 0; i < imgDatas.length; i++){
    elem = imgDatas[i].replace(/^data:image\/\w+;base64,/, '');
    var dataBuffer = new Buffer(elem, 'base64');
    var imgName = path.join(__dirname,'../public/tempImg/') +'img' + Date.now() + i + '.png';
    //改写开始,这是一个比较简单的闭包,这样就能解决你的问题了
    (function(imgName){
        fs.writeFile(imgName, dataBuffer, function(err){
            if (err) {
                console.log(err);
            }else{
                console.log(imgName);
            }
        })
    })(imgName)
}

因为你的imgName不是通过参数传进去的,实际上log的都是同一个,加一个闭包就好了。

大家都已经给出解决方案( HOW )了,我来说说为什么( WHY )吧。

声明一个函数( function )的时候,函数会捕获外部变量的引用,注意,是引用,而不是值。
下面是例子

var foo;
for (var offset = 0; offset < 5; offset++) {
    foo = offset;
    setTimeout(
        function () {
            console.log('foo:', foo);
        },
        1000 
    );
}

执行上面的代码,你会得到下面的结果:

foo: 4
foo: 4
foo: 4
foo: 4
foo: 4

看上起很不符合直觉,其实不然,注意我上面说过的 “函数会捕获外部变量的引用”,在每一次循环里面生成的匿名函数中,捕获的是 foo 的引用,而不是 foo 的值。

你可能会发现我上面的代码和你的实际情况有所出入,下面使用和你实际情况相似的代码。其实也就删掉 for 循环外面的 var foo; 并把 var 关键字加到循环内部的 foo 前面。你可以运行一下代码,发现和上面的运行结果是一样的,接下来就带来另一个问题了,我不是在每次循环的时候重新声明了 foo 吗,为什么还是会这样。

for (var offset = 0; offset < 5; offset++) {
    var foo = offset;
    setTimeout(
        function () {
            console.log('foo:', foo);
        },
        1000 
    );
}

Really? 其实真的重新声明了吗?
我们用下面的代码做个试验:

var foo = 25;
var print = function () {
    console.log('foo:', foo);
}
print(); // foo: 25

var foo = 35;
print(); // foo: 35

在这里,我定义了 print 这个函数,用于打印 foo 的值,下面我用 var 关键字再次声明了 fooprint 函数仍旧可以很好的工作!oops!。再次声明的 foo 和第一次声明的 foo 的引用是一样的。

所以呢?
让我们回到刚才的代码:

for (var offset = 0; offset < 5; offset++) {
    var foo = offset;
    setTimeout(
        function () {
            console.log('foo:', foo);
        },
        1000 
    );
}

你可能又有疑问了,我不是在 for 循环中声明的吗?这里可能有个关于作用域的误区,for 循环并不会生成新的作用域。所以上面的代码等价于:

var foo;
for (var offset = 0; offset < 5; offset++) {
    foo = offset;
    setTimeout(
        function () {
            console.log('foo:', foo);
        },
        1000 
    );
}

for 循环不会生成新的作用域,但是函数可以,使用立即执行函数表达式( IIFE )包装一下:

for (var offset = 0; offset < 5; offset++) {
    var foo = offset;
    (function (_foo) {
        setTimeout(
            function () {
                console.log('foo:', _foo);
            },
            1000 
        )
    })(foo);
}

执行结果:

foo: 0
foo: 1
foo: 2
foo: 3
foo: 4

Nice Work!

其实使用 ES6let 可以更简单,只需要把 var 换成 let 就好了:

for (let offset = 0; offset < 5; offset++) {
    let foo = offset;
    setTimeout(
        function () {
            console.log('foo:', foo);
        },
        1000 
    );
}

执行结果:

foo: 0
foo: 1
foo: 2
foo: 3
foo: 4

具体可参看:ECMAScript 6 入门 - let和const命令

这事用async就简单太多了

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