javascript tip: Duff's Device

偶尔在网上闲逛的时候,总能发现一些短小精悍的代码,优雅简练,下面这个就是一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var iterations = Math.ceil(values.length / 8);
var startAt = values.length % 8;
var i = 0;

do {
switch(startAt){
case 0: process(values[i++]);
case 7: process(values[i++]);
case 6: process(values[i++]);
case 5: process(values[i++]);
case 4: process(values[i++]);
case 3: process(values[i++]);
case 2: process(values[i++]);
case 1: process(values[i++]);
}
startAt = 0;
} while (--iterations > 0);

原来是用C实现的,这里改成Javascript版本。它的作用是用来优化对一个大数组的循环访问。如果对Javascript感兴趣,可以看这里,作者是yahoo的大牛。

how I fixed invalid byte sequence in UTF-8

TL;DR

The real problem I encounter and resolution is here.

About ArgumentError: invalid byte sequence in UTF-8

when I switch from linux box to Mac, I have to setup octopress
for my blog. I have followed the guide, do rvm requirements and
have ruby 1.9.3 installed cleanly. but when I hit bundle install every time, It just gives me nothing but an
ArgumentError: invalid byte sequence in UTF-8 error. Since I’m
not quite familiar with ruby, I googled a lot, and none of
the solutions fits me. After a long time of unlucky, finally I found the real problem.


First things first, the real problem is that I had changed files under /etc/paths.d/, and vim leaved undelete hidden file under that path. The consequence is that path_helper will read all files (including hidden files) to build PATH variable, which then included unprintable chars which is invalid utf8 sequence. This is the reason bundle install complained about. but after I deleted all hidden files under /etc/paths.d, the problems still exists! Now that as First Rule of Programming: It’s always your fault said, the problem must be mine. After a few moment of pondering, I found the problem is that I’m in a tmux session and it cached PATH variable! So I quit tmux and restart, everything works great now.

make mark all like this scope sensitive in javascript

sublime text 2的多cursor功能很强大,在做某些操作的时候很方便。但是标记
多cursor也很麻烦。emacs的multiple-cursors可以用来模仿这个特性,而且
更好的是它完全可以融入emacs的环境里,配合其他mode来使用会更加强大。但是
有个小问题是它的标记是基于正则的,这在有些时候不太方便,比如在js里,我
想标记某个变量的所有实例,就很难。于是自己写了一个函数来解决这个问题。

sycao/js2-mark-token-at-point
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

(defun sycao/js2-mark-token-at-point ()
"mark all semantic instances of the variable under cursor within the defining scope"
(interactive)
(mc/remove-fake-cursors)
(let ((ref-node (js2-node-at-point))
(ref-node-beg nil)
name
scope)

(setq ref-node-beg (js2-node-abs-pos ref-node))
(unless (region-active-p)
(goto-char ref-node-beg)
(push-mark (+ ref-node-beg (js2-node-len ref-node)))
(activate-mark))

(save-excursion
(when (and ref-node (js2-name-node-p ref-node))
(setq scope (js2-node-get-enclosing-scope ref-node)
name (js2-name-node-name ref-node))
(setq scope (js2-get-defining-scope scope name))
(js2-visit-ast
scope
(lambda (node end-p)
(when (and (not end-p)
(js2-name-node-p node)
(string= name (js2-name-node-name node)))
(let* ((beg (js2-node-abs-pos node))
(end (+ beg (js2-node-len node)))
(new-scope (js2-node-get-enclosing-scope node))
(new-scope (js2-get-defining-scope new-scope name)))
(if (and (eq new-scope scope)
(not (= beg end)))
(progn
(goto-char end)
(push-mark beg)
(exchange-point-and-mark)
(if (not (= ref-node-beg beg))
(mc/create-fake-cursor-at-point))
(exchange-point-and-mark)
))))

t)))))
(if (> (mc/num-cursors) 1)
(multiple-cursors-mode 1)
(multiple-cursors-mode 0)))

