skip to Main Content

Note that this question focuses on comparing if two functions come from the same function definition. Not a general comparison of two objects (e.g. array, dates) in javascript.

In an example code like this:

function father(num) {
  function generateChild() {
     console.log(num);
  }
  return generateChild;
}

const child1 = father(1);
const child2 = father(2);

// Tests:

console.log(child1 == child2); // false

console.log(child1 === child2); // false (as expected)

console.log(child1.prototype===child2.prototype); // false

console.log(child1.prototype.constructor===child2.constructor.prototype); // false

console.log(Object.getOwnPropertyDescriptor(child1.prototype, 'constructor').value === Object.getOwnPropertyDescriptor(child2.prototype, 'constructor').value); // false 

console.log(child1.toString() == child2.toString()); // true. 

How can I know that child1 and child2 are the same generateChild function?

Even though the toString() example works. I don’t want to use toString here. Two functions could match their toString() value but be defined with the same code in two different places, thus, they are two different functions that happen to have the same code. e.g.:

function test(){console.log('test')}
function test(){console.log('test')}

test1.toString() === test2.toString() // this is true. but they are not really the same function as in the case of generateChild

Notice that this is specially complicated since the function is defined inside another function and runs in a closure. There are simple ways to compare functions such as Array.prototype.map === Array.prototype.map. This is true for example, but it is not the same case.

It is also interesting to note, that the browser devtools knows they have the same constructor defined in the same place. Constructors in the devtools seem to have a [[FunctionLocation]] that it is not available in runtime.


Note about object comparison and other questions:
Other questions such as How can I determine equality for two JavaScript objects? and similar focus on comparing two objects e.g. compare {a:1} == {a:1} or [1,2] == [1,2] which can be compared using key/value comparison or iterating through values, but this is something that can’t be done with functions.

Though very similar, tt differentiates from Javascript comparing if 2 functions are exact same because this function is defined inside another function.

3

Answers


  1. Some option could be compare 2 stack traces if an error is thrown inside a function.
    If a function accepts argument we could provide a throwing argument.
    Maybe somebody knows a better way how to throw the error.

    function father() {
        function generateChild(x, y) {
            return x * y;
        }
        return generateChild;
    }
    
    const same = [father(), father()].map(fn => {
        try {
        
            const proxy = new Proxy({}, { get(){ throw new Error('test') } });
            fn(proxy);
        
        } catch (e) {
            return e.stack.split('n')[2].trim();
        }
    }).every((item, i, arr) => console.log(item) || !i || item === arr[i - 1]);
    
    console.log('functions are the same:', same);
    Login or Signup to reply.
  2. It sounds like you want to know if the function came from the same source code.

    This is impossible to accomplish as not all JavaScript platforms can guarantee a unique identifier for source code origin. For example, the JavaScript engine itself can compile a function from a string at runtime using eval raising questions as to where the source code came from. With eval specifically, it may get marked with the location of the eval call that generated it, not somehow the location of where the source code string got its value, which doesn’t even make sense if it’s not a string literal in code somewhere.

    I think the closest thing you can get is to peel off the location of the code from the stack trace of an Error as explained in answers to this question. And then, since functions are objects, you can manually add a property that you will use to test the equivalence of two functions. This of course only works for functions that you have specifically vetted and manually define their origin. In situations where the stack trace doesn’t provide a sufficiently unique identifier, you can manually tag any function with an appropriate unique identifier.

    The code might look something like this:

    function father(num) {
      function generateChild() {
         console.log(num);
      }
      generateChild.equivalenceId = new Error().stack.split('n')[1];
      return generateChild;
    }
    
    function mother(num) {
      function generateChild() {
         console.log(num);
      }
      generateChild.equivalenceId = new Error().stack.split('n')[1];
      return generateChild;
    }
    
    const child1 = father(1);
    const child2 = father(2);
    const child3 = mother(1);
    const child4 = mother(2);
    
    function areEquivalent(child1, child2) {
      return child1?.equivalenceId !== undefined && child1?.equivalenceId === child2?.equivalenceId;
    }
    
    console.log(areEquivalent(child1, child2)); // true
    console.log(areEquivalent(child1, child3)); // false
    console.log(areEquivalent(child3, child4)); // true
    

    If performance is an issue then just tag it manually generateChild.equivalenceId = 'someUniqueString'

    Login or Signup to reply.
  3. This is more of a hack but it was fun to implement, the idea is to make the function to throw an exception and then extract the line number from the exception stack trace. the script accomplish this by passing parameters where the valueOf method throws an exception.

    The line number corresponds to the place where the exception was thrown not where the function was defined. but given that they are from within the function it will tell you that in fact the two are from the same function.

    this will only work if the function meet the following conditions:

    • it has parameters
    • they are used in a way that the valueOf method is called (math, ==, <, >…)

    The script uses a regex to extract the information, it may fail if the stack trace is in another format. It includes two regex for chrome and firefox

    function getFunctionLocation (fn) {
        var args = [];
        for (var i = 0; i < fn.length; i++) {
            args.push({valueOf: function() { throw new Error() }});
        }
    
        try {
            fn(...args);
        } catch(e) {
            // the error within the function is at the 2 line of the stack trace
            var parts = getLocationFromTrace(e.stack)
            if (parts) {
                return {
                    name: parts[1],
                    file: parts[2],
                    line: parts[3],
                    column: parts[4]
                }
            }
        }
    
        return null;
    }
    
    function getLocationFromTrace(trace) {
        trace = trace.split("n");
        var regexp = [
            /at ([^s]+) [(](.*?):(d+):(d+)[)]/, // chrome
            /(.*?)@(.*?):(d+):(d+)/, // firefox
        ];
        var stackStart = 0;
        
        // find line in which the stack trace start
        // This is required because in chrome the first line is the exception name
        // but firefox the first line is the first location of the trace
        for (var i = 0; i < trace.length; i++) {
            var match = regexp.find(function(r) {
                return r.exec(trace[i]);
            });
    
            if (match) {
                stackStart = i;
                break;
            }
        }
      
        // find the correct regex to extract the information
        var correctRegExp = regexp.find(function(r) {
            return r.exec(trace[stackStart + 1])
        });
    
        return correctRegExp.exec(trace[stackStart + 1]);
    }
    
    function father() {
        return function child(a) {
          return a * 2;
        }
    }
    
    function externalScript(a) {
        if (a < 10) {
            return 10
        }
    }
    
    console.log(getFunctionLocation(inlineScript))
    console.log(getFunctionLocation(externalScript));
    
    var child1 = father();
    var child2 = father();
    
    var location1 = getFunctionLocation(child1);
    var location2 = getFunctionLocation(child2);
    
    // Both functions come from the same location
    console.log(location1.file === location2.file && location1.line === location2.line)
    <script>
        function inlineScript(x) {
            return x + 2;
        }
    </script>
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search