JavaScript: โคลนฟังก์ชัน


115

วิธีที่เร็วที่สุดในการโคลนฟังก์ชันใน JavaScript คืออะไร (มีหรือไม่มีคุณสมบัติ)

ตัวเลือกทั้งสองมาถึงใจและeval(func.toString()) function() { return func.apply(..) }แต่ฉันกังวลเกี่ยวกับประสิทธิภาพของการประเมินและการห่อจะทำให้สแต็กแย่ลงและอาจทำให้ประสิทธิภาพลดลงหากใช้มากหรือใช้กับการห่อแล้ว

new Function(args, body) ดูดี แต่ฉันจะแยกฟังก์ชันที่มีอยู่เป็น args และ body ที่เชื่อถือได้โดยไม่ใช้ตัวแยกวิเคราะห์ JS ใน JS ได้อย่างไร

ขอบคุณล่วงหน้า.

อัปเดต: สิ่งที่ฉันหมายถึงคือสามารถทำได้

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

คุณสามารถยกตัวอย่างที่แสดงให้เห็นว่าคุณหมายถึงอะไร
JoshBerke

แน่นอนเพิ่มแล้ว (15charsrequired)
Andrey Shchekin

ฉันไม่แน่ใจ แต่สามารถ copy = new your_function (); งาน?
Savageman

1
ฉันไม่คิดอย่างนั้นมันจะสร้างอินสแตนซ์โดยใช้ฟังก์ชันเป็นตัวสร้าง
Andrey Shchekin

คำตอบ:


54

ลองสิ่งนี้:

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

ตกลงดังนั้นสมัครเป็นวิธีเดียว? ฉันจะปรับปรุงเรื่องนี้เล็กน้อยเพื่อไม่ให้ห่อสองครั้งเมื่อเรียกสองครั้ง แต่อย่างอื่นก็โอเค
Andrey Shchekin

ใช้เพื่อส่งผ่านข้อโต้แย้งได้อย่างง่ายดาย นอกจากนี้สิ่งนี้จะใช้ได้กับอินสแตนซ์ที่คุณต้องการโคลนตัวสร้าง
Jared

6
ใช่ฉันเขียนเกี่ยวกับการสมัครในโพสต์ต้นฉบับ ปัญหาคือการตัดฟังก์ชันเช่นนี้จะทำลายชื่อและจะช้าลงหลังจากโคลนจำนวนมาก
Andrey Shchekin

ดูเหมือนจะมีวิธีหนึ่งที่อย่างน้อยจะส่งผลต่อคุณสมบัติ. name เช่นนี้: function fa () {} var fb = function () {fa.apply (this, arguments); }; Object.defineProperties (fb, {name: {value: 'fb'}});
Killroy

109

นี่คือคำตอบที่อัปเดต

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

อย่างไรก็ตาม.bindเป็นคุณลักษณะที่ทันสมัย ​​(> = iE9) ของ JavaScript (พร้อมวิธีแก้ไขปัญหาความเข้ากันได้จาก MDN )

หมายเหตุ

  1. มันไม่ได้โคลนฟังก์ชั่นวัตถุที่แนบมาเพิ่มเติมคุณสมบัติ , รวมทั้งต้นแบบคุณสมบัติ เครดิต@jchook

  2. ฟังก์ชั่นใหม่นี้ตัวแปรติดอยู่กับการโต้แย้งได้รับในการผูก () แม้ในฟังก์ชั่นใหม่ที่ใช้สาย () เครดิต@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. Bound function object, instanceof ถือว่า newFunc / oldFunc เหมือนกัน เครดิต@Christopher
(new newFunc()) instanceof oldFunc; //gives true
(new oldFunc()) instanceof newFunc; //gives true as well
newFunc == oldFunc; //gives false however

2
โปรดทราบว่าnewFuncจะไม่มีต้นแบบของตัวเองสำหรับnew newFuncอินสแตนซ์ในขณะที่oldFuncจะ
jchook

1
ข้อเสียในทางปฏิบัติ: instanceof จะไม่สามารถที่จะแยกแยะระหว่าง newFunc และ oldFunc
คริสโต Swasey

1
@ChristopherSwasey: จริงๆแล้วมันอาจจะเป็นกลับหัวได้เช่นกันเมื่อขยายฟังก์ชันออกไป แต่อนิจจามันจะสับสนหากไม่เข้าใจดี (เพิ่มคำตอบ)
PicoCreator

ปัญหาใหญ่สำหรับคำตอบนี้คือเมื่อคุณผูกมัดแล้วคุณจะผูกมัดครั้งที่สองไม่ได้ การเรียกใช้ครั้งต่อ ๆ ไปจะไม่สนใจวัตถุ "this" ที่ส่งผ่านไป ตัวอย่าง: var f = function() { console.log('hello ' + this.name) }เมื่อถูกผูกไว้กับ{name: 'Bob'}ภาพพิมพ์ "สวัสดีบ๊อบ" f.apply({name: 'Sam'})จะพิมพ์คำว่า "hello Bob" โดยไม่สนใจวัตถุ "this"
Kevin Mooney

