JavaScript: clonar una función

115

¿Cuál es la forma más rápida de clonar una función en JavaScript (con o sin sus propiedades)?

Dos opciones que me vienen a la mente son eval(func.toString())y function() { return func.apply(..) }. Pero me preocupa que el rendimiento de eval y la envoltura empeore la pila y probablemente degradará el rendimiento si se aplica mucho o se aplica a la ya envuelta.

new Function(args, body) se ve bien, pero ¿cómo exactamente puedo dividir de manera confiable la función existente en args y body sin un analizador JS en JS?

Gracias por adelantado.

Actualización: lo que quiero decir es poder hacer

var funcB = funcA.clone(); // where clone() is my extension
funcB.newField = {...};    // without affecting funcA
Andrey Shchekin
fuente
¿Puede dar un ejemplo que muestre lo que quiere decir?
JoshBerke
Claro, agregó. (Se requieren 15 caracteres)
Andrey Shchekin
No estoy seguro, pero podría copiar = new your_function (); ¿trabajo?
Savageman
1
No lo creo, creará una instancia usando la función como constructor
Andrey Shchekin

Respuestas:

54

prueba esto:

var x = function() {
    return 1;
};

var t = function(a,b,c) {
    return a+b+c;
};


Function.prototype.clone = function() {
    var that = this;
    var temp = function temporary() { return that.apply(this, arguments); };
    for(var key in this) {
        if (this.hasOwnProperty(key)) {
            temp[key] = this[key];
        }
    }
    return temp;
};

alert(x === x.clone());
alert(x() === x.clone()());

alert(t === t.clone());
alert(t(1,1,1) === t.clone()(1,1,1));
alert(t.clone()(1,1,1));
Jared
fuente
Ok, entonces aplicar es la única forma? Mejoraría esto un poco para que no se envuelva dos veces cuando se llame dos veces, pero de lo contrario, está bien.
Andrey Shchekin
apply se utiliza para pasar los argumentos fácilmente. Además, esto funcionará para las instancias en las que desee clonar un constructor.
Jared
6
sí, escribí sobre aplicar en la publicación original. El problema es que esta función de envoltura destruye su nombre y se ralentizará después de muchos clones.
Andrey Shchekin
Parece haber una forma de afectar al menos la propiedad .name de esta manera: function fa () {} var fb = function () {fa.apply (this, argumentos); }; Object.defineProperties (fb, {nombre: {valor: 'fb'}});
Killroy
109

Aquí hay una respuesta actualizada

var newFunc = oldFunc.bind({}); //clones the function with '{}' acting as it's new 'this' parameter

Sin embargo, .bindes una característica moderna (> = iE9) de JavaScript (con una solución alternativa de compatibilidad de MDN )

Notas

  1. No clona las propiedades adjuntas adicionales del objeto de función , incluida la propiedad del prototipo . Crédito a @jchook

  2. La nueva función esta variable está atascada con el argumento dado en bind (), incluso en las nuevas llamadas de función apply (). Crédito a @Kevin

function oldFunc() {
  console.log(this.msg);
}
var newFunc = oldFunc.bind({ msg: "You shall not pass!" }); // this object is binded
newFunc.apply({ msg: "hello world" }); //logs "You shall not pass!" instead
  1. Objeto de función enlazado, instanceof trata newFunc / oldFunc como lo mismo. Crédito a @Christopher
(new newFunc()) instanceof oldFunc; //gives true
(new oldFunc()) instanceof newFunc; //gives true as well
newFunc == oldFunc; //gives false however
PicoCreator
fuente
2
Tenga en cuenta que newFuncNO tendrá su propio prototipo para new newFuncinstancias, mientras que oldFuncsí.
jchook
1
Desventaja práctica: instanceof no podrá distinguir entre newFunc y oldFunc
Christopher Swasey
1
@ChristopherSwasey: En realidad, también puede ser una ventaja cuando se amplían las funcionalidades. Pero, por desgracia, será confuso si no se comprende bien (agregado a la respuesta)
PicoCreator
Un gran problema con esta respuesta es que una vez que se vincula, no se puede vincular por segunda vez. Las llamadas posteriores para aplicar también ignoran el objeto 'this' pasado. Ejemplo: var f = function() { console.log('hello ' + this.name) }cuando se enlaza a {name: 'Bob'}imprime 'hola Bob'. f.apply({name: 'Sam'})también imprimirá 'hola Bob', ignorando el objeto 'esto'.
Kevin Mooney
1
Otro caso límite a tener en cuenta: al menos en V8 (y posiblemente en otros motores), esto cambia el comportamiento de Function.prototype.toString (). Llamar a .toString () en la función enlazada le dará una cadena como en function () { [native code] }lugar del contenido completo de la función.
Gladstone
19