我参考了js2-highlight-vars.el和mc/mark-all-like-this的代码。基本思
路就是利用js2-mode提供的api遍历ast,找到所有目标变量的引用,并进行标记。
需要注意一点的是,要把函数自身加入mc--default-cmds-to-run-once里,否
则会产生循环调用。

1
(add-to-list 'mc--default-cmds-to-run-once 'sycao/js2-mark-token-at-point)

例如用下面的代码进行测试,如果光标在第二行的localVar上,执行
sycao/js2-mark-token-at-point就会标记第2、9、11行的localVar。

var localVar = 12;
1
2
3
4
5
6
7
8
9
10
11
12
13

function f() {
var localVar = 10;

function f2() {
var localVar = 20;
localVar++;
}

localVar += 1;
f2 ();
localVar += 9;
}

porting narcissus for nodejs

最近在看narcissus的代码,这是一个js实现的js解释器。为了方便导出AST,
交互式的检查运行环境,于是修改了一下代码,让它可以跑在nodejs上。
代码放在github上了,做的修改不多,主要原版年代久远,还使用了mozilla的
JS扩展,对这些部分进行一些修改即可。

主要修改要两个方面:

  • 改写conditional catch clause。这个是mozilla的扩展,nodejs不支持。
    例如
    1
    2
    3
    4
    5
    6
    7
    8
    try {
    execute(parse(s), x2);
    } catch (e if e == THROW) {
    x.result = x2.result;
    throw e;
    } finally {
    ExecutionContext.current = x;
    }

改为

1
2
3
4
5
6
7
8
9
10
11
12
try {
execute(parse(s), x2);
} catch (e) {
if (e == THROW) {
x.result = x2.result;
throw e;
} else {
throw e;
}
} finally {
ExecutionContext.current = x;
}

  • RegExp对象不是callable的,
    例如
    1
    2
    3
    4
    5
    6
    if ((match = fpRegExp(input))) {
    token.type = NUMBER;
    token.value = parseFloat(match[0]);
    } else if ((match = /^0[xX][\da-fA-F]+|^0[0-7]*|^\d+/(input))) {
    ....
    }

改为

1
2
3
4
5
6
if ((match = fpRegExp.exec(input))) {
token.type = NUMBER;
token.value = parseFloat(match[0]);
} else if ((match = /^0[xX][\da-fA-F]+|^0[0-7]*|^\d+/.exec(input))) {
....
}

另外,添加了一些原代码运行需要的函数的实现,print、snarf和__defineProperty__。
为了不影响宿主环境,所有代码都在一个新的vm上运行。

需要说明的是下面这个片段,如果没有,print和snarf运行时会出错,因为被narcissus认为
是非callable的。

1
2
3
s = 'print.__proto__ = Function;' +
'snarf.__proto__ = Function;';
vm.runInContext(s, runtime);

emacs tip: search active region

WebStorm有一个小功能特别好,就是当你选择一段文字之后,进行搜索,它会直接搜索选中的文字。
emacs的搜索功能强大,但是得在进入搜索模式后,再选择要搜索的表达式,下面的这个defadvice
可以达到WebStorm的效果,如果当前有活动区域,则直接搜索此区域:

search-region
1
2
3
4
5
6
7
8
9
10
11
12
13
14

(defadvice isearch-forward-regexp (around sycao/search-region)
"if there is an active region, search from it directly"
(if (region-active-p)
(progn
(let ((beg (region-beginning))
(end (region-end)))
(deactivate-mark)
(isearch-mode t t nil nil)
(isearch-yank-string (buffer-substring-no-properties beg end))))
ad-do-it))

