Reputation: 403
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.Upvotes: 20
Views: 1925
Reputation: 156
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});
Upvotes: 1
Reputation: 37
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()
Upvotes: 2
Reputation: 17903
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
Upvotes: 5
Reputation: 5955
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.
Upvotes: 0
Reputation: 618
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
Upvotes: -1