JavaScript: cloner une fonction

115

Quel est le moyen le plus rapide de cloner une fonction en JavaScript (avec ou sans ses propriétés)?

Deux options qui viennent à l'esprit sont eval(func.toString())et function() { return func.apply(..) }. Mais je suis préoccupé par les performances de l'évaluation et le wrapping aggravera la pile et dégradera probablement les performances s'il est appliqué beaucoup ou appliqué à déjà enveloppé.

new Function(args, body) ça a l'air sympa, mais comment puis-je diviser de manière fiable une fonction existante en args et body sans analyseur JS dans JS?

Merci d'avance.

Mise à jour: ce que je veux dire, c'est être capable de faire

var funcB = funcA.clone(); // where clone() is my extension
funcB.newField = {...};    // without affecting funcA
Andrey Shchekin
la source
Pouvez-vous donner un exemple montrant ce que vous voulez dire.
JoshBerke
Bien sûr, a ajouté. (15charsrequired)
Andrey Shchekin
Je ne suis pas sûr, mais je pourrais copier = new your_function (); travail?
Savageman
1
Je ne pense pas, cela créera une instance en utilisant la fonction comme constructeur
Andrey Shchekin

Réponses:

54

essaye ça:

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
la source
Ok, alors appliquer est le seul moyen? J'améliorerais un peu cela pour qu'il ne s'enroule pas deux fois lorsqu'il est appelé deux fois, mais sinon, d'accord.
Andrey Shchekin
apply est utilisé pour passer facilement les arguments. également, cela fonctionnera pour les instances où vous souhaitez cloner un constructeur.
Jared
6
oui, j'ai écrit sur postuler dans le post original. Le problème est que la fonction d'emballage comme celle-ci détruit son nom et ralentira après de nombreux clones.
Andrey Shchekin
Il semble y avoir une façon au moins d'affecter la propriété .name comme ceci: function fa () {} var fb = function () {fa.apply (this, arguments); }; Object.defineProperties (fb, {nom: {valeur: 'fb'}});
Killroy
109

Voici une réponse mise à jour

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

Cependant .bindest une fonctionnalité moderne (> = iE9) de JavaScript (avec une solution de contournement de compatibilité de MDN )

Remarques

  1. Il ne clone pas les propriétés attachées supplémentaires de l'objet fonction , y compris la propriété prototype . Crédit à @jchook

  2. La nouvelle fonction de cette variable est bloquée avec l'argument donné sur bind (), même sur les nouveaux appels de fonction apply (). Crédit à @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. Objet fonction lié, instanceof traite newFunc / oldFunc comme la même chose. Crédit à @Christopher
(new newFunc()) instanceof oldFunc; //gives true
(new oldFunc()) instanceof newFunc; //gives true as well
newFunc == oldFunc; //gives false however
PicoCreator
la source
2
Notez que cela newFuncn'aura PAS son propre prototype pour les new newFuncinstances, alors que le oldFuncsera.
jchook
1
Inconvénient pratique: instanceof ne pourra pas faire la distinction entre newFunc et oldFunc
Christopher Swasey
1
@ChristopherSwasey: Cela peut aussi être un avantage lors de l'extension des fonctionnalités. Mais hélas, ce sera déroutant s'il n'est pas bien compris (ajouté à la réponse)
PicoCreator
Un gros problème avec cette réponse est qu'une fois que vous liez, vous ne pouvez pas lier une deuxième fois. Les appels ultérieurs à apply ignorent également l'objet «this» passé. Exemple: var f = function() { console.log('hello ' + this.name) }lorsqu'il est lié à {name: 'Bob'}imprime «bonjour Bob». f.apply({name: 'Sam'})affichera également «bonjour Bob», en ignorant l'objet «this».
Kevin Mooney
1
Un autre cas de pointe à noter: au moins dans V8 (et éventuellement d'autres moteurs), cela change le comportement de Function.prototype.toString (). L'appel de .toString () sur la fonction liée vous donnera une chaîne comme function () { [native code] }au lieu du contenu complet de la fonction.
GladstoneKeep le
19

Voici une version légèrement meilleure de la réponse de Jared. Celui-ci ne se retrouvera pas avec des fonctions profondément imbriquées plus vous clonerez. Il appelle toujours l'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;
};

