Hàm trong JavaScript chỉ có thể được gọi một lần


116

Tôi cần tạo một hàm chỉ có thể được thực thi một lần, trong mỗi lần sau lần đầu tiên nó sẽ không được thực thi. Tôi biết từ C ++ và Java về các biến tĩnh có thể thực hiện công việc nhưng tôi muốn biết liệu có cách nào hiệu quả hơn để làm điều này không?


~ 8 năm sau, tôi đã tạo một yêu cầu tính năng cho lib trang trí của mình ( github.com/vlio20/utils-decorators ) để có một trình trang trí sẽ làm chính xác điều đó ( github.com/vlio20/utils-decorators/issues/77 )
vlio20

Câu trả lời:


221

Nếu bởi "sẽ không được thực thi", bạn có nghĩa là "sẽ không làm gì khi được gọi nhiều lần", bạn có thể tạo một bao đóng:

var something = (function() {
    var executed = false;
    return function() {
        if (!executed) {
            executed = true;
            // do something
        }
    };
})();

something(); // "do something" happens
something(); // nothing happens

Trả lời nhận xét của @Vladloffe (hiện đã bị xóa): Với một biến toàn cục, mã khác có thể đặt lại giá trị của cờ "đã thực thi" (bất kỳ tên nào bạn chọn cho nó). Với việc đóng, các mã khác không có cách nào để làm điều đó, dù vô tình hay cố ý.

Như các câu trả lời khác ở đây chỉ ra, một số thư viện (chẳng hạn như Dấu gạch dướiRamda ) có một hàm tiện ích nhỏ (thường được đặt tên là once()[*] ) chấp nhận một hàm làm đối số và trả về một hàm khác gọi hàm được cung cấp chính xác một lần, bất kể cách thức nhiều lần hàm trả về được gọi. Hàm trả về cũng lưu trữ giá trị được trả về đầu tiên bởi hàm được cung cấp và trả về giá trị đó trong các lần gọi tiếp theo.

Tuy nhiên, nếu bạn không sử dụng thư viện của bên thứ ba như vậy, nhưng vẫn muốn có một chức năng tiện ích như vậy (thay vì giải pháp nonce mà tôi đã đưa ra ở trên), nó đủ dễ dàng để thực hiện. Phiên bản đẹp nhất mà tôi đã thấy là phiên bản này được đăng bởi David Walsh :

function once(fn, context) { 
    var result;
    return function() { 
        if (fn) {
            result = fn.apply(context || this, arguments);
            fn = null;
        }
        return result;
    };
}

Tôi sẽ có xu hướng thay đổi fn = null;thành fn = context = null;. Không có lý do gì cho việc đóng cửa để duy trì một tham chiếu đến contextmột lần fnđã được gọi.

[*] Tuy nhiên, hãy lưu ý rằng các thư viện khác, chẳng hạn như phần mở rộng Drupal này cho jQuery , có thể có một hàm có tên once()thực hiện một cái gì đó khá khác biệt.


1
hoạt động rất tốt, bạn có thể giải thích đăng nhập đằng sau nó, làm thế nào để var thi hành = false; hoạt động
Amr.Ayoub

@EgyCode - Điều này được giải thích độc đáo trong tài liệu MDN về việc đóng .
Ted Hopp

xin lỗi, tôi có nghĩa là logic, tôi không bao giờ hiểu var boolean và làm thế nào nó hoạt động trong trường hợp đó thực hiện
Amr.Ayoub

1
@EgyCode - Trong các ngữ cảnh nhất định (như trong ifbiểu thức kiểm tra câu lệnh), JavaScript mong đợi một trong các giá trị truehoặc falsevà luồng chương trình phản ứng theo giá trị được tìm thấy khi biểu thức được đánh giá. Các toán tử có điều kiện ==luôn đánh giá thành giá trị boolean. Một biến cũng có thể giữ truehoặc false. (Mọi chi tiết, hãy xem tài liệu về boolean , truthy , và falsey .)
Ted Hopp

Tôi không hiểu tại sao thực thi không được đặt lại với mỗi cuộc gọi mới. Ai đó có thể giải thích nó, xin vui lòng?
Juanse Cora

64

Thay thế nó bằng một chức năng NOOP (không hoạt động) có thể tái sử dụng .

// this function does nothing
function noop() {};

function foo() {
    foo = noop; // swap the functions

    // do your thing
}

function bar() {
    bar = noop; // swap the functions

    // do your thing
}

11
@fableal: Làm thế nào là không lịch sự? Một lần nữa, nó rất sạch sẽ, yêu cầu ít mã hơn và không yêu cầu một biến mới cho mọi chức năng cần bị tắt. Một "noop" được thiết kế chính xác cho loại tình huống này.
I Hate Lazy