Aquí hay una versión ligeramente mejor de la respuesta de Jared. Este no terminará con funciones profundamente anidadas cuanto más clone. Siempre llama al original.

Function.prototype.clone = function() {
    var cloneObj = this;
    if(this.__isClone) {
      cloneObj = this.__clonedFrom;
    }

    var temp = function() { return cloneObj.apply(this, arguments); };
    for(var key in this) {
        temp[key] = this[key];
    }

    temp.__isClone = true;
    temp.__clonedFrom = cloneObj;

    return temp;
};

Además, en respuesta a la respuesta actualizada dada por pico.creator, vale la pena señalar que la bind()función agregada en Javascript 1.8.5 tiene el mismo problema que la respuesta de Jared: seguirá anidando y causando funciones cada vez más lentas cada vez que se usa.

Justin Warkentin
fuente
en 2019+, probablemente sea mejor usar Symbol () en lugar de __properties.
Alexander Mills
10

Siendo curioso pero aún incapaz de encontrar la respuesta al tema de rendimiento de la pregunta anterior, escribí esta esencia para nodejs para probar tanto el rendimiento como la confiabilidad de todas las soluciones presentadas (y calificadas).

He comparado los tiempos de muro de la creación de una función de clonación y la ejecución de un clon. Los resultados junto con los errores de afirmación se incluyen en el comentario de la esencia.

Más mis dos centavos (según la sugerencia del autor):

clone0 cent (más rápido pero más feo):

Function.prototype.clone = function() {
  var newfun;
  eval('newfun=' + this.toString());
  for (var key in this)
    newfun[key] = this[key];
  return newfun;
};

clone4 cent (más lento pero para aquellos a quienes no les gusta eval () para propósitos que solo ellos y sus ancestros conocen):

Function.prototype.clone = function() {
  var newfun = new Function('return ' + this.toString())();
  for (var key in this)
    newfun[key] = this[key];
  return newfun;
};

En cuanto al rendimiento, si eval / new Function es más lento que la solución de envoltura (y realmente depende del tamaño del cuerpo de la función), le brinda un clon de función desnudo (y me refiero al clon superficial real con propiedades pero estado no compartido) sin problemas innecesarios con propiedades ocultas, funciones de envoltura y problemas con la pila.

Además, siempre hay un factor importante que debe tener en cuenta: cuanto menos código, menos lugares para errores.

La desventaja de usar la función eval / new es que el clon y la función original operarán en diferentes ámbitos. No funcionará bien con funciones que utilizan variables de ámbito. Las soluciones que utilizan envoltura similar a un enlace son independientes del alcance.

royaltm
fuente
Tenga en cuenta que eval y new Function no son equivalentes. eval opera en el ámbito local, pero Function no. Esto podría generar problemas para acceder a otras variables desde dentro del código de función. Consulte perfectionkills.com/global-eval-what-are-the-options para obtener una explicación detallada.
Pierre
Correcto y al usar eval o new Function no puedes clonar la función junto con su alcance original.
royaltm
De hecho: una vez que agrega Object.assign(newfun.prototype, this.prototype);antes de la declaración de devolución (versión limpia), su método es la mejor respuesta.
Vivick
9

Fue muy emocionante hacer que este método funcione, por lo que hace un clon de una función usando la llamada de función.

Algunas limitaciones sobre los cierres descritos en Referencia de funciones de MDN