De plus, en réponse à la réponse mise à jour donnée par pico.creator, il convient de noter que la bind()fonction ajoutée dans Javascript 1.8.5 a le même problème que la réponse de Jared - elle continuera à s'emboîter causant des fonctions de plus en plus lentes à chaque fois qu'elle est utilisée.

Justin Warkentin
la source
en 2019+, il est probablement préférable d'utiliser Symbol () au lieu de __properties.
Alexander Mills le
10

Étant curieux mais toujours incapable de trouver la réponse au sujet de performance de la question ci-dessus, j'ai écrit cet essentiel pour nodejs afin de tester à la fois les performances et la fiabilité de toutes les solutions présentées (et notées).

J'ai comparé les temps de mur de la création d'une fonction de clone et de l'exécution d'un clone. Les résultats ainsi que les erreurs d'assertion sont inclus dans le commentaire de l'essentiel.

Plus mes deux cents (basé sur la suggestion de l'auteur):

clone0 cent (plus rapide mais plus laid):

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

clone4 cent (plus lent mais pour ceux qui n'aiment pas eval () à des fins connues seulement d'eux et de leurs ancêtres):

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

En ce qui concerne les performances, si eval / new Function est plus lent que la solution wrapper (et cela dépend vraiment de la taille du corps de la fonction), cela vous donne un clone de fonction nue (et je veux dire le vrai clone superficiel avec des propriétés mais un état non partagé) sans fuzz inutile avec des propriétés cachées, des fonctions de wrapper et des problèmes avec la pile.

De plus, il y a toujours un facteur important à prendre en compte: moins il y a de code, moins il y a d'erreurs.

L'inconvénient de l'utilisation de la fonction eval / new est que le clone et la fonction d'origine fonctionneront dans des portées différentes. Cela ne fonctionnera pas bien avec les fonctions qui utilisent des variables de portée. Les solutions utilisant un wrapping de type bind sont indépendantes de la portée.

royaltm
la source
Attention, eval et new Function ne sont pas équivalents. eval opers sur la portée locale, mais pas Function. Cela peut entraîner des problèmes d'accès à d'autres variables depuis le code de fonction. Voir perfectionkills.com/global-eval-what-are-the-options pour une explication détaillée.
Pierre
À droite et en utilisant eval ou new Function, vous ne pouvez pas cloner la fonction avec sa portée d'origine.
royaltm
En fait: une fois que vous ajoutez Object.assign(newfun.prototype, this.prototype);avant l'instruction return (version propre), votre méthode est la meilleure réponse.
Vivick
9

C'était assez excitant de faire fonctionner cette méthode, donc cela fait un clone d'une fonction en utilisant l'appel de fonction.

Certaines limitations concernant les fermetures décrites dans MDN Function Reference

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;
}

Prendre plaisir.

Max Dolgov
la source
5

Bref et simple:

Function.prototype.clone = function() {
  return new Function('return ' + this.toString())();
};
micahblu
la source
1
En outre, il utilise une variante de eval sous le capot, qu'il vaut mieux éviter pour diverses raisons (je n'entrerai pas dans cela ici, il est couvert dans des milliers d'autres endroits).
Andrew Faulkner
2
cette solution a sa place (lorsque vous clonez une fonction utilisateur et que vous ne vous souciez pas que eval soit utilisé)
Lloyd
2
Cela perd également la portée de la fonction. La nouvelle fonction peut faire référence à des variables de portée externe qui n'existent plus dans la nouvelle portée.
trusktr
4
const oldFunction = params => {
  // do something
};