1
อีกกรณีหนึ่งที่ควรทราบ: อย่างน้อยใน V8 (และอาจเป็นเครื่องยนต์อื่น ๆ ) สิ่งนี้จะเปลี่ยนพฤติกรรมของ Function.prototype.toString () การเรียก. toString () บนฟังก์ชันที่ถูกผูกไว้จะทำให้คุณมีสตริงเช่นfunction () { [native code] }แทนเนื้อหาของฟังก์ชันทั้งหมด
Gladstone ให้

19

นี่เป็นคำตอบของ 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 เป็นที่น่าสังเกตว่าbind()ฟังก์ชันที่เพิ่มใน Javascript 1.8.5 มีปัญหาเช่นเดียวกับคำตอบของ Jared ซึ่งจะทำให้การซ้อนกันทำให้ฟังก์ชันช้าลงและช้าลงทุกครั้งที่ใช้งาน


ในปี 2019+ น่าจะดีกว่าที่จะใช้ Symbol () แทน __properties
Alexander Mills

10

ด้วยความอยากรู้อยากเห็น แต่ยังไม่สามารถหาคำตอบสำหรับหัวข้อประสิทธิภาพของคำถามข้างต้นได้ฉันจึงเขียนส่วนสำคัญนี้สำหรับ nodejs เพื่อทดสอบทั้งประสิทธิภาพและความน่าเชื่อถือของโซลูชันที่นำเสนอ (และคะแนน) ทั้งหมด

ฉันได้เปรียบเทียบเวลาผนังของการสร้างฟังก์ชันโคลนและการดำเนินการของโคลน ผลลัพธ์พร้อมกับข้อผิดพลาดในการยืนยันจะรวมอยู่ในความคิดเห็นของส่วนสำคัญ

บวกสองเซ็นต์ของฉัน (ตามคำแนะนำของผู้เขียน):

โคลน 0 เซ็นต์ (เร็วกว่า แต่น่าเกลียดกว่า):

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

โคลน 4 เซ็นต์ (ช้ากว่า แต่สำหรับผู้ที่ไม่ชอบ 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 ช้ากว่า wrapper solution (และขึ้นอยู่กับขนาดตัวของฟังก์ชันจริงๆ) มันจะทำให้คุณสามารถโคลนฟังก์ชันเปล่า (และฉันหมายถึงโคลนตื้นที่แท้จริงที่มีคุณสมบัติ แต่ไม่ได้แบ่งใช้) โดยไม่มีฟัซที่ไม่จำเป็น ด้วยคุณสมบัติที่ซ่อนอยู่ฟังก์ชัน Wrapper และปัญหาเกี่ยวกับสแต็ก

นอกจากนี้ยังมีปัจจัยสำคัญอย่างหนึ่งที่คุณต้องคำนึงถึงเสมอนั่นคือยิ่งรหัสน้อยตำแหน่งที่ผิดพลาดก็จะน้อยลง

ข้อเสียของการใช้ฟังก์ชัน eval / new คือโคลนและฟังก์ชันเดิมจะทำงานในขอบเขตที่แตกต่างกัน ฟังก์ชันนี้ใช้ไม่ได้กับฟังก์ชันที่ใช้ตัวแปรที่กำหนดขอบเขต โซลูชันที่ใช้การห่อแบบผูกเป็นขอบเขตโดยไม่ขึ้นกับ


ระวังว่า eval และฟังก์ชันใหม่จะไม่เทียบเท่ากัน ประเมินตัวเลือกในขอบเขตท้องถิ่น แต่ฟังก์ชันไม่ทำ ซึ่งอาจทำให้เกิดปัญหาในการเข้าถึงตัวแปรอื่น ๆ จากในโค้ดฟังก์ชัน ดูperfectionkills.com/global-eval-what-are-the-optionsสำหรับคำอธิบายอย่างละเอียด
ปิแอร์

ถูกต้องและด้วยการใช้ eval หรือฟังก์ชันใหม่คุณไม่สามารถโคลนฟังก์ชันร่วมกับขอบเขตเดิมได้
royaltm

ตามความเป็นจริง: เมื่อคุณเพิ่มObject.assign(newfun.prototype, this.prototype);ก่อนคำสั่ง return (เวอร์ชันสะอาด) วิธีของคุณคือคำตอบที่ดีที่สุด
Vivick

9

มันค่อนข้างน่าตื่นเต้นที่ทำให้วิธีนี้ใช้งานได้ดังนั้นจึงสร้างการโคลนของฟังก์ชันโดยใช้การเรียกฟังก์ชัน

ข้อ จำกัด บางประการเกี่ยวกับการปิดที่อธิบายไว้ใน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

สั้นและง่าย:

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

1
นอกจากนี้ยังใช้รูปแบบของ eval ภายใต้ประทุนซึ่งหลีกเลี่ยงได้ดีที่สุดด้วยเหตุผลหลายประการ (จะไม่เข้าที่นี่มันครอบคลุมในที่อื่นกว่า 1,000 แห่ง)
Andrew Faulkner

2
โซลูชันนี้มีที่มา (เมื่อคุณโคลนฟังก์ชันผู้ใช้และไม่สนใจว่าจะใช้ eval)
ลอยด์

2
นอกจากนี้ยังสูญเสียขอบเขตฟังก์ชัน ฟังก์ชันใหม่อาจอ้างถึงตัวแปรขอบเขตภายนอกที่ไม่มีอยู่ในขอบเขตใหม่อีกต่อไป
trusktr


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

โปรดทราบว่าสิ่งนี้ยังไม่สมบูรณ์ การดำเนินการนี้จะคัดลอกคุณสมบัติจากoriginalFunctionแต่จะไม่ดำเนินการจริงเมื่อคุณเรียกใช้clonedFunctionซึ่งเป็นเรื่องที่ไม่คาดคิด
David Calhoun

2

คำตอบนี้เหมาะสำหรับผู้ที่มองเห็นโคลนฟังก์ชั่นเป็นคำตอบเพื่อการใช้งานที่ต้องการของพวกเขา แต่ที่หลายคนไม่ได้จริงต้องโคลนฟังก์ชั่นเพราะสิ่งที่พวกเขาต้องการจริงๆเป็นเพียงเพื่อให้สามารถอายัดทรัพย์สินที่แตกต่างกันเพื่อฟังก์ชั่นเหมือนกัน แต่เพียง ประกาศฟังก์ชันนั้นครั้งเดียว

ทำได้โดยสร้างฟังก์ชันสร้างฟังก์ชัน:

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ฟังก์ชันที่ซับซ้อน


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

1
หากมีเหตุผลที่ต้องเปลี่ยนฟิลด์ของฟังก์ชันเอง (แคชที่มีอยู่ในตัวคุณสมบัติ 'คงที่') จะมีสถานการณ์เกิดขึ้นเมื่อฉันต้องการโคลนฟังก์ชันและแก้ไขโดยไม่ส่งผลกระทบต่อฟังก์ชันดั้งเดิม
Andrey Shchekin

ฉันหมายถึงคุณสมบัติของฟังก์ชันนั่นเอง
Andrey Shchekin

1
ฟังก์ชันสามารถมีคุณสมบัติเช่นเดียวกับวัตถุใด ๆ นั่นคือเหตุผล
Radu Simionescu

1

หากคุณต้องการสร้างโคลนโดยใช้ตัวสร้างฟังก์ชันสิ่งนี้ควรใช้งานได้:

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

โคลนเหล่านี้จะสูญเสียชื่อและขอบเขตสำหรับตัวแปรที่ปิดไปแล้ว


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) ตอนนี้รองรับการโคลนคอนสตรัคเตอร์ (สามารถโทรใหม่ได้); ในกรณีนั้นใช้อาร์กิวเมนต์เพียง 10 อาร์กิวเมนต์ (คุณสามารถเปลี่ยนแปลงได้) - เนื่องจากเป็นไปไม่ได้ที่จะส่งผ่านอาร์กิวเมนต์ทั้งหมดในตัวสร้างดั้งเดิม

