Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Making a true copy of a function in Javascript

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).
like image 604
Joppy Avatar asked Jun 30 '21 23:06

Joppy


People also ask

What does () => mean in JavaScript?

It's a new feature that introduced in ES6 and is called arrow function. The left part denotes the input of a function and the right part the output of that function.

How do you copy properties from one object to another in JavaScript?

The Object.assign() method copies all enumerable own properties from one or more source objects to a target object. It returns the modified target object.

How to copy an object in JavaScript?

To copy an object in JavaScript, you have three options: 1 Use the spread ( ...) syntax. 2 Use the Object.assign () method. 3 Use the JSON.stringify () and JSON.parse () methods.

How to make a copy of a variable in JavaScript?

In JavaScript, you use variables to store values that can be primitive or references. When you make a copy of a value stored in a variable, you create a new variable with the same value. For a primitive value, you just simply use a simple assignment: let counter = 1 ; let copiedCounter = counter;

What is a deep copy in JavaScript?

So when the copy is mutated, the original also gets mutated. This is also known as shallow copying. If we instead want to copy an object so that we can modify it without affecting the original object, we need to make a deep copy . In JavaScript, we can perform a copy on objects using the following methods:

How to copy an object using assignment operator in JavaScript?

As a rule, the assignment operator doesn’t generate a copy of an object. It is capable of assigning a reference to it. Let’s check out the following code: let object = { a: 2 , b: 3 , }; let copy = object; object.a = 5 ; console .log (copy.a); // Result // a = 5;


3 Answers

  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.
like image 64
Leftium Avatar answered Oct 18 '22 01:10

Leftium


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()
like image 45
Hunq Vux Avatar answered Oct 17 '22 23:10

Hunq Vux


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});
like image 36
Ed_Johnsen Avatar answered Oct 18 '22 01:10

Ed_Johnsen