function cloneFunc( func ) {
  var reFn = /^function\s*([^\s(]*)\s*\(([^)]*)\)[^{]*\{([^]*)\}$/gi
    , s = func.toString().replace(/^\s|\s$/g, '')
    , m = reFn.exec(s);
  if (!m || !m.length) return; 
  var conf = {
      name : m[1] || '',
      args : m[2].replace(/\s+/g,'').split(','),
      body : m[3] || ''
  }
  var clone = Function.prototype.constructor.apply(this, [].concat(conf.args, conf.body));
  return clone;
}

Disfrutar.

Max Dolgov
fuente
5

Breve y simple:

Function.prototype.clone = function() {
  return new Function('return ' + this.toString())();
};
micahblu
fuente
1
Además, usa una variante de eval bajo el capó, que es mejor evitar por una variedad de razones (no entraré en eso aquí, está cubierto en miles de otros lugares).
Andrew Faulkner
2
esta solución tiene su lugar (cuando está clonando una función de usuario y no le importa que se use eval)
Lloyd
2
Esto también pierde el alcance de la función. La nueva función puede hacer referencia a variables de alcance externo que ya no existen en el nuevo alcance.
trusktr
4
const oldFunction = params => {
  // do something
};

const clonedFunction = (...args) => oldFunction(...args);
zhenyulin
fuente
3
const clonedFunction = Object.assign(() => {}, originalFunction);
TraceWright
fuente
Tenga en cuenta que esto está incompleto. Esto copiará las propiedades de originalFunction, pero en realidad no las ejecutará cuando ejecute clonedFunction, lo cual es inesperado.
David Calhoun
2

Esta respuesta es para las personas que ven la clonación de una función como la respuesta a su uso deseado, pero que en realidad no necesitan clonar una función, porque lo que realmente quieren es simplemente poder adjuntar diferentes propiedades a la misma función, pero solo declare esa función una vez.

Haga esto creando una función de creación de funciones:

function createFunction(param1, param2) {
   function doSomething() {
      console.log('in the function!');
   }
   // Assign properties to `doSomething` if desired, perhaps based
   // on the arguments passed into `param1` and `param2`. Or,
   // even return a different function from among a group of them.
   return doSomething;
};

let a = createFunction();
a.something = 1;
let b = createFunction();
b.something = 2; // does not overwrite a.something
console.log(a.something);
a();
b();

Esto no es exactamente lo mismo que ha descrito, sin embargo, depende de cómo desee utilizar la función que desea clonar. Esto también usa más memoria porque en realidad crea múltiples copias de la función, una vez por invocación. Sin embargo, esta técnica puede resolver el caso de uso de algunas personas sin la necesidad de una clonefunción complicada .

ErikE
fuente
1

Me pregunto: ¿por qué querría clonar una función cuando tiene prototipos Y puede establecer el alcance de una llamada de función a lo que desee?

 var funcA = {};
 funcA.data = 'something';
 funcA.changeData = function(d){ this.data = d; }

 var funcB = {};
 funcB.data = 'else';

 funcA.changeData.call(funcB.data);

 alert(funcA.data + ' ' + funcB.data);
Plataforma superior
fuente
1
Si hay una razón para cambiar los campos de la función en sí (caché autocontenido, propiedades 'estáticas'), entonces hay una situación en la que quiero clonar una función y modificarla sin afectar la original.
Andrey Shchekin
Me refiero a las propiedades de la función misma.
Andrey Shchekin
1
las funciones pueden tener propiedades, como cualquier objeto, por eso
Radu Simionescu
1

Si desea crear un clon usando el constructor de funciones, algo como esto debería funcionar:

_cloneFunction = function(_function){
    var _arguments, _body, _result;
    var _regexFunction = /^function[\s]+[\w]*\(([\w\s,_\$]*)?\)\{(.*)\}$/;
    var _regexArguments = /((?!=^|,)([\w\$_]))+/g;
    var _matches = _function.toString().match(_regexFunction)
    if(_matches){
        if(_matches[1]){
            _result = _matches[1].match(_regexArguments);
        }else{
            _result = [];
        }
        _result.push(_matches[2]);
    }else{
        _result = [];
    }
    var _clone = Function.apply(Function, _result);
    // if you want to add attached properties
    for(var _key in _function){
        _clone[_key] = _function[_key];
    }
    return _clone;
}

Una simple prueba:

(function(){
    var _clone, _functions, _key, _subKey;
    _functions = [
        function(){ return 'anonymous function'; }
        ,function Foo(){ return 'named function'; }
        ,function Bar(){ var a = function(){ return 'function with internal function declaration'; }; return a; }
        ,function Biz(a,boo,c){ return 'function with parameters'; }
    ];
    _functions[0].a = 'a';
    _functions[0].b = 'b';
    _functions[1].b = 'b';
    for(_key in _functions){
        _clone = window._cloneFunction(_functions[_key]);
        console.log(_clone.toString(), _clone);
        console.log('keys:');
        for(_subKey in _clone){
            console.log('\t', _subKey, ': ', _clone[_subKey]);
        }
    }
})()

Sin embargo, estos clones perderán sus nombres y el alcance de cualquier variable cerrada.

Tobymackenzie
fuente
1

He mejorado la respuesta de Jared a mi manera:

    Function.prototype.clone = function() {
        var that = this;
        function newThat() {
            return (new that(
                arguments[0],
                arguments[1],
                arguments[2],
                arguments[3],
                arguments[4],
                arguments[5],
                arguments[6],
                arguments[7],
                arguments[8],
                arguments[9]
            ));
        }
        function __clone__() {
            if (this instanceof __clone__) {
                return newThat.apply(null, arguments);
            }
            return that.apply(this, arguments);
        }
        for(var key in this ) {
            if (this.hasOwnProperty(key)) {
                __clone__[key] = this[key];
            }
        }
        return __clone__;
    };

1) ahora admite la clonación de constructores (se puede llamar con new); en ese caso, solo toma 10 argumentos (puede variarlo), debido a la imposibilidad de pasar todos los argumentos en el constructor original

