Paradoxis
Paradoxis

Reputation: 4708

Get object caller name by function call JavaScript

I'm writing a piece of code to easily save error logs in an object for debugging.

What I'm trying to achieve is to get the Object name from the function it was called from like so:

var MainObject = {
    test : function() {
        return MainObject.test.caller;
        // When called from MainObject.testcaller,
        // it should return MainObject.testcaller.
    },

    testcaller : function() {
       return MainObject.test(); // Should return MainObject.testcaller, Returns own function code.
    },

    anothercaller : function() {
       return MainObject.test(); // Should return MainObject.anothercaller, Returns own function code.
    }
}

However when I run this code it returns the function code from MainObject.testcaller.

JSFiddle example

Is there any way this is possible?

Update

After looking at Rhumborl's answer, I discovered that assigning the value through another function would lead it to point back at the function name without the object itself.

Code:

(function (name, func) {
    MainObject[name] = func;
})('invalid', function() {
    return MainObject.test("blah"); 
}); 

// This now points at invalid() rather than MainObject.invalid()

Updated fiddle

Upvotes: 0

Views: 390

Answers (2)

Rhumborl
Rhumborl

Reputation: 16609

You can use this trick at http://www.eriwen.com/javascript/js-stack-trace/ which throws an error, then parses the stack trace.

I have updated it for the latest versions of Firefox, Chrome and IE. Unfortunately it doesn't work well on my IE9 (and I haven't tested it on Opera).

function getStackTrace() {
    var callstack = [];
    var isCallstackPopulated = false;
    try {
        i.dont.exist += 0; //doesn't exist- that's the point
    } catch (e) {
        if (e.stack) { //Firefox/Chrome/IE11
            var lines = e.stack.split('\n');
            for (var i = 0, len = lines.length; i < len; i++) {
                var line = lines[i].trim();
                if (line.match(/^at [A-Za-z0-9\.\-_\$]+\s*\(/)) {
                    // Chrome/IE: "    at Object.MainObject.testcaller (url:line:char)"
                    var entry = line.substring(3, line.indexOf('(') - 1);
                    // Chrome appends "Object." to the front of the object functions, so strip it off
                    if (entry.indexOf("Object.") == 0) {
                        entry = entry.substr(7, entry.length);
                    }
                    callstack.push(entry);
                } else if (line.match(/^[A-Za-z0-9\.\-_\$]+\s*@/)) {
                    // Firefox: "MainObject.testcaller@url:line:char"
                    callstack.push(line.substring(0, lines[i].indexOf('@')));
                }
            }
            //Remove call to getStackTrace()
            callstack.shift();
            isCallstackPopulated = true;
        } else if (window.opera && e.message) { //Opera
            var lines = e.message.split('\n');
            for (var i = 0, len = lines.length; i < len; i++) {
                if (lines[i].match(/^\s*[A-Za-z0-9\-_\$]+\(/)) {
                    var entry = lines[i];
                    //Append next line also since it has the file info
                    if (lines[i + 1]) {
                        entry += lines[i + 1];
                        i++;
                    }
                    callstack.push(entry);
                }
            }
            //Remove call to getStackTrace()
            callstack.shift();
            isCallstackPopulated = true;
        }
    }
    if (!isCallstackPopulated) { //IE9 and Safari
        var currentFunction = arguments.callee.caller;
        while (currentFunction) {
            var fn = currentFunction.toString();
            var fname = fn.substring(fn.indexOf("function") + 8, fn.indexOf('')) || 'anonymous';
            callstack.push(fname);
            currentFunction = currentFunction.caller;
        }
    }
    return callstack;
}

var MainObject = {
    test: function (x) {
        // first entry is the current function (test), second entry is the caller
        var stackTrace = getStackTrace();
        var caller = stackTrace[1];
        return caller + "()";
    },

    testcaller: function () {
        return MainObject.test(1, null);
    }
}

function SomeFunction() {
  return MainObject.test("blah");
}

document.body.innerHTML += '<b style="color: red">' + MainObject.testcaller() + '</b>';

document.body.innerHTML += '<div>Calling SomeFunction() returns: <b style="color: red">' + SomeFunction() + '</b></div>';
MainObject.test() should return: <b style="color: blue">MainObject.testcaller()</b>
<hr />
MainObject.test() returns:

Updated fiddle here

Upvotes: 1

RobG
RobG

Reputation: 147363

There is a non–standard caller property of functions that returns the caller function, however that is a pointer to a function object and doesn't tell you the object it was called as a method of, or the object's name. You can get a reference to the function through arguments.callee.

There is also the obsolete arguments.caller, but don't use that. It also provides a reference to the calling function (where supported).

Once you have a reference to the calling function (if there is one), you then have the issue of resolving its name. Given that Functions are Objects, and objects can be referenced by multiple properties and variables, the concept of a function having a particular name is alluvial.

However, if you know that the function is a property of some object, you can iterate over the object's own enumerable properties to find out which one it is.

But that seems to be a rather odd thing to do. What are you actually trying to do? You may be trying to solve a problem that can be worked around in a much more robust and simpler way.

Edit

You can do what you want in a very limited way using the method described above for the case in the OP, however it is not robust or a general solution:

var mainObject = {
    test : function() {
      var obj = this;
      var caller = arguments.callee.caller;
      var global = (function(){return this}());
      var fnName, objName;

      for (var p in global) {
        if (global[p] === obj) {
          objName = p;
        }
      }

      for (var f in obj) {
        if (obj[f] === caller) {
          fnName = f;
        }
      }

      return objName + '.' + fnName;
    },

    testcaller : function() {
       return mainObject.test();
    },

    anothercaller : function() {
       return mainObject.test();
    }
}


console.log(mainObject.testcaller());    // mainObject.testcaller
console.log(mainObject.anothercaller()); // mainObject.anothercaller

but it's brittle:

var a = mainObject.anothercaller;
console.log(a()); // mainObject.anothercaller    

var b = {
  foo : mainObject.anothercaller
}

console.log(b.foo()); // mainObject.anothercaller

Oops.

Upvotes: 2

Related Questions