【问题标题】:Javascript losing context when hooking recursivelyJavascript在递归挂钩时丢失上下文
【发布时间】:2011-05-27 21:28:43
【问题描述】:

我开始为 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(通过 Firebug)上执行 $("p.neat").addClass("ohmy").show("slow"); 时,我得到了一个适当的调用堆栈,但我似乎在途中的某个地方丢失了我的上下文,因为什么也没发生,并且我从 jQuery 得到一个 e is undefined 错误(显然,挂钩搞砸了)。

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

【问题讨论】:

标签: javascript recursion hook language-features


【解决方案1】:

我认为你在正确的轨道上。当您使用 apply 时,this 的值会被破坏。 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 算法,此上下文会丢失。 Firebug 显示:

猴子很有趣!
连接到 obj.fn
没有猴子:(

我做了一点改动,在申请中使用了this,而不是obj(父级):

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

这一次 Firebug 说:

猴子很有趣!
连接到 obj.fn
猴子很有趣!

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

return fn.apply(this, arguments);

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

如果我没有正确理解您的问题,我深表歉意。有错误的地方请指正。

更新

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

我能够使以下示例正常工作。请注意,我正在处理对象的 instance 而不是对象本身。因此,在您的示例中,我将使用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 调用的函数和通过链接调用(或直接调用)的函数。

【讨论】:

  • 你完全理解它,我之前尝试过this,但是如果我使用this,函数链接似乎会中断(所以像$('something').addClass('asdfg').show('slow') 这样的东西会停止工作)。具体来说,我知道push() 是未定义的。
  • @David 我更新了我的答案。我不知道它是否回答了您的问题,但我试过了:p 我在答案中提到了这一点,但我认为当您尝试区分通过 @987654353 调用的函数时,您最终会遇到困难@ 与那些通过链接或直接调用的。我也有兴趣知道这个问题的解决方案,所以希望其他比我更有知识的人可以加入。
  • 即使在一般情况下它可能无法解决问题,但我的直觉告诉我这应该是可能的,我感谢您的洞察力。我认为当您区分方法链接和被调用的静态方法时,您一针见血。另外,做了一些试验,我发现有时调用 apply 会将参数包装在它们自己的数组中......这很奇怪。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2015-05-28
  • 2014-10-03
  • 2013-03-18
  • 2021-04-08
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多