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

2012-12-04 11:52:03 +08:00
 robhsiao
初次用expressjs写了点小东西,窃以为变量作用域名是node.js(或者应该是javascript) ugly的一个地方。

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


请问大家是如何规避这个问题?依靠命名规范?或者是我用法是不正确?
13334 次点击
所在节点    Node.js
26 条回复
luin
2012-12-04 12:43:08 +08:00
回调函数执行完会自动释放外层局部变量。
hidden
2012-12-04 12:46:05 +08:00
如果没有作用域照样存在这个这个问题,如果限制了作用域,访问callback外面的变量麻烦。用熟了表示没什么压力。
robhsiao
2012-12-04 13:00:22 +08:00
@luin 倒不是担心会内存的泄露,主要是可能会引用错误的变量,或者无意中篡改到外部变量。
robhsiao
2012-12-04 13:06:35 +08:00
@hidden
明白。可能是表达上有点错误,并不是说变量作用域造成的问题,而是想了解一下大家有没有一些 best practices之类的来解决这样的问题.. ^^
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
2012-12-04 13:20:19 +08:00
@hyq
这个也有了解,但是您知道,node.js 里边处处是异步,所有的都要通过callback,父级函数里边定义的变量,子函数内不能把它盖掉,因此孙函数可能还需要用它。

这样就造成每个变量命名都要全局是唯一。
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
2012-12-04 13:34:17 +08:00
@robhsiao 我觉得这应该是函数式编程都有的问题,如果用面向过程或者面向对象的思维去写JavaScript,都是不太合适的.
BOYPT
2012-12-04 14:01:29 +08:00
所以coffeescript就是爲了解決js的這些尷尬問題而定好的一系列規範而出現的。
robhsiao
2012-12-04 14:15:50 +08:00
@BOYPT 原来如此,我还以为是我打开的方式不对..

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

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

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

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

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

hidden
2012-12-04 18:20:18 +08:00
遇到过啊。 这个名字可以变来变去的用。 Request, request, req, res... 全局一个request,里面实例一个req。 套一个callback你可以取出你下面要用的具体数据例: var ip = req.socket.remoteAddress; 在里面就放心覆盖吧。 也就最多套两个callback。 再套的话你就得考虑拧一个函数出来了。 反正蛮灵活的。
jackyz
2012-12-04 19:53:28 +08:00
>> 初次用 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
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
2012-12-05 11:05:44 +08:00
js不是完美的语言。

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

Coffeescript从另外一个角度来解决这个问题。有个说法是,计算机科学里面的任意问题都可以通过一层封装解决,Coffeescript就是这么一层封装。那堆麻烦的plain javascript caveat,看看知道就好了,实在不想时刻惦记着。
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
2012-12-05 13:29:19 +08:00
@jackyz 我试着用promise模式写一下你这个例子

https://gist.github.com/4212565

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

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

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/54041

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX