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:
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.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.
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.
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.
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;
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:
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;
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
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()
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});
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With