1
@fableal: Tôi chỉ xem câu trả lời của hakra. Vì vậy, hãy tạo một bao đóng và một biến mới mỗi khi bạn cần thực hiện điều này với một hàm mới? Bạn có một định nghĩa rất buồn cười về "thanh lịch".
I Hate Lazy

2
Theo đó, theo phản hồi của asawyer, bạn chỉ cần thực hiện _.once (foo) hoặc _.once (bar) và bản thân các chức năng không cần biết là chỉ chạy một lần (không cần noop và không cần dấu * = noop).
fableal

7
Không hẳn là giải pháp tốt nhất. Nếu bạn đang chuyển hàm này như một lệnh gọi lại, nó vẫn có thể được gọi nhiều lần. Ví dụ: setInterval(foo, 1000)- và điều này đã không hoạt động nữa. Bạn chỉ đang ghi đè tham chiếu trong phạm vi hiện tại.
một con mèo

1
invalidateChức năng có thể tái sử dụng hoạt động với setIntervalvv,.: Jsbin.com/vicipar/1/edit?js,console
Q20

32

Trỏ đến một hàm trống khi nó đã được gọi:

function myFunc(){
     myFunc = function(){}; // kill it as soon as it was called
     console.log('call once and never again!'); // your stuff here
};
<button onClick=myFunc()>Call myFunc()</button>


Hoặc, như vậy:

var myFunc = function func(){
     if( myFunc.fired ) return;
     myFunc.fired = true;
     console.log('called once and never again!'); // your stuff here
};

// even if referenced & "renamed"
((refToMyfunc)=>{
  setInterval(refToMyfunc, 1000);
})(myFunc)


1
Giải pháp này phù hợp với tinh thần của một ngôn ngữ năng động cao như Javascript. Tại sao phải đặt semaphores, khi bạn chỉ có thể làm trống hàm một khi nó đã được sử dụng?
Ivan Čurdinjaković

Giải pháp rất hay! Giải pháp này cũng hoạt động tốt hơn so với phương pháp đóng cửa. "Hạn chế" nhỏ duy nhất là bạn cần phải giữ cho tên hàm được đồng bộ nếu tên thay đổi.
Lionel

5
Vấn đề với điều này là nếu có một tham chiếu khác đến hàm ở đâu đó (ví dụ: nó được truyền dưới dạng đối số và được lưu trữ trong một biến khác ở đâu đó - như trong lời gọi đến setInterval()) thì tham chiếu sẽ lặp lại chức năng ban đầu khi được gọi.
Ted Hopp

@TedHopp - đây là phương pháp điều trị đặc biệt cho những trường hợp đó
vsync

1
Vâng, đó chính xác là câu trả lời của Bunyk trên chủ đề này. Nó cũng tương tự như một bao đóng (như trong câu trả lời của tôi ) nhưng sử dụng một thuộc tính thay vì một biến đóng. Cả hai trường hợp đều hoàn toàn khác với cách tiếp cận của bạn trong câu trả lời này.
Ted Hopp

25

UnderscoreJs có một chức năng thực hiện điều đó, underscorejs.org/#once

  // Returns a function that will be executed at most one time, no matter how
  // often you call it. Useful for lazy initialization.
  _.once = function(func) {
    var ran = false, memo;
    return function() {
      if (ran) return memo;
      ran = true;
      memo = func.apply(this, arguments);
      func = null;
      return memo;
    };
  };

1
Tôi thấy buồn cười khi có những oncelập luận chấp nhận. Bạn có thể làm squareo = _.once(square); console.log(squareo(1)); console.log(squareo(2));và nhận được 1cho cả hai cuộc gọi đến squareo. Tôi hiểu điều này đúng không?
aschmied

@aschmied Bạn nói đúng - kết quả của tập đối số của lệnh gọi đầu tiên sẽ được ghi nhớ và trả về cho tất cả các lệnh gọi khác bất kể tham số là gì vì hàm cơ bản không bao giờ được gọi lại. Trong những trường hợp như vậy tôi không đề nghị sử dụng _.oncephương pháp này. Xem jsfiddle.net/631tgc5f/1
asawyer

1
@aschmied Hoặc tôi đoán sử dụng một lệnh gọi riêng biệt đến một lần cho mỗi tập đối số. Tôi không nghĩ điều này thực sự dành cho việc sử dụng như vậy.
asawyer

1
Tiện dụng nếu bạn đang sử dụng _; Tôi không khuyến khích phụ thuộc vào toàn bộ thư viện cho một đoạn mã nhỏ như vậy.
Joe Shanahan

11

