JavaScript: clone uma função

115

Qual é a maneira mais rápida de clonar uma função em JavaScript (com ou sem suas propriedades)?

Duas opções que vêm à mente são eval(func.toString())e function() { return func.apply(..) }. Mas estou preocupado com o desempenho de eval e o empacotamento tornará a pilha pior e provavelmente degradará o desempenho se aplicado muito ou aplicado a já empacotado.

new Function(args, body) parece bom, mas como exatamente posso dividir a função existente de forma confiável para args e corpo sem um analisador JS em JS?

Desde já, obrigado.

Atualização: o que quero dizer é ser capaz de fazer

var funcB = funcA.clone(); // where clone() is my extension
funcB.newField = {...};    // without affecting funcA
Andrey Shchekin
fonte
Você pode dar um exemplo mostrando o que você quer dizer.
JoshBerke
Claro, adicionado. (15charsrequired)
Andrey Shchekin
Não tenho certeza, mas poderia copiar = new your_function (); trabalhos?
Savageman
1
Acho que não, ele vai criar uma instância usando a função como um construtor
Andrey Shchekin

Respostas:

54

tente isto:

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
fonte
Ok, então aplicar é a única maneira? Eu melhoraria isso um pouco para que não embrulhe duas vezes quando chamado duas vezes, mas caso contrário, ok.
Andrey Shchekin
aplicar é usado para passar os argumentos facilmente. também, isso funcionará para instâncias em que você deseja clonar um construtor.
Jared
6
sim, eu escrevi sobre aplicar no post original. O problema é que uma função de agrupamento como essa destrói seu nome e ficará mais lenta após muitos clones.
Andrey Shchekin
Parece haver uma maneira de pelo menos afetar a propriedade .name assim: function fa () {} var fb = function () {fa.apply (this, arguments); }; Object.defineProperties (fb, {nome: {valor: 'fb'}});
Killroy
109

Aqui está uma resposta atualizada

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

No entanto, ".bind" é um recurso moderno (> = iE9) do JavaScript (com uma solução alternativa de compatibilidade do MDN)

https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Function/bind

Observação: ele não clona as propriedades anexadas adicionais do objeto de função , incluindo a propriedade prototype . Crédito para @jchook

Nota: que a nova função desta variável está presa ao argumento dado em bind (), mesmo em novas chamadas de função apply (). Crédito para @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

Nota: objeto de função vinculada, instanceof trata newFunc / oldFunc como iguais. Crédito para @Christopher

(new newFunc()) instanceof oldFunc; //gives true
(new oldFunc()) instanceof newFunc; //gives true as well
newFunc == oldFunc;                 //gives false however
PicoCreator
fonte
2
Observe que newFuncNÃO terá seu próprio protótipo para new newFuncinstâncias, mas oldFuncsim.
jchook
1
Desvantagem prática: instanceof não será capaz de distinguir entre newFunc e oldFunc
Christopher Swasey
1
@ChristopherSwasey: Também pode ser uma vantagem ao estender as funcionalidades. Mas, infelizmente, será confuso se não for bem compreendido (adicionado à resposta)
PicoCreator
Um grande problema com essa resposta é que, depois de vincular, você não pode vincular uma segunda vez. As chamadas subsequentes para aplicar também ignoram o objeto 'this' transmitido. Exemplo: var f = function() { console.log('hello ' + this.name) }quando vinculado a {name: 'Bob'}imprime 'hello Bob'. f.apply({name: 'Sam'})também imprimirá 'hello Bob', ignorando o objeto 'this'.
Kevin Mooney
1
Um outro caso extremo a ser observado: pelo menos no V8 (e possivelmente em outros motores), isso muda o comportamento de Function.prototype.toString (). Chamar .toString () na função associada fornecerá a você uma string em function () { [native code] }vez do conteúdo completo da função.
GladstoneKeep
19

Aqui está uma versão um pouco melhor da resposta de Jared. Quanto mais você clonar, este não terminará com funções profundamente aninhadas. Sempre chama o 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;
};

Além disso, em resposta à resposta atualizada fornecida por pico.creator, é importante notar que a bind()função adicionada em Javascript 1.8.5 tem o mesmo problema que a resposta de Jared - ela continuará aninhando causando funções cada vez mais lentas cada vez que é usada.

Justin Warkentin
fonte
em 2019+, provavelmente melhor usar Symbol () em vez de __properties.
Alexander Mills
10

Sendo curioso, mas ainda incapaz de encontrar a resposta para o tópico de desempenho da pergunta acima, escrevi esta essência para nodejs para testar o desempenho e a confiabilidade de todas as soluções apresentadas (e pontuadas).

Eu comparei os tempos de parede da criação de uma função de clone e a execução de um clone. Os resultados, juntamente com os erros de asserção, estão incluídos no comentário da essência.

Mais meus dois centavos (com base na sugestão do autor):

clone0 cent (mais rápido, mas mais feio):

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

clone4 cent (mais lento, mas para aqueles que não gostam de eval () para fins que só eles e seus ancestrais conhecem):

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

Quanto ao desempenho, se eval / new Function for mais lento do que a solução do wrapper (e realmente depende do tamanho do corpo da função), ele fornece um clone de função simples (e eu quero dizer o clone superficial verdadeiro com propriedades, mas estado não compartilhado) sem fuzz desnecessário com propriedades ocultas, funções de invólucro e problemas com pilha.

