V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
robhsiao
V2EX  ›  Node.js

node.js里由于回调函数层层嵌套,使用变量似乎是一件要很谨慎的事?

  •  
  •   robhsiao · 2012-12-04 11:52:03 +08:00 · 13075 次点击
    这是一个创建于 4154 天前的主题,其中的信息可能已经有所发展或是发生改变。
    初次用expressjs写了点小东西,窃以为变量作用域名是node.js(或者应该是javascript) ugly的一个地方。

    基本每一个模块需要占用一个全局变量,而且由于回调函数里可能需要使用父级函数的局部变量,所以回调函数嵌套几层之后,感觉使用每一个变量要非常谨慎,每一个变量都是一个炸弹。


    请问大家是如何规避这个问题?依靠命名规范?或者是我用法是不正确?
    26 条回复    1970-01-01 08:00:00 +08:00
    luin
        1
    luin  
       2012-12-04 12:43:08 +08:00
    回调函数执行完会自动释放外层局部变量。
    hidden
        2
    hidden  
       2012-12-04 12:46:05 +08:00
    如果没有作用域照样存在这个这个问题,如果限制了作用域,访问callback外面的变量麻烦。用熟了表示没什么压力。
    robhsiao
        3
    robhsiao  
    OP
       2012-12-04 13:00:22 +08:00
    @luin 倒不是担心会内存的泄露,主要是可能会引用错误的变量,或者无意中篡改到外部变量。
    robhsiao
        4
    robhsiao  
    OP
       2012-12-04 13:06:35 +08:00
    @hidden
    明白。可能是表达上有点错误,并不是说变量作用域造成的问题,而是想了解一下大家有没有一些 best practices之类的来解决这样的问题.. ^^
    hyq
        5
    hyq  
       2012-12-04 13:12:32 +08:00
    @robhsiao 用var关键字声明变量,要养成这个好习惯
    var x = 1;
    function(){
    var x = 2;
    console.log("inner x = " + x);
    }()
    console.log("outer x = " + x);
    robhsiao
        6
    robhsiao  
    OP
       2012-12-04 13:20:19 +08:00
    @hyq
    这个也有了解,但是您知道,node.js 里边处处是异步,所有的都要通过callback,父级函数里边定义的变量,子函数内不能把它盖掉,因此孙函数可能还需要用它。

    这样就造成每个变量命名都要全局是唯一。
    hyq
        7
    hyq  
       2012-12-04 13:31:43 +08:00
    那么你可以用一个变量来保存作用域,如
    function fun(){
    var _scopeFun = this;
    var a = 1;
    function(){
    var a = 2;
    console.log("inner a = ", a);
    console.log("outer a = ", _scopeFun.a);
    }()
    }
    hyq
        8
    hyq  
       2012-12-04 13:34:17 +08:00
    @robhsiao 我觉得这应该是函数式编程都有的问题,如果用面向过程或者面向对象的思维去写JavaScript,都是不太合适的.
    BOYPT
        9
    BOYPT  
       2012-12-04 14:01:29 +08:00
    所以coffeescript就是爲了解決js的這些尷尬問題而定好的一系列規範而出現的。
    robhsiao
        10
    robhsiao  
    OP
       2012-12-04 14:15:50 +08:00
    @BOYPT 原来如此,我还以为是我打开的方式不对..

    当然,也有可能是如 @hyq 所说 思维定式造成..

    看来得试试coffeescript去
    hidden
        11
    hidden  
       2012-12-04 16:45:15 +08:00
    个人使用倒是不麻烦,覆盖了全局变量,并且后面还要用到这个变量的概率是很小的。 即使用到了,后面来修改也不麻烦。
    robhsiao
        12
    robhsiao  
    OP
       2012-12-04 17:09:02 +08:00
    @hidden

    最常见的就是... request、response 这样的变量

    最外层的http server callback会有一组这样的变量,里边的callback可能在连memcache、或者使用http.request请求其它API服务时都可能有一个request一个response,而这些callback最终要生成响应吐给客户端,所以最外层的response不能覆盖。

    ---
    也不知道大家是不是清楚我在说什么

    hidden
        13
    hidden  
       2012-12-04 18:20:18 +08:00
    遇到过啊。 这个名字可以变来变去的用。 Request, request, req, res... 全局一个request,里面实例一个req。 套一个callback你可以取出你下面要用的具体数据例: var ip = req.socket.remoteAddress; 在里面就放心覆盖吧。 也就最多套两个callback。 再套的话你就得考虑拧一个函数出来了。 反正蛮灵活的。
    jackyz
        14
    jackyz  
       2012-12-04 19:53:28 +08:00   ❤️ 3
    >> 初次用 expressjs 写了点小东西,窃以为变量作用域名是node.js(或者应该是javascript) ugly 的一个地方。

    用了很长时间的 node.js 了,真没觉得。

    >> 基本每一个模块需要占用一个全局变量,

    类似

    var m = require('./my_module');

    你可以在 node 命令行里 require 你自己的代码,然后输入 m. 按 tab 试试看。全局的可以直接按 tab 看到,或者 global. tab 查看得到。这会让你对 exports 有个感性认识。你需要了解的是 node.js 的 module 机制。参考下面的代码, module 的概念与之类似:

    (funciton(){ var a = 1; .... return {x:a}; })();

    这个叫啥来着,立即执行的匿名函数?闭包?反正就是这个东西了。

    你的 module 对外可见的,只是你 exports.fun = xxx 的部分,其余的一律不可见(我认为,这种机制比起 public private 什么的,至少是一样的强大)。全局变量的问题,基本不用担心。

    >> 而且由于回调函数里可能需要使用父级函数的局部变量,所以回调函数嵌套几层之后,感觉使用每一个变量要非常谨慎,每一个变量都是一个炸弹。

    ...
    function some_fun(req,res,next){
    redis.incr('next_id', function(e,r){
    if(e) return next(e);
    redis.set('key', object, function(e,r){
    if(e) return next(e);
    res.send([1,'ok']);
    }
    }
    }
    ...

    类似这样的回调层次和作用域(不同层次都有 e 和 r 存在,这里,因为有 var shadow 机制,你可以很放心地取相同的名字),没什么可担心的。

    回调使用外层的变量,这是 javascript 提供的语法机制,但如果回调里很大量地在使用外层变量,那就有可能是 bad smell 了,这很微妙,但不复杂,这种情况一般都可以很简单地重构为传参的形式。

    >> 请问大家是如何规避这个问题?依靠命名规范?或者是我用法是不正确?

    我认为你可能是还没有“习惯”。

    btw. 个人建议慎用 coffee script 既然 plain javascript 能解决所有的问题,那就没有必要引入实质上是同一种语言(coffe script IS javascript)的另外一套语法。有个老外写过一篇,处处不记得了,转载在这里 http://cliclip.com/#clip/7/521
    linlinqi
        15
    linlinqi  
       2012-12-04 20:13:56 +08:00
    解决连环嵌套比较好的方法就是使用promise模式编程了,见这里的解释:

    http://www.infoq.com/cn/news/2011/09/js-promise

    node.js能用上的promise库有哪些呢?

    http://stackoverflow.com/questions/7588581/what-nodejs-library-is-most-like-jquerys-deferreds
    BOYPT
        16
    BOYPT  
       2012-12-05 11:05:44 +08:00
    js不是完美的语言。

    作为一个语言,容易让人混淆困惑的地方都应该认为是语言的缺陷;当然这些可以通过提倡一种“xxxx规范”,在某情况下本来可以有多种approach,但根据规范仅限定仅使用一种,等等。 看C++就是了,什么规范的书比基本语法书要厚几倍。

    Coffeescript从另外一个角度来解决这个问题。有个说法是,计算机科学里面的任意问题都可以通过一层封装解决,Coffeescript就是这么一层封装。那堆麻烦的plain javascript caveat,看看知道就好了,实在不想时刻惦记着。
    jackyz
        17
    jackyz  
       2012-12-05 12:14:38 +08:00
    @linlinqi 借机与楼上探讨。

    n 层嵌套确实很难看,但我很怀疑 promise 或者 async 这类的解决方案是否真的有效。我也曾经尝试过一些 module ——感觉对代码表达的限制很大。这类东西基本都是语法糖,而且是 for control flow 的语法糖,但 control flow 其实是一段程序的精髓,是很灵活的东西,对于这个东西的抽象常常让人产生“还不如退回去的感想”。

    举例说明

    function some_fun(req,res,next){
    --redis.incr('next_id', function(e,r){
    ----if(e) return next(e);
    ----var object = {id:r, now:Date.now()};
    ----redis.set('key', object, function(e,r){
    ------if(e) return next(e);
    ------res.send([1,'ok']);
    ----}
    --}
    }

    贴代码没有缩进,就用-符号代替 space 了。

    通常来说,似乎可以用 serial 来理解这里的两层嵌套,但,如果考虑错误处理,问题就很复杂了。比如,if(e)return next(e); 这一句 return 则可以避免内层的 redis 语句执行,是有性能意义的代码。这里的处理是极度简化之后的情况,实际情况比这可能要复杂很多。

    我要表达的意思是,如果以通用的 promise 库来做 serial 之类的流程抽象,似乎无法准确的表达这里的精微之处。

    @linlinqi 对此如何取舍?
    linlinqi
        18
    linlinqi  
       2012-12-05 13:29:19 +08:00
    @jackyz 我试着用promise模式写一下你这个例子

    https://gist.github.com/4212565

    Deferreds是可以嵌套的,这里我就没细写了。用done和fail可以表现出这些流程,习惯之后会好用很多。
    linlinqi
        19
    linlinqi  
       2012-12-05 13:30:24 +08:00
    测试直接插入gist

    <script src="https://gist.github.com/4212565.js"> </script>
    zhangxiao
        20
    zhangxiao  
       2012-12-05 14:53:04 +08:00
    @linlinqi 不要用https,用http就可以嵌入gist了
    jackyz
        21
    jackyz  
       2012-12-05 15:38:07 +08:00
    @linlinqi

    谢谢重构,两个感觉不太适应的地方:

    1. 10 行变成 23 行 ,没感觉在哪里变得更清晰了呢?
    2. 从 node 的 callback(e,r) 风格转变为 resolve -> done reject -> fail 风格。

    另外,如果再加一层回调呢,会变成什么样子?在实际应用中有个 5,6 层的回调不稀奇呀。

    那个层次还需要再包装 resolve 和 reject 还是可以“重用” promise 又或者怎样?各个层次的异常如果要有不同的处理代码,要怎么表达呢?是:
    fun1().done().fail().fun2().done().fail()
    还是:
    fun().done( fun2().done().fail() ).fail()
    又或者还是怎样?

    之前的尝试,进行到这里就退回去了(感觉没啥区别呀,而且还要分别包装 e 和 r ),没准我是那个关节没相通?
    luin
        22
    luin  
       2012-12-05 15:59:35 +08:00
    @jackyz 你觉得使用async如何呢?
    http://gist.github.com/4213629
    linlinqi
        23
    linlinqi  
       2012-12-05 17:33:24 +08:00
    @jackyz 假设redis这个对象已经是基于Deferred模式封装良好的话,那么写的好看一些会是这样

    http://gist.github.com/4212565.js?file=ahhhh.js

    这样的行数是不是满足了?
    jackyz
        24
    jackyz  
       2012-12-05 19:20:05 +08:00
    嵌套层次太多确实难看。要追求漂亮,但不能以失去灵活性为代价。

    @luin 论坛氛围不错,我改写了例子,希望能把问题引向更深入。
    主要的改写是引入了需要传递更多因素的情况,来表达这种灵活性在使用 control flow module 之后的丧失。

    http://gist.github.com/4214679.js

    引入之后,感觉有两个问题。

    传参自由度的问题:

    第 6 行分别引用了两个外层里定义的 id 和 val 。对应在 async 写法的第 24 行,是不 work 的。要解决这个问题,需要显式地向外用 callback 传递或者引入一个 params object 来解决问题。需要传递的参数更多的话,可能更麻烦。

    async 的思路是通过 callback 的参数传递给下一个 function 。各个 function 的变量作用域是并列的,也就是说,嵌套层次的扁平是以引用外层变量的能力作为代价的。

    错误处理自由度的问题:

    上述每一个错误都给了一个不同的错误处理,之前的例子是全都用 next(e) 来处理,所以,体现不出这种约束来。在 async 的版本里,所有的错误都汇集到 27 行来处理,要如何区分这许多种错误呢?

    我也有用 async ,主要是用它抽象数据结构的相关方法,比如 map 之类,但 control flow 的因为上述的问题,就暂时还没有用。

    @linlinqi 这是否意味着需要在 callback(e,r) 的 node.js 标准风格和 deferred 风格之间做适配呢?这个适配,在 node 没有推出标准的 deferred 风格之前,似乎是没有动力去完成的。

    还是那句话,要追求漂亮,但不能以失去灵活性为代价。我知道这可能有些不切实际,必然要 trade off 什么的。不过,思考一下倒也有益。
    jackyz
        25
    jackyz  
       2012-12-07 17:10:26 +08:00
    这个问题踢到铁板讨论不下去了吗?
    robhsiao
        26
    robhsiao  
    OP
       2012-12-07 17:49:11 +08:00
    sorry, 楼主读书不多,只能围观 :(
    各位请继续...
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   5630 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 32ms · UTC 06:23 · PVG 14:23 · LAX 23:23 · JFK 02:23
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.