Nói về biến tĩnh, điều này hơi giống với biến thể đóng:

var once = function() {
    if(once.done) return;
    console.log('Doing this once!');
    once.done = true;
};

once(); once(); 

Sau đó, bạn có thể đặt lại một chức năng nếu bạn muốn:

once.done = false;


3

Bạn có thể chỉ cần có chức năng "loại bỏ chính nó"

function Once(){
    console.log("run");

    Once = undefined;
}

Once();  // run
Once();  // Uncaught TypeError: undefined is not a function 

Nhưng đây có thể không phải là câu trả lời tốt nhất nếu bạn không muốn mắc lỗi nuốt chữ.

Bạn cũng có thể làm điều này:

function Once(){
    console.log("run");

    Once = function(){};
}

Once(); // run
Once(); // nothing happens

Tôi cần nó hoạt động giống như con trỏ thông minh, nếu không có phần tử nào từ kiểu A, nó có thể được thực thi, nếu có một hoặc nhiều phần tử A thì hàm không thể thực thi được.

function Conditional(){
    if (!<no elements from type A>) return;

    // do stuff
}

1
Tôi cần nó hoạt động giống như con trỏ thông minh, nếu không có phần tử nào từ loại A, nó có thể được thực thi, nếu có một hoặc nhiều phần tử A thì hàm không thể thực thi được.
vlio 20

@VladIoffe Đó không phải là những gì bạn hỏi.
Shmiddty

Điều này sẽ không hoạt động nếu Onceđược chuyển dưới dạng gọi lại (ví dụ setInterval(Once, 100):). Hàm gốc sẽ tiếp tục được gọi.
Ted Hopp

2

thử cái này

var fun = (function() {
  var called = false;
  return function() {
    if (!called) {
      console.log("I  called");
      called = true;
    }
  }
})()

2

Từ một anh chàng tên là Crockford ... :)

function once(func) {
    return function () {
        var f = func;
        func = null;
        return f.apply(
            this,
            arguments
        );
    };
}

1
Điều này thật tuyệt nếu bạn nghĩ rằng điều đó TypeError: Cannot read property 'apply' of nullthật tuyệt. Đó là những gì bạn nhận được vào lần thứ hai gọi hàm trả về.
Ted Hopp

2

invalidateChức năng có thể tái sử dụng hoạt động với setInterval:

var myFunc = function (){
  if (invalidate(arguments)) return;
  console.log('called once and never again!'); // your stuff here
};

const invalidate = function(a) {
  var fired = a.callee.fired;
  a.callee.fired = true;
  return fired;
}

setInterval(myFunc, 1000);

Hãy thử nó trên JSBin: https://jsbin.com/vicipar/edit?js,console

Biến thể của câu trả lời từ Bunyk


1

Đây là một ví dụ JSFiddle - http://jsfiddle.net/6yL6t/

Và mã:

function hashCode(str) {
    var hash = 0, i, chr, len;
    if (str.length == 0) return hash;
    for (i = 0, len = str.length; i < len; i++) {
        chr   = str.charCodeAt(i);
        hash  = ((hash << 5) - hash) + chr;
        hash |= 0; // Convert to 32bit integer
    }
    return hash;
}

var onceHashes = {};

function once(func) {
    var unique = hashCode(func.toString().match(/function[^{]+\{([\s\S]*)\}$/)[1]);

    if (!onceHashes[unique]) {
        onceHashes[unique] = true;
        func();
    }
}

Bạn có thể làm:

for (var i=0; i<10; i++) {
    once(function() {
        alert(i);
    });
}

Và nó sẽ chỉ chạy một lần :)


1

Thiết lập ban đầu:

var once = function( once_fn ) {
    var ret, is_called;
    // return new function which is our control function 
    // to make sure once_fn is only called once:
    return function(arg1, arg2, arg3) {
        if ( is_called ) return ret;
        is_called = true;
        // return the result from once_fn and store to so we can return it multiply times:
        // you might wanna look at Function.prototype.apply:
        ret = once_fn(arg1, arg2, arg3);
        return ret;
    };
}

1

Nếu bạn đang sử dụng Node.js hoặc viết JavaScript với Browserify, hãy xem xét mô-đun npm "một lần" :

var once = require('once')

function load (file, cb) {
  cb = once(cb)
  loader.load('file')
  loader.once('load', cb)
  loader.once('error', cb)
}

1

