hakobera's blog

技術メモ。たまに雑談

thunkify する時に注意すべきたった1つのこと

結論

thunkifyをオブジェクトのメソッドに適用する時は、元のメソッドに代入するか、thisを元のオブジェクトにbindすること。

fs.readFile = thunkify(fs.readFile)
// または
var readFile = thunkify(fs.readFile.bind(fs));
  • ただし、fs.readFilevar readFile = thunkify(fs.readFile.bind(fs));でも動きます。詳細は以下。
  • 元のオブジェクトに影響を与えない、ということを考えると後者の方が良いかも。

原因

thunkify は、Node で generator を使ったプログラムを書く時に便利ですが、1つ気をつけることがあります。

thunkify の実装は以下のようになっています。

function thunkify(fn){
  return function(){
    // 省略
    fn.apply(this, args);  // (1) 

    return function(fn){
      // 省略
    }
  }
};

上記の (1) の部分のthisが曲者で、

var obj = {
  func1: function (cb) {
    console.log('func1');
    this.func2(cb); // (2) 動かない!
  },

  func2: function (cb) {
     console.log('func2');
     cb(null, 'func2');
  }
};

こういうオブジェクトに対して、

var func1 = thunkify(obj.func1);

とやってしまうと、(2) の部分のthisがglobalスコープになってしまって、TypeError: Object #<Object> has no method 'func2'と言われてしまうのです。

ただし、fs.readFileのように内部でthisを利用しないメソッドは動いてしまうので、なかなか気が付きにくい。自分で書いたメソッドの場合は良いけど、ライブラリを利用する場合は、念の為に元のメソッドに代入するか、thisを元のオブジェクトにbindしておいた方が良さそう。

気がついた経緯から解決まで

Twitter で高速解決。

Thanks, @Jack_ さん、@KOBA789 さん。

参考資料

とてもわかりやすいジェネレータの解説

ジェネレータの解説と非同期への適用 - Block Rockin’ Codes

おまけ: エラー再現コード

var thunkify = require('thunkify');

var obj = {
  func1: function (cb) {
    console.log('func1');
    this.func2(cb); // (2) 動かない!
  },

  func2: function (cb) {
     console.log('func2');
     cb(null, 'func2');
  }
};

var func1 = thunkify(obj.func1);
//var func1 = thunkify(obj.func1.bind(obj)); // こっちならOK

function* gen() {
  yield func1();
};

var g = gen();
g.next();