Javascript在递归挂钩时丢失上下文

时间:2011-05-27 21:28:43

标签: javascript recursion hook language-features

我开始研究JS的动态分析工具,我想不引人注目地分析整个环境。我基本上遍历各种上下文,深入挖掘对象,每次我点击一个函数,我都会陷入其中。现在,除了在处理jQuery / prototype等库时它会中断的事实,这种方法相对较好。

这是我迄今为止的代码(尽我所能评论):

var __PROFILER_global_props = new Array(); // visited properties

/**
* Hook into a function
* @name the name of the function
* @fn the reference to the function
* @parent the parent object
*/
function __PROFILER_hook(name, fn, parent) {
    //console.log('hooking ' + name + ' ' + fn + ' ' + parent);

    if (typeof parent == 'undefined')
        parent = window;

    for (var i in parent) {
        // find the right function
        if (parent[i] === fn) {
            // hook into it
            console.log('--> hooking ' + name);
                parent[i] = function() {
                        console.log('called ' + name);
                        return fn.apply(parent, arguments);
                }

                //parent[i] = fn; // <-- this works (obviously)
                break;
        }
    }
}

/**
* Traverse object recursively, looking for functions or objects
* @obj the object we're going into
* @parent the parent (used for keeping a breadcrumb)
*/
function __PROFILER_traverse(obj, parent) {
    for (i in obj) {
        // get the toString object type
        var oo = Object.prototype.toString.call(obj[i]);
        // if we're NOT an object Object or an object Function
        if (oo != '[object Object]' && oo != '[object Function]') {
            console.log("...skipping " + i);
            // skip
            // ... the reason we do this is because Functions can have sub-functions and sub-objects (just like Objects)
            continue;
        }
        if (__PROFILER_global_props.indexOf(i) == -1 // first we want to make sure we haven't already visited this property
            && (i != '__PROFILER_global_props'       // we want to make sure we're not descending infinitely
            && i != '__PROFILER_traverse'            // or recusrively hooking into our own hooking functions
            && i != '__PROFILER_hook'                // ...
            && i != 'Event'              // Event tends to be called a lot, so just skip it
            && i != 'log'                // using FireBug for debugging, so again, avoid hooking into the logging functions
            && i != 'notifyFirebug')) {          // another firebug quirk, skip this as well

            // log the element we're looking at
            console.log(parent+'.'+i);
            // push it.. it's going to end up looking like '__PROFILER_BASE_.something.somethingElse.foo'
            __PROFILER_global_props.push(parent+'.'+i);
            try {
                // traverse the property recursively
                __PROFILER_traverse(obj[i], parent+'.'+i);
                // hook into it (this function does nothing if obj[i] is not a function)
                __PROFILER_hook(i, obj[i], obj);
            } catch (err) {
                // most likely a security exception. we don't care about this.
            }
        } else {
            // some debugging
            console.log(i + ' already visited');
        }
    }
}

这是个人资料,这就是我调用它的方式:

// traverse the window
__PROFILER_traverse(window, '__PROFILER_BASE_');

// testing this on jQuery.com
$("p.neat").addClass("ohmy").show("slow");

遍历工作正常并且挂钩工作正常,只要函数简单且非匿名(我认为挂钩到匿名函数是不可能的,所以我不太担心它。)

这是预处理阶段的一些修剪输出。

notifyFirebug already visited
...skipping firebug
...skipping userObjects
__PROFILER_BASE_.loadFirebugConsole
--> hooking loadFirebugConsole
...skipping location
__PROFILER_BASE_.$
__PROFILER_BASE_.$.fn
__PROFILER_BASE_.$.fn.init
--> hooking init
...skipping selector
...skipping jquery
...skipping length
__PROFILER_BASE_.$.fn.size
--> hooking size
__PROFILER_BASE_.$.fn.toArray
--> hooking toArray
__PROFILER_BASE_.$.fn.get
--> hooking get
__PROFILER_BASE_.$.fn.pushStack
--> hooking pushStack
__PROFILER_BASE_.$.fn.each
--> hooking each
__PROFILER_BASE_.$.fn.ready
--> hooking ready
__PROFILER_BASE_.$.fn.eq
--> hooking eq
__PROFILER_BASE_.$.fn.first
--> hooking first
__PROFILER_BASE_.$.fn.last
--> hooking last
__PROFILER_BASE_.$.fn.slice
--> hooking slice
__PROFILER_BASE_.$.fn.map
--> hooking map
__PROFILER_BASE_.$.fn.end
--> hooking end
__PROFILER_BASE_.$.fn.push
--> hooking push
__PROFILER_BASE_.$.fn.sort
--> hooking sort
__PROFILER_BASE_.$.fn.splice
--> hooking splice
__PROFILER_BASE_.$.fn.extend
--> hooking extend
__PROFILER_BASE_.$.fn.data
--> hooking data
__PROFILER_BASE_.$.fn.removeData
--> hooking removeData
__PROFILER_BASE_.$.fn.queue