Nếu bạn muốn có thể sử dụng lại chức năng này trong tương lai thì chức năng này hoạt động tốt dựa trên mã của ed Hopp ở trên (tôi nhận thấy rằng câu hỏi ban đầu không yêu cầu tính năng bổ sung này!):

   var something = (function() {
   var executed = false;              
    return function(value) {
        // if an argument is not present then
        if(arguments.length == 0) {               
            if (!executed) {
            executed = true;
            //Do stuff here only once unless reset
            console.log("Hello World!");
            }
            else return;

        } else {
            // otherwise allow the function to fire again
            executed = value;
            return;
        }       
    }
})();

something();//Hello World!
something();
something();
console.log("Reset"); //Reset
something(false);
something();//Hello World!
something();
something();

Đầu ra trông giống như:

Hello World!
Reset
Hello World!

1

trang trí đơn giản dễ viết khi bạn cần

function one(func) {
  return function () {
     func && func.apply(this, arguments);
     func = null;
  }
}

sử dụng:

var initializer= one( _ =>{
      console.log('initializing')
  })

initializer() // 'initializing'
initializer() // nop
initializer() // nop

0

Cố gắng sử dụng dấu gạch dưới hàm "một lần":

var initialize = _.once(createApplication);
initialize();
initialize();
// Application is only created once.

http://underscorejs.org/#once


nah, thật quá tệ khi bạn bắt đầu gọi nó bằng các đối số.
vsync

0
var init = function() {
    console.log("logges only once");
    init = false;
}; 

if(init) { init(); }

/* next time executing init() will cause error because now init is 
   -equal to false, thus typing init will return false; */

0

Nếu bạn đang sử dụng Ramda, bạn có thể sử dụng chức năng "một lần" .

Trích dẫn từ tài liệu:

một lần Hàm (a… → b) → (a… → b) THAM SỐ được thêm vào v0.1.0

Chấp nhận một hàm fn và trả về một hàm bảo vệ việc gọi fn sao cho fn chỉ có thể được gọi một lần, bất kể hàm trả về được gọi bao nhiêu lần. Giá trị đầu tiên được tính toán được trả về trong các lần gọi tiếp theo.

var addOneOnce = R.once(x => x + 1);
addOneOnce(10); //=> 11
addOneOnce(addOneOnce(50)); //=> 11

0

giữ nó càng đơn giản càng tốt

function sree(){
  console.log('hey');
  window.sree = _=>{};
}

Bạn có thể xem kết quả

kết quả kịch bản


Nếu bạn đang ở trong mô-đun, chỉ cần sử dụng thisthay vìwindow
Shreeketh K

0

JQuery chỉ cho phép gọi hàm một lần bằng phương thức one () :

let func = function() {
  console.log('Calling just once!');
}
  
let elem = $('#example');
  
elem.one('click', func);
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div>
  <p>Function that can be called only once</p>
  <button id="example" >JQuery one()</button>
</div>

Triển khai bằng phương thức JQuery trên () :

let func = function(e) {
  console.log('Calling just once!');
  $(e.target).off(e.type, func)
}
  
let elem = $('#example');
  
elem.on('click', func);
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div>
  <p>Function that can be called only once</p>
  <button id="example" >JQuery on()</button>
</div>

Triển khai bằng JS gốc:

let func = function(e) {
  console.log('Calling just once!');
  e.target.removeEventListener(e.type, func);
}
  
let elem = document.getElementById('example');
  
elem.addEventListener('click', func);
<div>
  <p>Functions that can be called only once</p>
  <button id="example" >ECMAScript addEventListener</button>
</div>


-1
if (!window.doesThisOnce){
  function myFunction() {
    // do something
    window.doesThisOnce = true;
  };
};

Đó là một thực tiễn xấu để gây ô nhiễm phạm vi toàn cầu (hay còn gọi là cửa sổ)
vlio20

Tôi đồng ý với bạn nhưng ai đó có thể hiểu được điều gì đó từ nó.
atw

Điều này không hiệu quả. Khi mã đó được thực thi lần đầu tiên, hàm được tạo. Sau đó, khi hàm được gọi, nó được thực thi và toàn cục được đặt thành false, nhưng hàm vẫn có thể được gọi vào lần sau.
trincot

Nó không được đặt thành false ở bất kỳ đâu.
atw

-2

Điều này rất hữu ích để ngăn chặn các vòng lặp vô hạn (sử dụng jQuery):

<script>
var doIt = true;
if(doIt){
  // do stuff
  $('body').html(String($('body').html()).replace("var doIt = true;", 
                                                  "var doIt = false;"));
} 
</script>

Nếu bạn lo lắng về ô nhiễm không gian tên, hãy thêm một chuỗi dài, ngẫu nhiên cho "doIt".


-2

Nó giúp ngăn chặn quá trình thực thi dính

var done = false;

function doItOnce(func){
  if(!done){
    done = true;
    func()
  }
  setTimeout(function(){
    done = false;
  },1000)
}
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.