JavaScript: sao chép một hàm


115

Cách nhanh nhất để sao chép một hàm trong JavaScript (có hoặc không có thuộc tính của nó) là gì?

Hai lựa chọn xuất hiện trong tâm trí là eval(func.toString())function() { return func.apply(..) }. Nhưng tôi lo lắng về hiệu suất của eval và wrapper sẽ làm cho ngăn xếp tồi tệ hơn và có thể sẽ làm giảm hiệu suất nếu áp dụng nhiều hoặc áp dụng cho đã được bọc.

new Function(args, body) trông đẹp, nhưng chính xác thì làm cách nào để tôi có thể phân chia đáng tin cậy hàm hiện có thành args và body mà không có trình phân tích cú pháp JS trong JS?

Cảm ơn trước.

Cập nhật: Ý tôi là có thể làm

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

Bạn có thể đưa ra một ví dụ cho thấy những gì bạn muốn nói.
JoshBerke

Chắc chắn, đã thêm. (15 yêu cầu)
Andrey Shchekin

Tôi không chắc, nhưng có thể copy = new your_ functions (); công việc?
Savageman

1
Tôi không nghĩ như vậy, nó sẽ tạo ra một thể hiện bằng chức năng như một nhà xây dựng
Andrey Shchekin

Câu trả lời:


54

thử cái này:

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

Ok, vậy áp dụng là cách duy nhất? Tôi sẽ cải thiện điều này một chút để nó không quấn hai lần khi được gọi hai lần, nhưng nếu không, ok.
Andrey Shchekin

apply được sử dụng để chuyển các đối số một cách dễ dàng. ngoài ra, điều này sẽ hoạt động đối với các trường hợp bạn muốn sao chép một hàm tạo.
Jared

6
vâng, tôi đã viết về việc áp dụng trong bài đăng gốc. vấn đề là chức năng gói như thế này phá hủy tên của nó và sẽ chậm lại sau nhiều lần sao chép.
Andrey Shchekin

Có vẻ như có một cách để ảnh hưởng ít nhất đến thuộc tính .name như sau: function fa () {} var fb = function () {fa.apply (this, đối số); }; Object.defineProperties (fb, {name: {value: 'fb'}});
Killroy

109

Đây là câu trả lời được cập nhật

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

Tuy nhiên, .bindlà một tính năng hiện đại (> = iE9) của JavaScript (với giải pháp tương thích từ MDN )