Além disso, há sempre um fator importante que você precisa levar em consideração: quanto menos código, menos espaços para erros.

A desvantagem de usar a função eval / new é que o clone e a função original irão operar em escopos diferentes. Não funcionará bem com funções que usam variáveis ​​de escopo. As soluções que usam encapsulamento tipo bind são independentes do escopo.

Royaltm
fonte
Esteja ciente de que eval e new Function não são equivalentes. eval opera no escopo local, mas Function não. Isso pode levar a problemas de acesso a outras variáveis ​​de dentro do código da função. Consulte perfectionkills.com/global-eval-what-are-the-options para obter uma explicação detalhada.
Pierre
Certo e usando eval ou new Function, você não pode clonar a função junto com seu escopo original.
royaltm
Na verdade: depois de adicionar Object.assign(newfun.prototype, this.prototype);antes da instrução return (versão limpa), seu método é a melhor resposta.
Vivick
9

Foi muito emocionante fazer esse método funcionar, então ele faz um clone de uma função usando a chamada de Function.

Algumas limitações sobre fechamentos descritos em Referência de função 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;
}

Aproveitar.

Max Dolgov
fonte
5

Curto e simples:

Function.prototype.clone = function() {
  return new Function('return ' + this.toString())();
};
micahblu
fonte
1
Além disso, ele usa uma variante de eval sob o capô, que é melhor evitada por uma variedade de razões (não vou entrar nisso aqui, ela é coberta em milhares de outros lugares).
Andrew Faulkner
2
esta solução tem seu lugar (quando você está clonando uma função de usuário e não se importa se eval é usado)
Lloyd
2
Isso também perde o escopo da função. A nova função pode se referir a vars de escopo externo que não existem mais no novo escopo.
trusktr
4
const oldFunction = params => {
  // do something
};

const clonedFunction = (...args) => oldFunction(...args);
zhenyulin
fonte
3
const clonedFunction = Object.assign(() => {}, originalFunction);
TraceWright
fonte
Observe que isso está incompleto. Isso copiará as propriedades de originalFunction, mas não executará realmente quando você executar clonedFunction, o que é inesperado.
David Calhoun
2

Esta resposta é para pessoas que veem a clonagem de uma função como a resposta para seu uso desejado, mas que muitos não precisam realmente clonar uma função, porque o que eles realmente querem é simplesmente poder anexar propriedades diferentes à mesma função, mas apenas declare essa função uma vez.

Faça isso criando uma função de criação de função:

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

Isso não é exatamente o mesmo que você descreveu, no entanto, depende de como você deseja usar a função que deseja clonar. Isso também usa mais memória porque, na verdade, cria várias cópias da função, uma vez por invocação. No entanto, essa técnica pode resolver o caso de uso de algumas pessoas sem a necessidade de uma clonefunção complicada .

ErikE
fonte
1

Apenas me perguntando - por que você desejaria clonar uma função quando você tem protótipos E pode definir o escopo de uma chamada de função para qualquer coisa que desejar?

 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);
Estágio superior
fonte
1
Se houver uma razão para alterar os campos da própria função (cache autocontido, propriedades 'estáticas'), então há uma situação em que desejo clonar uma função e modificá-la sem afetar a original.
Andrey Shchekin
Refiro-me às propriedades da própria função.
Andrey Shchekin
1
funções podem ter propriedades, como qualquer objeto, é por isso
Radu Simionescu
1

Se você deseja criar um clone usando o construtor Function, algo como isto deve 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;
}

Um teste simples:

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

No entanto, esses clones perderão seus nomes e escopo para quaisquer variáveis ​​fechadas.

tobymackenzie
fonte
1

Impulsionei a resposta de Jared da minha própria maneira:

    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) agora ele suporta clonagem de construtores (pode chamar com novos); nesse caso leva apenas 10 argumentos (você pode variar) - devido à impossibilidade de passar todos os argumentos no construtor original

2) tudo está em fechamentos corretos

max.minin
fonte
em vez de arguments[0], arguments[1] /*[...]*/por que você simplesmente não usa ...arguments? 1) Não há dependência em relação à quantidade de argumentos (aqui limitados a 10) 2) mais curtos
Vivick
Com o uso do operador spread, esse seria definitivamente meu método de clonagem OG para funções, muito obrigado.
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();

Embora eu nunca recomende usar isso, achei que seria um pequeno desafio interessante chegar a um clone mais preciso pegando algumas das práticas que pareciam ser as melhores e corrigindo-as um pouco. Aqui está o resultado dos logs:

Equal?  false
Names:  myFunction myFunction
{ [Function: myFunction] value: 'something' }
{ [Function: myFunction] value: 'something', another: 'somethingelse' }
InstanceOf?  false
Called Function
Called Function
Braden Rockwell Napier
fonte
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 função clone:

  1. Preserva o contexto.
  2. É um wrapper e executa a função original.
  3. Cópias das propriedades da função.

Observe que esta versão executa apenas uma cópia superficial. Se sua função tiver objetos como propriedades, a referência ao objeto original é preservada (mesmo comportamento de Propagação de objeto ou Object.assign). Isso significa que alterar as propriedades profundas na função clonada afetará o objeto referenciado na função original!

David Calhoun
fonte