20

I would like to define two functions (or classes) in Javascript with the exact same function body, but have them be completely different objects. The use-case for this is that I have some common logic in the body which is polymorphic (the function can accept multiple types), but by only calling the function with a single type the function ends up faster, I assume since the JIT can take a happier fast path in each case.

One way of doing this is simply to repeat the function body entirely:

function func1(x) { /* some body */ }
function func2(x) { /* some body */ }

Another way of accomplishing the same thing with less repetition is eval():

function func(x) { /* some body */ }
function factory() { return eval("(" + func.toString() + ")") }
let func1 = factory(), func2 = factory()

The downside of eval() of course being that any other tools (minifiers, optimisers, etc) are completely taken by surprise and have the potential to mangle my code so this doesn't work.

Are there any sensible ways of doing this within the bounds of a standard toolchain (I use Typescript, esbuild, and Vite), without using eval() trickery or just copy-pasting the code? I also have the analagous question about class definitions.


Edit: to summarise what's been going on in the comments:

  1. Yes, the performance difference is real and measurable (especially on Chrome, less pronounced on Firefox and Safari), as demonstrated by this microbenchmark. The real program motivating this question is much larger and the performance differences are much more pronounced, I suspect because the JIT can do more inlining for monomorphic functions, which has many knock-on effects.
  2. The obvious solution of returning a closure does not work, i.e.
    function factory() { function func() { /* some body */ } return func }
    let func1 = factory(), func2 = factory()
    
    as demonstrated by this second microbenchmark. This is because a JIT will only compile a function body once, even if it is a closure.
  3. It may be the case that this is already the best solution, at least when working within a standard JS/Typescript toolchain (which does not include code-generation or macro facilities).