2) todo está en cierres correctos

min máx.
fuente
en lugar de arguments[0], arguments[1] /*[...]*/por qué no usas simplemente ...arguments? 1) No hay dependencia con respecto a la cantidad de argumentos (aquí limitado a 10) 2) más corto
Vivick
Con el uso del operador de propagación, este sería definitivamente mi método de clonación OG para funciones, muchas gracias.
Vivick
0
function cloneFunction(Func, ...args) {
  function newThat(...args2) {
    return new Func(...args2);
  }
  function clone() {
    if (this instanceof clone) {
      return newThat(...args);
    }
    return Func.apply(this, args);
  }
  for (const key in Func) {
    if (Func.hasOwnProperty(key)) {
      clone[key] = Func[key];
    }
  }
  Object.defineProperty(clone, 'name', { value: Func.name, configurable: true })
  return clone
};

function myFunction() {
  console.log('Called Function')
}

myFunction.value = 'something';

const newFunction = cloneFunction(myFunction);

newFunction.another = 'somethingelse';

console.log('Equal? ', newFunction === myFunction);
console.log('Names: ', myFunction.name, newFunction.name);
console.log(myFunction);
console.log(newFunction);
console.log('InstanceOf? ', newFunction instanceof myFunction);

myFunction();
newFunction();

Si bien nunca recomendaría usar esto, pensé que sería un pequeño desafío interesante crear un clon más preciso tomando algunas de las prácticas que parecían ser las mejores y arreglándolas un poco. Aquí está el resultado de los registros:

Equal?  false
Names:  myFunction myFunction
{ [Function: myFunction] value: 'something' }
{ [Function: myFunction] value: 'something', another: 'somethingelse' }
InstanceOf?  false
Called Function
Called Function
Braden Rockwell Napier
fuente
0
const clone = (fn, context = this) => {
  // Creates a new function, optionally preserving desired context.
  const newFn = fn.bind(context);

  // Shallow copies over function properties, if any.
  return Object.assign(newFn, fn);
}

// Usage:

// Setup the function to copy from.
const log = (...args) => console.log(...args);
log.testProperty = 1;

// Clone and make sure the function and properties are intact.
const log2 = clone(log);
log2('foo');
// -> 'foo'
log2.testProperty;
// -> 1

// Make sure tweaks to the clone function's properties don't affect the original function properties.
log2.testProperty = 2;
log2.testProperty;
// -> 2
log.testProperty;
// -> 1

Esta función de clonación:

  1. Conserva el contexto.
  2. Es un contenedor y ejecuta la función original.
  3. Copia las propiedades de la función.

Tenga en cuenta que esta versión solo realiza una copia superficial. Si su función tiene objetos como propiedades, la referencia al objeto original se conserva (el mismo comportamiento que Object spread o Object.assign). Esto significa que cambiar las propiedades profundas en la función clonada afectará al objeto al que se hace referencia en la función original.

David Calhoun
fuente