当我在jQuery.com上执行$("p.neat").addClass("ohmy").show("slow");时(通过Firebug),我得到了一个合适的调用堆栈,但是我似乎在某个地方丢失了我的上下文,因为没有任何反应,我得到了e is undefined来自jQuery的错误(显然,挂钩搞砸了一些东西)。

called init
called init
called find
called find
called pushStack
called pushStack
called init
called init
called isArray
called isArray
called merge
called merge
called addClass
called addClass
called isFunction
called isFunction
called show
called show
called each
called each
called isFunction
called isFunction
called animate
called animate
called speed
called speed
called isFunction
called isFunction
called isEmptyObject
called isEmptyObject
called queue
called queue
called each
called each
called each
called each
called isFunction
called isFunction

问题是我在调用

时失去this上下文
return fn.apply(parent, arguments);

这是另一个有趣的怪癖。如果我在穿越之前挂钩,即:

        // hook into it (this function does nothing if obj[i] is not a function)
        __PROFILER_hook(i, obj[i], obj);
        // traverse the property recursively
        __PROFILER_traverse(obj[i], parent+'.'+i);

..应用程序运行绝对正常,但由于某种原因,调用堆栈被更改(我似乎没有获得特定于jQuery的函数):

called $
called getComputedStyle
called getComputedStyle
called getComputedStyle
called getComputedStyle
called getComputedStyle
called getComputedStyle
called setInterval
called getComputedStyle
called getComputedStyle
called getComputedStyle
called getComputedStyle
called getComputedStyle
called getComputedStyle
called getComputedStyle
called getComputedStyle
called getComputedStyle
called getComputedStyle
called getComputedStyle
called getComputedStyle
called getComputedStyle
called getComputedStyle
called getComputedStyle
called getComputedStyle
called getComputedStyle
called clearInterval

..而不是animationshowmerge等。现在,所有的钩子都是called functionName,但最终我想做堆栈跟踪和时间函数(通过Java小程序)。

这个问题最终变得很大,我很抱歉,但感谢任何帮助!

注意:如果您不小心,上面的代码可能会导致浏览器崩溃。公平警告:P

1 个答案:

答案 0 :(得分:3)

我认为你走在正确的轨道上。当您使用this时,apply的值会被破坏。 jQuery中定义的函数可能在内部通过apply调用,并且取决于this的值。

apply的第一个参数是将用于this的值。你确定你应该使用parent吗?

我能够以下列方式复制问题:

var obj = {
   fn : function() { 
      if(this == "monkeys") {
         console.log("Monkeys are funny!");
      }

      else {
         console.log("There are no monkeys :(");
      }
   }
};

obj.fn.apply("monkeys");

var ref = obj.fn;

//assuming parent here is obj
obj.fn = function() {
   console.log("hooking to obj.fn");
   return ref.apply(obj);
};

obj.fn.apply("monkeys");

此处,函数取决于this的值以打印文本Monkeys are funny!。如您所见,使用hook算法,此上下文将丢失。萤火虫表明:

Monkeys are funny!
hooking to obj.fn
There are no monkeys :(

我做了一些小改动,并在申请中使用了this而不是obj(父母):

obj.fn = function() {
   console.log("hooking to obj.fn");
   return ref.apply(this);
};

这次Firebug说:

Monkeys are funny!
hooking to obj.fn
Monkeys are funny!

问题的根源恕我直言,你正在设置this的显式值(即parent引用父对象)。因此,您的钩子函数最终会覆盖this的值,这可能是由调用原始函数的任何代码显式设置的。当然,该代码不知道您使用自己的钩子函数包装原始函数。所以你的钩子函数应该在调用原始函数时保留this的值:

return fn.apply(this, arguments);

因此,如果您在申请中使用this而不是parent,则问题可能会得到解决。

如果我没有正确理解你的问题,我道歉。无论我哪里错了,请纠正我。

<强>更新

jQuery中有两种函数。附加到jQuery对象本身的那些(有点像静态方法)然后你有一些操作jQuery(selector)的结果(有点像实例方法)。这是你需要关注的后者。在这里,this非常重要,因为这就是你实现链接的方式。

我能够得到以下示例。请注意,我正在处理对象的实例,而不是对象本身。因此,在您的示例中,我将处理jQuery("#someId")而不仅仅是jQuery

var obj = function(element) {
   this.element = element;
   this.fn0 = function(arg) {
      console.log(arg, element);
      return this;
   }

   this.fn1 = function(arg) {
      console.log(arg, arg, element);
      return this;
   }

   if(this instanceof obj) {
      return this.obj;
   }

   else {
      return new obj(element);
   }
};

var objInst = obj("monkeys");

var ref0 = objInst.fn0;

objInst.fn0 = function(arg) {
   console.log("calling f0");
   return ref0.apply(this, [arg]);
};

var ref1 = objInst.fn1;

objInst.fn1 = function(arg) {
   console.log("calling f1");
   return ref1.apply(this, [arg]);
};

objInst.fn0("hello").fn1("bye");

我不知道这是否能解决你的问题。也许查看jQuery源代码会给你一些更多的见解:)。我认为你难以区分通过apply调用的函数和通过链接调用的函数(或者只是直接调用)。