【问题标题】:JavaScript: clone a functionJavaScript:克隆一个函数
【发布时间】:2010-12-22 11:01:47
【问题描述】:

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

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

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

提前致谢。

更新: 我的意思是能够做到

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

【问题讨论】:

  • 你能举个例子说明你的意思吗?
  • 当然,已添加。 (需要 15 个字符)
  • 我不确定,但可以复制 = new your_function();工作吗?
  • 我不这么认为,它会使用函数作为构造函数来创建实例

标签: javascript function


【解决方案1】:

这是一个更新的答案

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

但是 .bind 是 JavaScript 的现代 ( >=iE9 ) 功能(带有 compatibility workaround from MDN

注意事项

  1. 不克隆附加附加属性的函数对象,包括原型属性。感谢@jchook

  2. 新函数 this 变量卡在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
  1. 绑定函数对象,instanceofnewFunc/oldFunc视为相同。归功于@Christopher
(new newFunc()) instanceof oldFunc; //gives true
(new oldFunc()) instanceof newFunc; //gives true as well
newFunc == oldFunc; //gives false however

【讨论】:

  • 请注意newFunc 不会有自己的new newFunc 实例原型,而oldFunc 会有。
  • 实际缺点:instanceof 将无法区分 newFunc 和 oldFunc
  • @ChristopherSwasey:在扩展功能时,它实际上也有好处。但是,唉,如果理解不好会很混乱(添加到答案中)
  • 这个答案的一个大问题是,一旦绑定,就不能再绑定了。对应用的后续调用也会忽略传递的“this”对象。示例:var f = function() { console.log('hello ' + this.name) } 绑定到 {name: 'Bob'} 时会打印 'hello Bob'。 f.apply({name: 'Sam'}) 也会打印 'hello Bob',忽略 'this' 对象。
  • 另一个需要注意的边缘情况:至少在 V8(可能还有其他引擎)中,这会改变 Function.prototype.toString() 的行为。在绑定函数上调用 .toString() 会给你一个类似 function () { [native code] } 的字符串,而不是完整函数的内容。
【解决方案2】:

试试这个:

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

【讨论】:

  • 好的,那么 apply 是唯一的方法吗?我会对此进行一些改进,以便在调用两次时它不会换行两次,否则,好的。
  • apply 用于轻松传递参数。此外,这也适用于您想要克隆构造函数的实例。
  • 是的,我在原帖中写过申请。问题是这样的包装函数会破坏它的名字,并且在多次克隆后会变慢。
  • 似乎有一种方法至少可以像这样影响 .name 属性: function fa () {} var fb = function() { fa.apply(this, arguments); }; Object.defineProperties(fb, { name: { value: 'fb' } });
【解决方案3】:

这是 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 的答案存在相同的问题——它会不断嵌套导致函数越来越慢每次使用。

【讨论】:

  • 在 2019 年+,使用 Symbol() 代替 __properties 可能更好。
【解决方案4】:

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

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

加上我的两分钱(根据作者的建议):

clone0 cent(更快但更丑):

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 Function 比包装解决方案慢(它实际上取决于函数体大小),它会为您提供裸函数克隆(我的意思是具有属性但未共享状态的真正浅层克隆)无需对隐藏属性、包装函数和堆栈问题进行不必要的模糊处理。

此外,您始终需要考虑一个重要因素:代码越少,出错的地方就越少。

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

【讨论】:

  • 请注意 eval 和 new Function 不是等价的。 eval 在本地范围内操作,但 Function 不在。这可能会导致从函数代码内部访问其他变量时出现问题。有关详细说明,请参阅 perfectionkills.com/global-eval-what-are-the-options
  • 是的,通过使用 eval 或 new Function 您不能将函数与其原始范围一起克隆。
  • 事实上:一旦你在return语句之前添加Object.assign(newfun.prototype, this.prototype);(干净的版本),你的方法就是最好的答案。
【解决方案5】:

让这个方法工作非常令人兴奋,所以它使用函数调用来克隆一个函数。

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

享受吧。

【讨论】:

    【解决方案6】:

    简短:

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

    【讨论】:

    • 此外,它在底层使用了 eval 的变体,出于各种原因最好避免使用它(这里不再赘述,它在 1000 多个其他地方都有介绍)。
    • 这个解决方案有它的位置(当你克隆一个用户函数并且不关心使用了 eval 时)
    • 这也失去了功能范围。新函数可能会引用新作用域中不再存在的外部作用域变量。
    【解决方案7】:
    const oldFunction = params => {
      // do something
    };
    
    const clonedFunction = (...args) => oldFunction(...args);
    

    【讨论】:

      【解决方案8】:
      const clonedFunction = Object.assign(() => {}, originalFunction);
      

      【讨论】:

      • 请注意,这是不完整的。这将从originalFunction 复制属性,但在您运行clonedFunction 时不会实际执行它,这是意料之外的。
      【解决方案9】:

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

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

      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 函数。

      【讨论】:

        【解决方案10】:
        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】:

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

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

          【讨论】:

          • 如果有理由改变函数本身的字段(自包含缓存,'静态'属性),那么有些情况我想克隆一个函数并修改它而不影响原来的.
          • 我是指函数本身的属性。
          • 函数可以有属性,就像任何对象一样,这就是为什么
          【解决方案12】:

          如果你想使用 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]);
                  }
              }
          })()
          

          但是,这些克隆将丢失它们的名称和任何封闭变量的范围。

          【讨论】:

            【解决方案13】:

            我以自己的方式改进了 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) 一切都在正确的闭包中

            【讨论】:

            • 而不是 arguments[0], arguments[1] /*[...]*/ 为什么不简单地使用 ...arguments ? 1)关于参数的数量没有依赖性(这里限制为 10) 2)更短
            • 使用扩展运算符,这绝对是我的OG克隆函数的方法,非常感谢。
            【解决方案14】:

            这是一个普通的 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});

            【讨论】:

              【解决方案15】:
              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
              

              【讨论】:

                猜你喜欢
                • 2020-09-01
                • 1970-01-01
                • 2010-10-01
                • 1970-01-01
                • 1970-01-01
                • 2015-02-12
                • 2011-12-19
                • 2011-04-15
                • 2016-06-01
                相关资源
                最近更新 更多