Ghi chú

  1. không sao chép đối tượng chức năng gắn thêm thuộc tính , bao gồm các mẫu tài sản. Ghi có cho @jchook

  2. Chức năng mới này biến là khó khăn với đối số đưa ra trên bind (), ngay cả về chức năng mới áp dụng) các cuộc gọi (. Tín dụng cho @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. Đối tượng hàm ràng buộc, instanceof xử lý newFunc / oldFunc như nhau. Tín dụng để @Christopher
(new newFunc()) instanceof oldFunc; //gives true
(new oldFunc()) instanceof newFunc; //gives true as well
newFunc == oldFunc; //gives false however

2
Lưu ý rằng newFuncsẽ KHÔNG có nguyên mẫu riêng cho các new newFunctrường hợp, trong khi oldFuncsẽ có.
jchook

1
Nhược điểm thực hiện: instanceof sẽ không thể phân biệt giữa newFunc và oldFunc
Christopher Swasey

1
@ChristopherSwasey: Nó thực sự cũng có thể là một mặt trái, khi mở rộng các chức năng. Nhưng than ôi, nó sẽ được khó hiểu nếu không hiểu rõ (thêm vào câu trả lời)
PicoCreator

Một vấn đề lớn với câu trả lời này là một khi bạn ràng buộc, bạn không thể ràng buộc lần thứ hai. Các lệnh gọi tiếp theo để áp dụng cũng bỏ qua đối tượng 'this' đã được chuyển. Ví dụ: var f = function() { console.log('hello ' + this.name) }khi được liên kết để {name: 'Bob'}in 'xin chào Bob'. f.apply({name: 'Sam'})cũng sẽ in 'xin chào Bob', bỏ qua đối tượng 'this'.
Kevin Mooney

1
Một trường hợp khác cần lưu ý: Ít nhất là trong V8 (và có thể là các động cơ khác), điều này thay đổi hành vi của Function.prototype.toString (). Gọi .toString () trên hàm ràng buộc sẽ cung cấp cho bạn một chuỗi giống như function () { [native code] }thay vì nội dung của hàm đầy đủ.
GladstoneKeep

19

Đây là một phiên bản tốt hơn một chút cho câu trả lời của Jared. Cái này sẽ không kết thúc với các chức năng lồng nhau sâu sắc khi bạn sao chép nhiều hơn. Nó luôn luôn gọi là bản gốc.

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

Ngoài ra, theo câu trả lời được cập nhật bởi pico.creator, cần lưu ý rằng bind()hàm được thêm vào trong Javascript 1.8.5 có cùng vấn đề với câu trả lời của Jared - nó sẽ tiếp tục lồng vào nhau gây ra các hàm ngày càng chậm hơn mỗi khi nó được sử dụng.


trong năm 2019+, có lẽ tốt hơn nên sử dụng Symbol () thay vì __properties.
Alexander Mills

10

Vì tò mò nhưng vẫn không thể tìm thấy câu trả lời cho chủ đề hiệu suất của câu hỏi trên, tôi đã viết ý chính này chính này cho nodejs để kiểm tra cả hiệu suất và độ tin cậy của tất cả các giải pháp được trình bày (và cho điểm).

Tôi đã so sánh thời gian tường tận của việc tạo hàm sao chép và việc thực hiện một bản sao. Kết quả cùng với lỗi xác nhận được bao gồm trong nhận xét của ý chính.

Cộng với hai xu của tôi (dựa trên gợi ý của tác giả):

clone0 cent (nhanh hơn nhưng xấu hơn):

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

clone4 cent (chậm hơn nhưng đối với những người không thích eval () vì mục đích chỉ được biết đến với họ và tổ tiên của họ):

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

Đối với hiệu suất, nếu eval / new Function chậm hơn giải pháp wrapper (và nó thực sự phụ thuộc vào kích thước thân hàm), nó sẽ cung cấp cho bạn bản sao chức năng trần (và ý tôi là bản sao nông thực sự có thuộc tính nhưng trạng thái không chia sẻ) mà không có lông tơ không cần thiết với các thuộc tính ẩn, các chức năng của trình bao bọc và các vấn đề với ngăn xếp.

Thêm vào đó, luôn có một yếu tố quan trọng mà bạn cần xem xét: càng ít mã, càng ít chỗ mắc lỗi.

Nhược điểm của việc sử dụng hàm eval / new là bản sao và hàm gốc sẽ hoạt động trong các phạm vi khác nhau. Nó sẽ không hoạt động tốt với các hàm đang sử dụng các biến phạm vi. Các giải pháp sử dụng bao gói giống như ràng buộc là không phụ thuộc vào phạm vi.


Hãy lưu ý rằng eval và Hàm mới không tương đương. đánh giá lựa chọn trên phạm vi cục bộ, nhưng Hàm thì không. Điều này có thể dẫn đến vấn đề buộc phải truy cập các biến khác từ bên trong mã hàm. Hãy xem perfectkills.com/global-eval-what-are-the-options để được giải thích sâu hơn.
Pierre

Đúng và bằng cách sử dụng eval hoặc Hàm mới, bạn không thể sao chép hàm cùng với phạm vi ban đầu của nó.
royaltm

Thực tế là: một khi bạn thêm vào Object.assign(newfun.prototype, this.prototype);trước câu lệnh trả về (phiên bản sạch), phương pháp của bạn là câu trả lời tốt nhất.
Vivick 19/07/17

9

Thật là thú vị khi làm cho phương pháp này hoạt động, vì vậy nó tạo một bản sao của một hàm bằng cách sử dụng hàm gọi.

Một số hạn chế về các bao đóng được mô tả tại Tham chiếu Hàm 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;
}

Thưởng thức.


5

Ngắn gọn và đơn giản:

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

1
Ngoài ra, nó sử dụng một biến thể của eval dưới mui xe, điều này tốt nhất nên tránh vì nhiều lý do (sẽ không đi sâu vào đó ở đây, nó được đề cập đến ở hàng nghìn nơi khác).
Andrew Faulkner

2
giải pháp này có vị trí của nó (khi bạn nhân bản một chức năng sử dụng và không quan tâm eval được sử dụng)
Lloyd

