Javascript Thunk深入解析

Thunk函数的诞生是源于一个编译器设计的问题:求值策略,即函数的参数到底应该何时求值。

例如:

1
2
3
4
5
var x = 1;
function f(m) {
return m * 2;
}
f(x + 5);

其中x+5这个表达式应该什么时候求值,有两种思路

  1. 传值调用(call by value),即在进入函数体之间,先计算x+5的值,再将这个值(6)传入函数f,例如c语言,这种做法的好处是实现比较简单,但是有可能会造成性能损失。
  2. 传名调用(call by name),即直接将表达式(x+5)传入函数体,只在用到它的时候求值。

Thunk函数的含义

编译器的传名调用实现,往往就是将参数放到一个临时函数之中,再将这个临时函数转入函数体,这个临时函数就叫做Thunk函数。

来看一段代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function f(m) {
return m*2;
}
f(x + 5);
// 等价于以下代码
var thunk = function () {
return x + 5;
};
function f(thunk) {
return thunk() * 2;
}

javascript中的Thunk函数

我们都知道JavaScript是传值调用的,那么js中的Thunk函数又是怎么回事?

在Javascript语言中,Thunk函数替换的不是表达式,而是多参数函数,将其替换成单参数的版本,且只接受回调函数作为参数。

还是通过代码来理解,即

1
2
3
4
5
6
// 正常版本的readFile,需要两个参数filename、callback
fs.readFile(fileName, callback);
// thunk版本的readFile
var readFileThunk = thunkify(fs.readFile);
readFileThunk(fileName)(callback);

原文中例子就是柯里化,预置参数fileName,直接调用fs.readFile

好,现在我们来思考如何实现thunkify函数。我们从调用的形式来看,返回的应该是一个高阶函数,即返回一个函数a,a的返回还是一个函数。

1
2
3
4
5
6
7
var thunkify = function (fn) {
return function () {
return function () {
}
}
};

结合上述例子,因为是包装函数,因此最终还是readFile执行,且需要fileName,因此:

1
2
3
4
5
6
7
8
9
var thunkify = function (fn) {
return function () {
var args = Array.prototype.slice.call(arguments);
return function (callback) {
args.push(callback);
return fn.apply(this, args);
}
}
};

这样似乎很完美,我们运行整个示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const fs = require('fs');
var thunkify = function (fn) {
return function () {
var args = Array.prototype.slice.call(arguments);
return function (callback) {
args.push(callback);
return fn.apply(this, args);
}
}
};
var readFileThunk = thunkify(fs.readFile);
readFileThunk('test.txt', 'utf-8')( (err, data) => {
console.log(data);
});

运行结果为

打造thunkify模块

要写出一个健壮的thunkify函数,需要考虑的各种情况,而我们通过tj大神写的thunkify模块的测试代码,来看看我们自己的thunkify还存在哪些不足,一步步来优化。

  1. 保存上下文的问题
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function load(fn) {
    fn(null, this.name);
    }
    var user = {
    name: 'tobi',
    load: thunkify(load)
    };
    user.load()((err, res) => {
    console.log(res);
    });

运行之后,res的结果为undefined,原因是没有保存上下文,改进一下

1
2
3
4
5
6
7
8
9
10
var thunkify = function (fn) {
return function () {
var args = Array.prototype.slice.call(arguments);
var ctx = this;
return function (callback) {
args.push(callback);
return fn.apply(ctx, args);
}
}
};

  1. 捕抓错误
    1
    2
    3
    4
    5
    function load(fn) {
    throw new Error('boom');
    }
    load = thunkify(load);
    load()(err => console.log(err.message));

运行之后,发现并没有捕抓到错误,我们需要执行函数进行try/catch,并且当出错时,传递出错信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var thunkify = function (fn) {
return function () {
var args = Array.prototype.slice.call(arguments);
var ctx = this;
return function (callback) {
args.push(callback);
var result;
// try/catch捕抓信息,并且出错时,传递给回调函数
try {
result = fn.apply(ctx, args);
} catch (e) {
callback(e);
}
return result;
}
}
};

  1. 回调函数应该只调用一次。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    function load(fn) {
    fn(null, 1);
    fn(null, 2);
    fn(null, 3);
    }
    load = thunkify(load);
    load()((err,ret) => console.log(ret));

运行输出结果为1 2 3,而我们期望结果只为1,那么需要判断callback是否已经执行过了,使其只执行一次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var thunkify = function (fn) {
return function () {
var args = Array.prototype.slice.call(arguments);
var ctx = this;
return function (callback) {
var called;
// 对callback进行封装,使其只能执行一次。
args.push(function () {
if(called) return;
called = true;
callback.apply(null, arguments);
});
var result;
try {
result = fn.apply(ctx, args);
} catch (e) {
callback(e);
}
return result;
}
}
};

到这里,我们通过了所有的测试,完成了一个健壮的thunkify模块。

来源

Thunk-阮一峰

thunkify-tj

Thunk深入解析

文章目录
  1. 1. Thunk函数的含义
  2. 2. javascript中的Thunk函数
  3. 3. 打造thunkify模块
  4. 4. 来源
|