JavaScript:克隆一个函数

时间:2009-12-02 15:22:19

标签: javascript function

在JavaScript中克隆函数的最快方法是什么?(有或没有属性)?

我想到的两个选项是eval(func.toString())function() { return func.apply(..) }。但我担心eval的性能和包装会使堆栈变得更糟,如果应用很多或应用于已经包装的话,可能会降低性能。

new Function(args, body)看起来很不错,但是如果在JS中没有JS解析器,我可以如何可靠地将现有函数拆分为args和body?

提前致谢。

更新 我的意思是能够做到

var funcB = funcA.clone(); // where clone() is my extension
funcB.newField = {...};    // without affecting funcA

15 个答案:

答案 0 :(得分:87)

这是更新的答案

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

然而" .bind"是JavaScript的现代(> = iE9)功能(具有MDN兼容性解决方案)

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

注意: 不克隆功能对象附加附加属性包括 原型属性。归功于@jchook

注意:新函数变量与bind()上给出的参数一致,即使是新函数apply()调用也是如此。归功于@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

注意:绑定的函数对象,instanceof将newFunc / oldFunc视为相同。归功于@Christopher

(new newFunc()) instanceof oldFunc; //gives true
(new oldFunc()) instanceof newFunc; //gives true as well
newFunc == oldFunc;                 //gives false however

答案 1 :(得分:49)

试试这个:

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

答案 2 :(得分:18)

这是Jared答案的稍微好一点的版本。克隆的次数越多,嵌套函数就越深。它总是称为原始。

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

另外,为了回应pico.creator给出的更新答案,值得注意的是Javascript 1.8.5中添加的bind()函数与Jared的答案具有相同的问题 - 它会使嵌套导致速度变慢,每次使用时功能都会变慢。

答案 3 :(得分:8)

好奇但仍无法找到上述问题的性能主题的答案,我为nodejs写了gist来测试所有呈现(和评分)解决方案的性能和可靠性。

我比较了克隆函数创建和克隆执行的挂壁时间。 结果与断言错误一起包含在要点的评论中。

加上我的两分钱(基于作者的建议):

克隆0分(更快但更丑):

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

clone4 cent(对于那些不喜欢eval()的人来说速度较慢,只为了他们和他们的祖先所知的目的):

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

至于性能,如果eval / new函数比包装器解决方案慢(并且它实际上取决于函数体大小),它会为您提供裸函数克隆(我的意思是具有属性但非共享状态的真正浅层克隆)没有不必要的模糊隐藏属性,包装函数和堆栈问题。

另外,总有一个重要因素需要考虑:代码越少,错误的地方就越少。

使用eval / new函数的缺点是克隆和原始函数将在不同的范围内运行。它不适用于使用范围变量的函数。使用类似绑定的包装的解决方案与范围无关。

答案 4 :(得分:7)

使这个方法工作非常令人兴奋,所以它使用Function调用来复制函数。

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

享受。

答案 5 :(得分:5)

简短而简单:

Function.prototype.clone = function() {
  return new Function('return ' + this.toString())();
};

答案 6 :(得分:3)

const oldFunction = params => {
  // do something
};

const clonedFunction = (...args) => oldFunction(...args);

答案 7 :(得分:1)

只是想知道 - 为什么你想要在拥有原型时克隆一个函数并且可以将函数调用的范围设置为你想要的任何东西?

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

答案 8 :(得分:1)

如果要使用Function构造函数创建克隆,则应该使用以下内容:

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

一个简单的测试:

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

但是,这些克隆将失去任何已关闭的变量的名称和范围。

答案 9 :(得分:1)

我以自己的方式给了Jared的答案:

    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)现在它支持克隆构造函数(可以用new调用);在这种情况下只需要10个参数(你可以改变它) - 由于不可能在原始构造函数中传递所有参数

2)一切都在正确的封闭中

答案 10 :(得分:1)

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

此克隆功能:

  1. 保留上下文。
  2. 是包装器,并运行原始功能。
  3. 复制功能属性。

注意 ,该版本仅执行浅表复制。如果函数将对象作为属性,则保留对原始对象的引用(与Object spread或Object.assign相同的行为)。这意味着更改克隆函数的深层属性将影响原始函数中引用的对象!

答案 11 :(得分: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();

虽然我永远不建议使用此方法,但我认为通过采取一些似乎最好的方法并对其进行一些修复来提出更精确的克隆将是一个有趣的小挑战。这是日志的结果:

Equal?  false
Names:  myFunction myFunction
{ [Function: myFunction] value: 'something' }
{ [Function: myFunction] value: 'something', another: 'somethingelse' }
InstanceOf?  false
Called Function
Called Function

答案 12 :(得分:0)

此答案适用于将克隆功能视为其所需用法的答案的人,但实际上并非真正的许多人需要克隆功能的人,因为他们真正想要的只是能够将不同的属性附加到同一函数,但只能一次声明该函数。

通过创建函数创建函数来做到这一点:

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

这与您所概述的并不完全相同,但是,这取决于您要如何使用希望克隆的功能。这还会占用更多的内存,因为它实际上创建了该函数的多个副本,每次调用一次。但是,此技术可以解决某些人的用例,而无需复杂的clone函数。

答案 13 :(得分:0)

const clonedFunction = Object.assign(() => {}, originalFunction);

答案 14 :(得分:0)

这是一个普通的 ES5 解决方案(甚至适用于类)。

函数和类保留其原始名称,您可以克隆克隆,没有任何绑定问题,无需 eval。

(第一个解决方案必须全局声明;第二个解决方案更冗长,但可以在任何范围内声明) ((这两个函数仅在克隆引用全局可访问内容的函数时有效))

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

以防万一您的原始函数有属性,这里有一个解决方案也可以深入处理这些属性:

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