Joppy
  • 363
  • 2
  • 12
  • 5
    > by only calling the function with a single type the function ends up faster - any source on that? Always having the same number of arguments [does help](https://v8.dev/blog/adaptor-frame)., but I'm really confused by what you mean by 'single type' here. – raina77ow Jun 30 '21 at 23:22
  • 3
    Instead of a polymorphic function, why not different functions for each type? E.g. OOP. – Barmar Jun 30 '21 at 23:26
  • @raina77ow The actual case I had is this: I have some abstract class A, which is extended in two different ways to classes B and C. The logic inside A is highly non-trivial (it implements a hash table, B and C implement different backing stores for the keys and values). When benchmarking I found that if I only benchmarked B or only benchmarked C, each was fast. However if both are used, each only runs at half speed. This half speed issue gets rectified if I just copy A so that the base class B and C are extending are different. Alternatively there was this eval trick... – Joppy Jun 30 '21 at 23:29
  • 2
    @raina77ow Here is a silly benchmark showing that monomorphic functions execute faster: https://jsben.ch/xH0Iu . A function is called repeatedly on either an array of numbers or an array of strings to warm up the JIT, then only the call on an array of numbers is benchmarked. There is a clear performance hit in chrome, which can be removed by commenting out the part of the warm-up where the function is called on strings. – Joppy Jun 30 '21 at 23:55
  • 5
    move the function inside the factory and return it directly? `function factory() { return function func(x) { /* some body */ }}`. Saves you typing it twice or eval wrangling. – pilchard Jun 30 '21 at 23:58
  • @pilchard Yes this was my first attempt - unfortunately it does not solve the problem (see https://jsben.ch/VBmYo), but perhaps there is some variant of it that would. I think that the JIT creates a function object whenever it sees one in source, and re-uses that for calls - closures returned from functions would perform terribly if they needed to be re-optimised every time. However in my case I want that re-optimisation to happen. – Joppy Jul 01 '21 at 00:07
  • 1
    Did you tried with func1.call()? – Aliaga Aliyev Jul 03 '21 at 21:18
  • What about using the Blob API and creating a second script source on the fly? 1. Define a function: `function myFunction(){ console.log("This is one of the functions:", arguments.callee.name); }`. 2. Create the copy: `const script = Object.assign(document.createElement("script"), { src: URL.createObjectURL(new Blob([ myFunction.toString().replace("myFunction", "myOtherFunction") ], { type: "text/javascript" })) });`. 3. Call the copy when the script is loaded: `script.addEventListener("load", () => myOtherFunction());`. 4. Append the script: `document.head.append(script);`. – Sebastian Simon Jul 03 '21 at 23:32
  • Perhaps the Blob API can also be used with [dynamic imports](//developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#dynamic_imports). – Sebastian Simon Jul 03 '21 at 23:37
  • @SebastianSimon Thanks, but that just looks like a far more complicated version of `eval("(" + func.toString() + ")")`, which still uses `func.toString()`. – Joppy Jul 04 '21 at 00:34
  • 4
    I looked at your benchmark and I accept your premise that individual copies of functions are more performant (slightly, it would seem). But what effect will this have in improving your application's overall performance and at what price considering the added complexity? **"Premature optimization is the root of all evil." - Sir Tony Hoare** – Booboo Jul 04 '21 at 11:23
  • 2
    @Booboo I appreciate your concern, but keep in mind that the benchmark I posted above is just to convince the reader that there *is* a difference. The real use-case I have is some base class implementing a hashtable which is extended by different classes for different key types. Using this eval trick (or just copy-pasting the whole base class so each copy is only extended in one direction) gives me a 3x or 4x speedup overall in the code that uses the hash table. I find it silly to be copy-pasting a few hundred lines of "BaseHashTable" and renaming it "BaseHashTable2", hence the question. – Joppy Jul 04 '21 at 11:33
  • 1
    Instead of trying to micro-optimise the js code by duplicating function code, I would suggest to report a performance bug against V8. – Bergi Jul 04 '21 at 11:46
  • 2
    Related: https://mrale.ph/blog/2012/09/23/grokking-v8-closures-for-fun.html and https://bugs.chromium.org/p/v8/issues/detail?id=2206 – Bergi Jul 04 '21 at 11:46
  • 1
    @Bergi: Thanks, but this happens in Chrome, Firefox, and Safari, and I am interested in addressing the problem in all three at once (which copy-paste or this eval trick does). What's more is that I don't think this is a performance bug as much as a sensible performance decision which these implementations have made (roughly akin to virtual function calls rather than templating, if we were talking C++), and I'm wondering if there is some escape hatch to get around it. – Joppy Jul 04 '21 at 12:21
  • Does this not solve your issue? https://stackoverflow.com/a/1833851/2367338 – Kim Skogsmo Jul 04 '21 at 13:42
  • @KimSkogsmo No, the compiled code is still shared in that solution – Bergi Jul 04 '21 at 14:33

5 Answers5

5
  1. Use the file system to make a single ESM module file appear as multiple different files.
  2. Then import your function multiple times.

The only toolchain requirement is esbuild, but other bundlers like rollup will also work:

Output:

// From command: esbuild  main.js --bundle
(() => {
  // .func1.js
  function func(x2) {
    return x2 + 1;
  }

  // .func2.js
  function func2(x2) {
    return x2 + 1;
  }

  // main.js
  func(x) + func2(x);
})();



// From command: rollup main.js
function func$1(x) { return x+1 }

function func(x) { return x+1 }

func$1(x) + func(x);

With these files as input:

// func.js
export function func(x) { return x+1 }
// main.js
import {func as func1} from './.func1';
import {func as func2} from './.func2';

func1(x) + func2(x)

The imported files are actually hard links to the same file generated by this script:

#!/bin/sh
# generate-func.sh

ln func.js .func1.js
ln func.js .func2.js

To prevent the hard links from messing up your repository, tell git to ignore the generated hard links. Otherwise, the hard links may diverge as separate files if they are checked in and checked out again:

# .gitignore
.func*

Notes

  • I put everything in the same folder for simplicity, but you can generate the hard links in their own folder for organization.
  • Rollup will "see through" this trick if you use symlinks to the same JS file. However symlinks to directories work fine.
  • Tested on git-bash for Windows; YMMV on other platforms.
Leftium
  • 16,497
  • 6
  • 64
  • 99
  • Note you cannot have a comment before the shebang in the script. – 0xLogN Jul 04 '21 at 17:44
  • @Joppy: I updated my answer with details for esbuild. (And added a short summary at the top.) – Leftium Jul 05 '21 at 00:43
  • Thanks! This is not quite the solution I was looking for, but I have the feeling it will win the most creative answer hands-down. – Joppy Jul 05 '21 at 11:01
2

Playing around with the Function constructor function, I guess that this would do the job

function func(x) { /* some body */ }
function factory() {
    return (
        new Function('return ' + func.toString())
    )();
}

let func1 = factory(), func2 = factory()
Hunq Vux
  • 35
  • 2
  • 7
1

Here's a vanilla, ES5 solution: (it must be declared globally... and only works on functions that reference globally-reachable content)

function dirtyClone(class_or_function){
    
  if(typeof class_or_function !== "function"){

    console.log("wrong input type");

    return false;
  }


  let stringVersion = class_or_function.toString();

  let newFunction = 'dirtyClone.arr.push(' + stringVersion + ')';


  let funScript = document.createElement("SCRIPT");

  funScript.text = newFunction;

  document.body.append(funScript);

  funScript.remove();


  let last = dirtyClone.arr.length-1;

  dirtyClone.arr[last].prototype = class_or_function.prototype;

  return dirtyClone.arr[last];
}
dirtyClone.arr = [];



// TESTS
class Animal {
  constructor(name) {
    this.name = name;
  }

  speak() {
    console.log(`${this.name} makes a noise.`);
  }
}

class Dog extends Animal {
  constructor(name) {
    super(name); // call the super class constructor and pass in the name parameter
  }

  speak() {
    console.log(`${this.name} barks.`);
  }
}

function aFunc(x){console.log(x);}

let newFunc = dirtyClone(aFunc);
newFunc("y");

let newAni = dirtyClone(Animal);
let nA = new newAni("person");
nA.speak();

let newDog = dirtyClone(Dog);
let nD = new newDog("mutt");
nD.speak();

console.log({newFunc});
console.log({newAni});
console.log({newDog});

Also, just in case your original function has deep properties (no need for global declaration... but it still only works on functions that reference content that is reachable from the global scope).

let dirtyDeepClone = (function(){
    // Create a non-colliding variable name  
    // for an array that will hold functions.
    let alfUUID = "alf_" + makeUUID();
    
    // Create a new script element.
    let scriptEl = document.createElement('SCRIPT');
    
    // Add a non-colliding, object declaration 
    // to that new script element's text.
    scriptEl.text = alfUUID + " = [];";
    
    // Append the new script element to the document's body
    document.body.append(scriptEl);
                

    // The function that does the magic
    function dirtyDeepClone(class_or_function){
      
        if(typeof class_or_function !== "function"){

            console.log("wrong input type");

            return false;
        }

        
        let stringVersion = class_or_function.toString();
        
        let newFunction = alfUUID + '.push(' + stringVersion + ')';
    
        
        let funScript = document.createElement("SCRIPT");

        funScript.text = newFunction;
        
        document.body.append(funScript);
    
        funScript.remove();
        
        
        let last = window[alfUUID].length-1;
        
        window[alfUUID][last] = extras(true, class_or_function, window[alfUUID][last]);
      
        window[alfUUID][last].prototype = class_or_function.prototype;
        
        return window[alfUUID][last];
    }



    ////////////////////////////////////////////////
    // SUPPORT FUNCTIONS FOR dirtyDeepClone FUNCTION
    function makeUUID(){
        
        // uuid adapted from: https://stackoverflow.com/a/21963136
        var lut = []; 
        
        for (var i=0; i<256; i++)
            lut[i] = (i<16?'0':'')+(i).toString(16);
        
        
        var d0 = Math.random()*0xffffffff|0;
        var d1 = Math.random()*0xffffffff|0;
        var d2 = Math.random()*0xffffffff|0;
        var d3 = Math.random()*0xffffffff|0;
        
        
        var UUID = lut[d0&0xff]+lut[d0>>8&0xff]+lut[d0>>16&0xff]+lut[d0>>24&0xff]+'_'+
        lut[d1&0xff]+lut[d1>>8&0xff]+'_'+lut[d1>>16&0x0f|0x40]+lut[d1>>24&0xff]+'_'+
        lut[d2&0x3f|0x80]+lut[d2>>8&0xff]+'_'+lut[d2>>16&0xff]+lut[d2>>24&0xff]+
        lut[d3&0xff]+lut[d3>>8&0xff]+lut[d3>>16&0xff]+lut[d3>>24&0xff];
        
        return UUID;
    }
  
  
  // Support variables for extras function
    var errorConstructor = {
        "Error":true,
        "EvalError":true,
        "RangeError":true,
        "ReferenceError":true,
        "SyntaxError":true,
        "TypeError":true,
        "URIError":true
    };
    var filledConstructor = {
        "Boolean":true,
        "Date":true,
        "String":true,
        "Number":true,
        "RegExp":true
    };
    var arrayConstructorsES5 = {
        "Array":true,
        "BigInt64Array":true,
        "BigUint64Array":true,
        "Float32Array":true,
        "Float64Array":true,
        "Int8Array":true,
        "Int16Array":true,
        "Int32Array":true,
        "Uint8Array":true,
        "Uint8ClampedArray":true,
        "Uint16Array":true,
        "Uint32Array":true,
    };
    var filledConstructorES6 = {
        "BigInt":true,
        "Symbol":true
    };


    function extras(top, from, to){
        
        // determine if obj is truthy 
        // and if obj is an object.
        if(from !== null && (typeof from === "object" || top) && !from.isActiveClone){
            
            // stifle further functions from entering this conditional
            // (initially, top === true because we are expecting that to is a function)
            top = false; 
            
            // if object was constructed
            // handle inheritance,
            // or utilize built-in constructors
            if(from.constructor && !to){

                let oType = from.constructor.name;


                if(filledConstructor[oType])
                    to = new from.constructor(from);

                else if(filledConstructorES6[oType])
                    to = from.constructor(from);

                else if(from.cloneNode)
                    to = from.cloneNode(true);

                else if(arrayConstructorsES5[oType])
                    to = new from.constructor(from.length);

                else if ( errorConstructor[oType] ){

                    if(from.stack){

                        to = new from.constructor(from.message);

                        to.stack = from.stack;
                    }

                    else
                        to = new Error(from.message + " INACCURATE OR MISSING STACK-TRACE");
                    
                }

                else // troublesome if constructor is poorly formed
                    to = new from.constructor(); 
                
            }
            
            else // loses cross-frame magic
                to = Object.create(null); 

            
            
            
            let props = Object.getOwnPropertyNames(from);

            let descriptor;


            for(let i in props){

                descriptor = Object.getOwnPropertyDescriptor( from, props[i] );
                prop = props[i];

                // recurse into descriptor, if necessary
                // and assign prop to from
                if(descriptor.value){

                    if(
                      descriptor.value !== null && 
                      typeof descriptor.value === "object" &&
                      typeof descriptor.value.constructor !== "function"
                    ){
                          from.isActiveClone = true;
                          to[prop] = extras(false, from[prop]);
                          delete from.isActiveClone;
                        
                    }
                  else
                        to[prop] = from[prop];
                }
                else
                    Object.defineProperty( to, prop, descriptor );
            }
        }
      
        else if(typeof from === "function")
            return dirtyDeepClone(from);
        
        return from;
    }
    
    return dirtyDeepClone;
})();



// TESTS
class Animal {
  constructor(name) {
    this.name = name;
  }

  speak() {
    console.log(`${this.name} makes a noise.`);
  }
}

class Dog extends Animal {
  constructor(name) {
    super(name); // call the super class constructor and pass in the name parameter
  }

  speak() {
    console.log(`${this.name} barks.`);
  }
}

function aFunc(x){console.log(x);}
aFunc.g = "h";
aFunc.Fun = function(){this.a = "b";}

let newFunc = dirtyDeepClone(aFunc);
newFunc("y");
let deepNewFunc = new newFunc.Fun();
console.log(deepNewFunc);

let newAni = dirtyDeepClone(Animal);
let nA = new newAni("person");
nA.speak();

let newDog = dirtyDeepClone(Dog);
let nD = new newDog("mutt");
nD.speak();

console.log({newFunc});
console.log({newAni});
console.log({newDog});
Ed_Johnsen
  • 136
  • 8
  • This approach seems fine, but I don't understand why you'd need UUIDs? Just use an indexed array, or let the caller of the `dirtyClone` method specify a name if they care. – Bergi Jul 09 '21 at 17:07
  • @Bergi The use of uuids ensures zero collisions without any further thought on the part of the developer. Names given to variables that receive the output of this cloning function are controlled by the dev, and outputs can be pushed to an array or wherever they prefer. – Ed_Johnsen Jul 09 '21 at 17:16
  • `array.push()` ensures zero collisions as well but is much simpler :-) You also shouldn't need to distinguish between `class` and `function` - whatever their `.toString()` returns is a valid expression. – Bergi Jul 09 '21 at 17:19
  • Bare bones, this solution declares a new function (or class) in the global space; that's why the function (or class) names themselves need to be non-colliding. Though .toString() returns a valid expression, redeclaration would overwrite the original, not clone it. The extra work done by this solution helps keep window pollution to a minimum; hence the non-colliding name of the object that will store newly declared functions or classes. While that object could instead be an array to which I could push functions, I prefer keynames that match function or class names. – Ed_Johnsen Jul 09 '21 at 17:52
  • "*this solution declares a new function (or class) in the global space*" - no it doesn't. It uses function (or class) *expressions* that it assigns to properties of that `olfUUID` object. The names of expressions cannot collide, there's no reason to patch their names (which likely breaks stuff). And even if you wanted to use declarations, you just could do that in a local scope and assign them to the global object from there. – Bergi Jul 09 '21 at 18:02
  • The magic comes from doing the "declaration" and assignment in one step in a new script element. If you try to do it locally, the string isn't parsed into a function; no new function is declared. Here is code that I think fits what you are thinking... it doesn't work:function dirtyClone(class_or_function){ let funcArr = []; let stringVersion = class_or_function.toString(); funcArr.push(stringVersion); return funcArr[0]; } function whoa(){console.log("nope");} whoa(); let newFunc = dirtyClone(whoa); newFunc(); // TypeError: newFunc is not a function – Ed_Johnsen Jul 09 '21 at 18:28
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/234706/discussion-between-ed-johnsen-and-bergi). – Ed_Johnsen Jul 09 '21 at 19:18
  • 1
    Thanks for the edit, this looks good now :-) You could further get rid of the global uuid-named variable by using `dirtyClone.olf = []` as the array (assuming of course that `dirtyClone` itself is a global variable). – Bergi Jul 09 '21 at 21:58
  • 1
    Sorry if I'm missing something, but what advantages does this approach have over `eval("(" + func.toString() + ")")`? It seems more verbose, and only runs in environments where scripts can be injected via the DOM. – Joppy Jul 09 '21 at 23:06
  • I don't think you're missing anything. The only advantage my solution has is that it isn't eval. The eval solution is thrice as fast (in latest chrome) and twice as fast (in latest Firefox), and works within closures when cloning a function that references other things in that same closure -- which mine does not. – Ed_Johnsen Jul 10 '21 at 00:46
  • @Joppy Perhaps the only advantage is that my ridiculous solution (in the situations where it can work) might work in firefox extensions, by default -- which eval definitely doesn't. https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/content_security_policy – Ed_Johnsen Jul 10 '21 at 01:29
  • This looks like basically the same as [my earlier comment](/q/68202541/4642212#comment120606038_68202541). – Sebastian Simon Jul 10 '21 at 06:20
0

Your idea of having a "factory" or a master function that would produce independent, physically separate, functions instead of referencing to the same one is a very good start...

In times before CSS animations, we had to use JavaScript for creating timed effects and so on. The idea of hovering over elements that would light them up, but as the mouse leaves that element hovering over the other, you'd want them to slowly fade out, ( in sort of leaving a smooth trail of light kind of fashion ), not abruptly over hundreds of elements, it would be impossible to do with a single function, whereas rewriting the same function body with slightly different names hundreds of times would be nonsensical, let alone assigning each function to the target element individually.

We faced the same problem...

To cut the story short, we had to go with a 'sensible solution' as you say, or drop the idea completely..., I didn't!

Here is a logic behind the (Factory) solution and a console log content to prove that these identical twin functions are physically separate (as required) not references to the same.

function Factory( x ){ return function( ){ console.log( x ) } };

func1 = new Factory("I'm the first born!");
func2 = new Factory("I'm the second born!");

func1(); func2();

Hope you find this solution sensible enough.

p.s.: You can add as many arguments you need to the Factory and provide their specific values during the creation of the functions which will be available throughout the session at all times, just as the console.log string we see in the demo is.

Regards.

Bekim Bacaj
  • 5,707
  • 2
  • 24
  • 26
  • 1
    This is just a standard closure. It does not solve the OP's problem, since the closures share the same code and the compiled code becomes polymorphic. – Bergi Jul 04 '21 at 17:52
  • Thanks, but this is the first thing I tried, and has already been mentioned in the comments. It does not solve the problem (there is even a benchmark in the comments to demonstrate that). – Joppy Jul 04 '21 at 23:06
  • @Bergi; @Joppy; as much as I would like to take you guys for your words, I think you will need to prove it. Could you please demonstrate that `func1` and `func2` are the same function you claim it to be. – Bekim Bacaj Jul 05 '21 at 02:02
  • @BekimBacaj https://i.stack.imgur.com/kTvpE.png https://mrale.ph/blog/2012/09/23/grokking-v8-closures-for-fun.html – Bergi Jul 05 '21 at 02:14
  • 2
    See also the benchmark that was linked in the comments (and is now in the post): https://jsben.ch/VBmYo. Running this in Chrome shows that the two functions coming from the factory perform the same (about 20% slower than the stand-alone function). If you comment out the part of the setup where one of the factory-output functions is warmed up on strings, you will get equal performance for all three. (This shows implicitly that the two functions coming from the factory are linked, but I found the screenshot from Bergi even more convincing). – Joppy Jul 05 '21 at 02:28
  • 1
    I was thinking a direct & immediate proof such as `func1 == func2 > true'. What browser vendor decides to do with functional code which it finds identical, is no longer a JavaScript problem. Every time a new Factory is executed, a new func is created and is not only separate and distinctive from any other but is also a discrete and lives in its own separate context. Now, this may pose a problem for you because the deeper the context, recursion becomes slower. Even pulling values from arguments rather than a variable makes a recursive function significantly slower. – Bekim Bacaj Jul 07 '21 at 11:21
-1

you can try this way:

function func1(x) { /* some body */ }

var func2 = new Function("x", func1.toString().match(/{.+/g)[0].slice(1,-1));

I are defining new function func2(x) using the function constructor where the first n-1 arguments are parameters and the last parameter is the function body

for function body I used regex to extract all the lines in the scope of func1 i.e. between the function braces { and }

you can read more about the Function constructor here

ahmedazhar05
  • 580
  • 4
  • 7
  • 1
    Thanks for the reply - this still has all the drawbacks of `eval()` though doesn't it? – Joppy Jul 01 '21 at 08:26