2
Điều này cũng làm mất phạm vi chức năng. Hàm mới có thể tham chiếu đến các vars phạm vi bên ngoài không còn tồn tại trong phạm vi mới.
trusktr

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

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

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

Lưu ý rằng điều này là không đầy đủ. Điều này sẽ sao chép các thuộc tính từ originalFunction, nhưng sẽ không thực sự thực thi nó khi bạn chạy clonedFunction, điều này là không mong muốn.
David Calhoun

2

Câu trả lời này dành cho những người xem sao chép một chức năng là câu trả lời cho cách sử dụng mong muốn của họ, nhưng nhiều người không thực sự cần sao chép một hàm, bởi vì điều họ thực sự muốn đơn giản là có thể gắn các thuộc tính khác nhau vào cùng một hàm, nhưng chỉ khai báo hàm đó một lần.

Thực hiện việc này bằng cách tạo một hàm tạo hàm:

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

Điều này không hoàn toàn giống như bạn đã phác thảo, tuy nhiên, nó phụ thuộc vào cách bạn muốn sử dụng chức năng mà bạn muốn sao chép. Điều này cũng sử dụng nhiều bộ nhớ hơn vì nó thực sự tạo ra nhiều bản sao của hàm, một lần cho mỗi lần gọi. Tuy nhiên, kỹ thuật này có thể giải quyết trường hợp sử dụng của một số người mà không cần đến một clonehàm phức tạp .


1

Chỉ cần tự hỏi - tại sao bạn lại muốn sao chép một hàm khi bạn có các nguyên mẫu VÀ có thể đặt phạm vi của một lệnh gọi hàm thành bất kỳ thứ gì bạn muốn?

 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
Nếu có lý do để thay đổi các trường của chính hàm (bộ đệm ẩn chứa, thuộc tính 'tĩnh'), thì sẽ có tình huống khi tôi muốn sao chép một hàm và sửa đổi nó mà không ảnh hưởng đến hàm ban đầu.
Andrey Shchekin

Ý tôi là các thuộc tính của chính hàm.
Andrey Shchekin

1
chức năng có thể có các thuộc tính, giống như bất kỳ đối tượng, đó là lý do tại sao
Radu Simionescu

1

Nếu bạn muốn tạo một bản sao bằng cách sử dụng Hàm tạo, một cái gì đó như thế này sẽ hoạt động:

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

Một bài kiểm tra đơn giản:

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

Mặc dù vậy, những bản sao này sẽ mất tên và phạm vi đối với bất kỳ biến nào bị đóng trên các biến.


1

Tôi đã loại bỏ câu trả lời của Jared theo cách của riêng tôi:

    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) bây giờ nó hỗ trợ sao chép các hàm tạo (có thể gọi bằng new); trong trường hợp đó chỉ cần 10 đối số (bạn có thể thay đổi nó) - do không thể chuyển tất cả các đối số trong hàm tạo ban đầu

2) mọi thứ đều đóng đúng


thay vì arguments[0], arguments[1] /*[...]*/tại sao bạn không sử dụng đơn giản ...arguments? 1) Không có sự phụ thuộc về số lượng các đối số (ở đây chỉ giới hạn đến 10) 2) ngắn hơn
Vivick

Với việc sử dụng toán tử spread, đây chắc chắn sẽ là phương pháp nhân bản OG của tôi cho các hàm, thx rất nhiều.
Vivick 19/07/17

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

Mặc dù tôi sẽ không bao giờ khuyên bạn nên sử dụng điều này, nhưng tôi nghĩ sẽ là một thử thách nhỏ thú vị để tạo ra một bản sao chính xác hơn bằng cách thực hiện một số phương pháp có vẻ là tốt nhất và sửa chữa nó một chút. Đây là kết quả của các bản ghi:

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

Chức năng nhân bản này:

  1. Giữ nguyên ngữ cảnh.
  2. Là một trình bao bọc, và chạy chức năng ban đầu.
  3. Sao chép trên thuộc tính chức năng.

Lưu ý rằng phiên bản này chỉ thực hiện một bản sao nông. Nếu hàm của bạn có các đối tượng là thuộc tính, thì tham chiếu đến đối tượng gốc sẽ được giữ nguyên (hành vi giống như Object spread hoặc Object.assign). Điều này có nghĩa là việc thay đổi các thuộc tính sâu trong hàm được nhân bản sẽ ảnh hưởng đến đối tượng được tham chiếu trong hàm gốc!

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.