2) ทุกอย่างอยู่ในการปิดที่ถูกต้อง


แทนที่จะใช้arguments[0], arguments[1] /*[...]*/ทำไมคุณไม่ใช้...arguments? 1) ไม่มีการพึ่งพาเกี่ยวกับจำนวนอาร์กิวเมนต์ (ที่นี่ จำกัด ที่ 10) 2) สั้นกว่า
Vivick

ด้วยการใช้ตัวดำเนินการการแพร่กระจายนี่จะเป็นวิธีการโคลน OG สำหรับฟังก์ชันของฉันอย่างแน่นอนขอบคุณมาก
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();

แม้ว่าฉันจะไม่แนะนำให้ใช้สิ่งนี้ แต่ฉันคิดว่ามันจะเป็นความท้าทายเล็กน้อยที่น่าสนใจในการสร้างโคลนที่แม่นยำยิ่งขึ้นโดยใช้แนวทางปฏิบัติบางอย่างที่ดูเหมือนจะดีที่สุดและแก้ไขมันเล็กน้อย นี่คือผลลัพธ์ของบันทึก:

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

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

ฟังก์ชันโคลนนี้:

  1. รักษาบริบท
  2. เป็นกระดาษห่อหุ้มและเรียกใช้ฟังก์ชันดั้งเดิม
  3. คัดลอกคุณสมบัติของฟังก์ชัน

โปรดทราบว่าเวอร์ชันนี้ทำการคัดลอกแบบตื้นเท่านั้น หากฟังก์ชันของคุณมีอ็อบเจ็กต์เป็นคุณสมบัติการอ้างอิงไปยังอ็อบเจ็กต์ดั้งเดิมจะถูกเก็บรักษาไว้ (ลักษณะการทำงานเช่นเดียวกับ Object spread หรือ Object.assign) ซึ่งหมายความว่าการเปลี่ยนคุณสมบัติเชิงลึกในฟังก์ชันโคลนจะส่งผลต่อวัตถุที่อ้างถึงในฟังก์ชันดั้งเดิม!

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.