const clonedFunction = (...args) => oldFunction(...args);
zhenyulin
la source
3
const clonedFunction = Object.assign(() => {}, originalFunction);
TraceWright
la source
Notez que ceci est incomplet. Cela copiera les propriétés de originalFunction, mais ne l'exécutera pas réellement lorsque vous exécuterez clonedFunction, ce qui est inattendu.
David Calhoun le
2

Cette réponse s'adresse aux personnes qui voient le clonage d'une fonction comme la réponse à leur utilisation souhaitée, mais qui n'ont pas vraiment besoin de cloner une fonction, car ce qu'elles veulent vraiment, c'est simplement pouvoir attacher différentes propriétés à la même fonction, mais seulement déclarer cette fonction une fois.

Pour ce faire, créez une fonction de création de fonction:

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();

Ce n'est pas exactement la même chose que celle que vous avez décrite, cependant, cela dépend de la façon dont vous souhaitez utiliser la fonction que vous souhaitez cloner. Cela utilise également plus de mémoire car il crée en fait plusieurs copies de la fonction, une fois par appel. Cependant, cette technique peut résoudre le cas d'utilisation de certaines personnes sans avoir besoin d'une clonefonction compliquée .

ErikE
la source
1

Je me demande simplement - pourquoi voudriez-vous cloner une fonction alors que vous avez des prototypes ET pouvez définir la portée d'un appel de fonction à tout ce que vous souhaitez?

 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);
Niveau supérieur
la source
1
S'il y a une raison de changer les champs de la fonction elle-même (cache autonome, propriétés «statiques»), il y a une situation où je veux cloner une fonction et la modifier sans affecter la fonction d'origine.
Andrey Shchekin
Je veux dire les propriétés de la fonction elle-même.
Andrey Shchekin
1
les fonctions peuvent avoir des propriétés, comme n'importe quel objet, c'est pourquoi
Radu Simionescu
1

Si vous souhaitez créer un clone à l'aide du constructeur Function, quelque chose comme ceci devrait fonctionner:

_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;
}

Un test simple:

(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]);
        }
    }
})()

Cependant, ces clones perdront leurs noms et la portée de toutes les variables fermées.

tobymackenzie
la source
1

J'ai modifié la réponse de Jared à ma manière:

    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) maintenant, il prend en charge le clonage des constructeurs (peut appeler avec new); dans ce cas ne prend que 10 arguments (vous pouvez le faire varier) - en raison de l'impossibilité de passer tous les arguments dans le constructeur d'origine

2) tout est correctement fermé

max.minin
la source
au lieu de arguments[0], arguments[1] /*[...]*/pourquoi n'utilisez-vous pas simplement ...arguments? 1) Il n'y a pas de dépendance concernant le nombre d'arguments (ici limité à 10) 2) plus court
Vivick
Avec l'utilisation de l'opérateur de propagation, ce serait certainement ma méthode de clonage OG pour les fonctions, merci beaucoup.
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();

Bien que je ne recommanderais jamais d'utiliser cela, j'ai pensé que ce serait un petit défi intéressant de trouver un clone plus précis en prenant certaines des pratiques qui semblaient être les meilleures et en les corrigeant un peu. Voici le résultat des journaux:

Equal?  false
Names:  myFunction myFunction
{ [Function: myFunction] value: 'something' }
{ [Function: myFunction] value: 'something', another: 'somethingelse' }
InstanceOf?  false
Called Function
Called Function
Braden Rockwell Napier
la source
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

Cette fonction de clonage:

  1. Préserve le contexte.
  2. Est un wrapper et exécute la fonction d'origine.
  3. Copie les propriétés de la fonction.

Notez que cette version n'effectue qu'une copie superficielle. Si votre fonction a des objets comme propriétés, la référence à l'objet d'origine est conservée (même comportement que Object spread ou Object.assign). Cela signifie que la modification des propriétés profondes dans la fonction clonée affectera l'objet référencé dans la fonction d'origine!

David Calhoun
la source