JavaScript: 克隆一个函数

在 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
127686 次浏览

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

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

试试这个:

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

这是最新的答案

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

然而 .bind是 JavaScript 的一个现代(> = iE9)特性(带有 来自 MDN 的兼容性解决方案)

笔记

  1. 不能克隆附加的函数对象 物业包括附加的 原型机属性

  2. 新的函数 this变量坚持使用在 bind()上给出的参数,即使在新的函数 apply()调用中也是如此。来源: http://stackoverflow. com/users/700980/”>@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视为相同的
(new newFunc()) instanceof oldFunc; //gives true
(new oldFunc()) instanceof newFunc; //gives true as well
newFunc == oldFunc; //gives false however

这里有一个稍微好一点的 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.create 给出的更新答案,值得注意的是,JavaScript 1.8.5中添加的 bind()函数与 Jared 的答案有着相同的问题——每次使用它时,它都会继续嵌套,导致函数越来越慢。

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

但是,这些克隆将丢失它们的名称和任何闭合变量的作用域。

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

我比较了创建克隆函数和执行克隆函数的时间。 结果和断言错误都包含在要点的注释中。

再加上我的建议(基于作者的建议) :

克隆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 Function 比包装器解决方案慢(它确实取决于函数体的大小) ,它会给你一个纯函数克隆(我的意思是真正的带有属性但非共享状态的浅克隆) ,没有不必要的隐藏属性、包装器函数和堆栈问题的模糊。

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

使用 eval/new Function 的缺点是,克隆函数和原始函数将在不同的作用域中进行操作。对于使用作用域变量的函数,它无法很好地工作。使用类绑定包装的解决方案与范围无关。

使这个方法工作是非常令人兴奋的,因此它使用 Function 调用克隆了一个函数。

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

好好享受吧。

我用自己的方式改进了 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)所有东西都处于正确的闭合状态

简而言之:

Function.prototype.clone = function() {
return new Function('return ' + this.toString())();
};
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
const oldFunction = params => {
// do something
};


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

这个答案是给那些把克隆一个函数看作是他们所希望的用法的答案的人的,但是很多人不是 事实上,他们需要克隆一个函数,因为他们真正想要的只是能够将不同的属性附加到同一个函数上,但是只声明该函数一次。

通过创建函数创建函数来实现这一点:

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

const clonedFunction = Object.assign(() => {}, originalFunction);
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 传播或 Object.sign 相同)。这意味着更改克隆函数中的深度属性将影响原始函数中引用的对象!

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

在单个表达式中(将通用函数调用绑定到函数,而不是将 this浪费在任意的直接绑定上,或者重新评估其序列化,而这在本机函数中是不可能的) :

let scope=boundFunction?thisObject:undefined;
let funcB=Function.call.bind(funcA,scope);

例如:

let log=Function.call.bind(console.log,undefined);
let map=Function.call.bind(Array.prototype.map,[1,2,3]);
let dynamicmap=Function.call.bind(Array.prototype.map);
map(a=>a*2);
dynamicmap([1,2,3],a=>a*2);