(ad-activate 'isearch-forward-regexp)

node exec buffer problem

最近遇到一个诡异的问题,由于代码逻辑过于复杂,一开始还没想到是这个问题。
后来使用强大的node-inspector大致定位到出错的位置,经过仔细分析,
发现这个问题竟然出在child_process.exec的buffer上。

出错的代码经过简化大致是这样的:

exec_bad.jsview raw
1
2
3
4
5
6
var exec = require('child_process').exec;

var cmd = exec('mkfs.ext4 /dev/sdb2');
cmd.stdout.on('exit', function(extcode, signal) {
console.log('exit with ', extcode, signal);
});

一般没有问题,但是如果sdb2的分区大小足够大,当格式化正在进行的过程中,会收到SIGTERM退出。
原因其实很简单,就是exec有一个内部的buffer来缓冲输出,默认的buffer大小是200K,所以当
输出大小超过时就被kill了。看child_process的代码就很清楚了:

这是execFile函数的一部分,基本上就是buffer了所有的输出,在child退出时传递给callback。

1
2
3
4
5
6
7
8
9
10
11

child.stdout.setEncoding(options.encoding);
child.stderr.setEncoding(options.encoding);

child.stdout.addListener('data', function(chunk) {
stdout += chunk;
if (stdout.length > options.maxBuffer) {
err = new Error('maxBuffer exceeded.');
kill();
}
});

所以只要传递一个更大的buffer就可以了:

exec_well.jsview raw
1
2
3
4
5
6
var exec = require('child_process').exec;

var cmd = exec('mkfs.ext4 /dev/sdb2', {maxBuffer: 1<<20});
cmd.stdout.on('exit', function(extcode, signal) {
console.log('exit with ', extcode, signal);
});

但是这种方法不容易扩展,所以如果需要更灵活的处理,用spawn比较好。

a pattern for creating js object

在看一些node模块的代码的时候,发现很多都会使用一种奇怪的方式构造对象。
比如下面这个:

1
2
3
4
5
6
7
8
function dnode (wrapper) {
if (!(this instanceof dnode)) return new dnode(wrapper);

this.proto = protocol(wrapper);
this.stack = [];
this.streams = [];
return this;
}

代码来自dnode,它奇怪的地方是if (!(this instanceof dnode)) return new dnode(wrapper);
这条语句。
为什么要这么写呢:其实理由很简单,为了避免js程序员们忘记在调用构造函数时前面家new而导致
污染了全局对象。
所以在构造时留了个心眼,如果this求值不是dnode实例,那就说明你忘记加new了,
这时候上面的保险做法可以保证正确的执行。

再比如下面这个例子(从node-glob摘抄过来):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Glob (pattern, options, cb) {
if (!(this instanceof Glob)) {
return new Glob(pattern, options, cb)
}

if (typeof cb === "function") {
this.on("error", cb)
this.on("end", function (matches) {
// console.error("cb with matches", matches)
cb(null, matches)
})
}
...
}

更新:dailyjs上提到ECMAScript 5.1计划让builtin的构造函数支持类似的用法。

connect middleware introduction

connect是一个node的开发框架,connect作者对它做了介绍
connect的精华就在于middleware堆栈,它允许你在一个请求到达或者对请求进行回复之前
对过程进行微调。比如,你可以写一个logger,追踪所有的URL请求,可以解析请求中的
query或数据等,还可以在发送回复之前对数据进行压缩以节省带宽。connect
默认已经提供了很丰富的middleware,也有许多第三方的middleware可供使用,必要
的时候也可以自己编写。由于connect是建立在nodejs的http模块基础上的,因此倚赖
http模块的实现,而且得很熟悉才行。

要注意的是现在express 2.x使用了connect 1.x版本,这个版本缺少一些middleware,
比如compress。而connect现在已经到了2.3.3版本,最近好像3.0也已见端倪,这几个
版本之间的middleware不一定兼容,所以交叉使用的时候可能会出现问题。

比如我从connect 1.8.7拿过来的compress在使用过程中
发现有时候会出现Can't ... after they are sent.的错误,于是我在拷贝
过来的compress中做了一点改动,对connect的patch再次做了hack。

1
2
3
4
5
6
7
8
9
10
11
12
//Hack, override connect 1.x patch
var http = require('http')
, _resp = http.OutgoingMessage.prototype;

(function() {
if (_resp._compressPatched) return;
_resp.__defineGetter__('headerSent', function(){
return this._header || this._headerSent;
});
_resp._compressPatched = true;

}());