GreaseMonkey这个插件大家都早已熟悉了。最近我遇到一个问题:需要让页面在调用完某个函数之后自动执行我的函数。 其实这个并不难,写个函数替换原有的函数即可:

function hook() {
  var f = unsafeWindow.foo;         // 保存旧函数
  unsafeWindow.foo = function() {   // 定义新函数
    alert("Hello!");                // 先执行我们的处理
    f();                            // 再执行旧函数
  }
}

然后加载到页面上:

setTimeout(hook, 1000);

这样,页面再执行foo函数时,就会先执行我们的alert("Hello!")了。

不过这个函数还有很大的问题。比如,原有函数的参数不能正确传给foo,返回值无法取出来,无法应用到对象中的方法,通用性不好等。 下面来一个个解决。

返回值的问题比较好办,让它return一下就行了:

function hook() {
  var f = unsafeWindow.foo;         // 保存旧函数
  unsafeWindow.foo = function() {   // 定义新函数
    alert("Hello!");                // 先执行我们的处理
    return f();                     // 再执行旧函数
}

对象方法的意思是,如果要hook的函数不是全局函数,而是某个对象的方法的话,上述函数就无法正确执行。

var object = {
  value: 10,
  foo: function () { alert("Foo!" + this.value); }
};
object.foo();

此时如果只是简单修改一下最初的GreaseMonkey脚本,改成这样的话:

// 注意这种实现方法是错误的!
function hook() {
  var f = unsafeWindow.object.foo;         // 保存旧函数
  unsafeWindow.object.foo = function() {   // 定义新函数
    alert("Hello!");                       // 先执行我们的处理
    return f();                            // 再执行旧函数
  }
}

那么执行foo()时你会发现,本应输出”Foo!10“,但实际输出结果变成了”Foo!undefined“。 这是因为临时保存下来的 f 函数在执行时没有绑定到unsafeWindow.object上,因此找不到this.value

解决方法是在调用 f 时使用 apply:

// 正确的方法
function hook() {
  var f = unsafeWindow.object.foo;         // 保存旧函数
  unsafeWindow.object.foo = function() {   // 定义新函数
    alert("Hello!");                       // 先执行我们的处理
    return f.apply(unsafeWindow.object);   // 再执行旧函数
  }
}

有了apply,参数问题也迎刃而解:

function hook() {
  var f = unsafeWindow.object.foo;                    // 保存旧函数
  unsafeWindow.object.foo = function() {              // 定义新函数
    alert("Hello!");                                  // 先执行我们的处理
    return f.apply(unsafeWindow.object, arguments);   // 再执行旧函数
  }
}

最后的问题就是这个函数通用性还不好。代码中把要hook的函数(unsafeWindow.object.foo)写死了, 这样每hook一个函数都要写专用的函数才行。下面试着让它通用一些:

/**
 * hook系统中的已有函数
 * @param object {Object} 要hook的函数所在的对象
 * @param name {String} 要hook的函数名称
 * @param pre {Function} 原函数执行前要执行的动作
 * @param post {Function} 原函数执行后要执行的动作
 * @return 旧函数的返回值
 */
function hook(object, func, pre, post) {
  var f = object[func];                       // 保存旧函数
  object[func] = function() {                 // 定义新函数
    if (pre) pre.apply(window, arguments);    // 旧函数执行前执行的自定义函数
    var ret = f.apply(object, arguments);     // 执行旧函数
    if (post) post.apply(window, arguments);  // 旧函数执行后执行的自定义函数
    return ret;
  }
}

这样这个函数就相当通用了。在hook时需要执行以下代码:

setTimeout(function() {
  hook(unsafeWindow.object, 
       "foo", 
       function(){alert("pre")}, 
       function(){alert("post")}
  );
}, 1000);

当然上述函数还有可以改进的地方。如调用pre和post时直接apply到了window上, 这样如果在hook时给出的pre和post不是全局函数而是某个对象的方法的话, 函数就无法正常执行了。不过对于一般的应用来说,上面的方法已经足够了吧。