Javascript теряет контекст при рекурсивном подключении - PullRequest
6 голосов
/ 28 мая 2011

Я начинаю работать над инструментом динамического анализа для 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

Когда я выполняю $("p.neat").addClass("ohmy").show("slow"); на jQuery.com (через 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

.. вместо animation, show, merge и т. Д. Прямо сейчас все, что нужно сделать, это сказать called functionName, но в конце концов я бы хотел выполнять трассировку стека и функции времени (через Java апплет).

Этот вопрос оказался огромным, и я прошу прощения, но любая помощь приветствуется!

Примечание. Приведенный выше код может привести к сбою браузера, если вы не будете осторожны. Справедливое предупреждение: P

1 Ответ

3 голосов
/ 28 мая 2011

Я думаю, вы на правильном пути. Значение 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, этот контекст теряется. Firebug показывает:

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!

Корень вашей проблемы IMHO в том, что вы устанавливаете явное значение для this (т.е. parent, которое относится к родительскому объекту). Таким образом, ваша функция ловушки в итоге перезаписывает значение this, которое могло быть явно установлено любым кодом, вызывающим исходную функцию. Конечно, этот код не знает, что вы обернули исходную функцию своей собственной функцией ловушки. Таким образом, ваша функция ловушки должна сохранять значение this при вызове исходной функции:

return fn.apply(this, arguments);

Так что если вы используете this вместо parent в вашей заявке, ваша проблема может быть решена.

Я прошу прощения, если я не правильно понял вашу проблему. Пожалуйста, поправьте меня, где я не прав.

UPDATE

В 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, и функции, которые вызываются через цепочку (или просто вызываются напрямую).

...