ฟังก์ชันใน JavaScript ที่สามารถเรียกใช้ได้เพียงครั้งเดียว


116

ฉันต้องการสร้างฟังก์ชันที่สามารถเรียกใช้งานได้เพียงครั้งเดียวในแต่ละครั้งหลังจากครั้งแรกจะไม่ถูกเรียกใช้งาน ฉันรู้จาก C ++ และ Java เกี่ยวกับตัวแปรคงที่สามารถทำงานได้ แต่ฉันอยากรู้ว่ามีวิธีที่หรูหรากว่านี้หรือไม่?


~ 8 ปีต่อมาฉันได้สร้างคำขอคุณสมบัติให้นักตกแต่งของฉัน lib ( github.com/vlio20/utils-decorators ) เพื่อให้มีมัณฑนากรซึ่งจะทำเช่นนั้น ( github.com/vlio20/utils-decorators/issues/77 )
vlio20

คำตอบ:


221

หาก "จะไม่ดำเนินการ" คุณหมายความว่า "จะไม่ดำเนินการใด ๆ เมื่อถูกเรียกมากกว่าหนึ่งครั้ง" คุณสามารถสร้างการปิดได้:

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

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

ในคำตอบสำหรับความคิดเห็นโดย @Vladloffe (ขณะนี้ถูกลบไปแล้ว): ด้วยตัวแปรส่วนกลางโค้ดอื่น ๆ สามารถรีเซ็ตค่าของแฟล็ก "ดำเนินการ" ได้ (ชื่อใดก็ตามที่คุณเลือก) ด้วยการปิดรหัสอื่น ๆ จะไม่มีทางทำเช่นนั้นได้ไม่ว่าจะโดยบังเอิญหรือจงใจ

ดังที่คำตอบอื่น ๆ ชี้ให้เห็นในที่นี้หลาย ๆ ไลบรารี (เช่นUnderscoreและRamda ) มีฟังก์ชันยูทิลิตี้เล็กน้อย (โดยทั่วไปชื่อonce()[*] ) ที่รับฟังก์ชันเป็นอาร์กิวเมนต์และส่งคืนฟังก์ชันอื่นที่เรียกใช้ฟังก์ชันที่ให้มาทุกครั้งโดยไม่คำนึงถึงวิธีการ หลายครั้งที่เรียกใช้ฟังก์ชันที่ส่งคืน ฟังก์ชันที่ส่งคืนยังแคชค่าที่ส่งกลับเป็นครั้งแรกโดยฟังก์ชันที่ให้มาและส่งกลับค่านั้นในการโทรครั้งต่อ ๆ ไป

อย่างไรก็ตามหากคุณไม่ได้ใช้ไลบรารีของบุคคลที่สาม แต่ยังต้องการฟังก์ชั่นยูทิลิตี้ดังกล่าว (แทนที่จะเป็นโซลูชัน nonce ที่ฉันเสนอไว้ข้างต้น) มันก็ง่ายพอที่จะนำไปใช้ รุ่นที่อร่อยที่สุดที่ฉันเคยเห็นคือรุ่นนี้โพสต์โดย David Walsh :

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

ฉันจะมีแนวโน้มที่จะเปลี่ยนแปลงไปfn = null; fn = context = null;ไม่มีเหตุผลสำหรับการปิดเพื่อรักษาการอ้างอิงถึงcontextเมื่อfnมีการเรียก

[*] อย่างไรก็ตาม โปรดทราบว่าไลบรารีอื่น ๆ เช่นส่วนขยาย Drupal นี้ไปยัง jQueryอาจมีฟังก์ชันชื่อonce()ที่ทำสิ่งที่แตกต่างออกไป


1
ใช้งานได้ดีมากคุณสามารถอธิบายการเข้าสู่ระบบเบื้องหลังได้อย่างไร var ดำเนินการ = false; งาน
Amr.Ayoub

@EgyCode - นี่คือคำอธิบายอย่างในเอกสาร MDN ในการปิด
Ted Hopp

ขอโทษฉันหมายถึงตรรกะฉันไม่เคยเข้าใจบูลีน var และวิธีการทำงานในกรณีนั้นที่ดำเนินการ
Amr.Ayoub

1
@EgyCode - ในบางบริบท (เช่นในifนิพจน์การทดสอบคำสั่ง) JavaScript คาดว่าค่าใดค่าหนึ่งtrueหรือfalseและโฟลว์โปรแกรมจะตอบสนองตามค่าที่พบเมื่อนิพจน์ได้รับการประเมิน ตัวดำเนินการตามเงื่อนไข==มักจะประเมินเป็นค่าบูลีน ตัวแปรยังสามารถระงับการอย่างใดอย่างหนึ่งหรือtrue false(สำหรับข้อมูลเพิ่มเติมโปรดดูเอกสารเกี่ยวกับบูล , truthyและfalsey .)
เท็ด Hopp

ฉันไม่เข้าใจว่าทำไมการดำเนินการไม่ถูกรีเซ็ตทุกครั้งที่เรียกใหม่ ใครช่วยอธิบายหน่อยได้ไหม
Juanse Cora

64

แทนที่ด้วยฟังก์ชันNOOP ที่ใช้ซ้ำได้(ไม่มีการใช้งาน)

// 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: มันไม่สง่างามได้อย่างไร? อีกครั้งมันสะอาดมากต้องใช้รหัสน้อยลงและไม่ต้องการตัวแปรใหม่สำหรับทุกฟังก์ชันที่ควรปิดใช้งาน "noop"ถูกออกแบบมาว่าสำหรับการเรียงลำดับของสถานการณ์นี้
I Hate Lazy

1
@fableal: ฉันเพิ่งดูคำตอบของ hakra ดังนั้นทำการปิดและตัวแปรใหม่ทุกครั้งที่คุณต้องทำสิ่งนี้กับฟังก์ชันใหม่หรือไม่? คุณมีคำจำกัดความที่ตลกมากว่า "สง่างาม"
I Hate Lazy

2
ตามคำตอบของ asawyer คุณต้องทำ _.once (foo) หรือ _.once (bar) เท่านั้นและฟังก์ชั่นนั้นไม่จำเป็นต้องตระหนักถึงการทำงานเพียงครั้งเดียว (ไม่จำเป็นต้องใช้ noop และไม่จำเป็นต้อง the * = noop)
นิทาน

7
ไม่ใช่ทางออกที่ดีที่สุดจริงๆ หากคุณกำลังส่งฟังก์ชันนี้เป็นการโทรกลับก็ยังสามารถเรียกใช้ได้หลายครั้ง ตัวอย่างเช่น: setInterval(foo, 1000)- และสิ่งนี้ใช้ไม่ได้อีกต่อไป คุณแค่เขียนทับข้อมูลอ้างอิงในขอบเขตปัจจุบัน
แมว

1
invalidateฟังก์ชั่นที่ใช้ซ้ำได้ซึ่งทำงานร่วมกับsetIntervalฯลฯ : jsbin.com/vicipar/1/edit?js,console
Q20

32

ชี้ไปที่ฟังก์ชันว่างเมื่อถูกเรียก:

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>


หรือเช่นนั้น:

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
โซลูชันนี้มีมากขึ้นในจิตวิญญาณของภาษาที่มีไดนามิกสูงเช่น Javascript ทำไมต้องตั้งค่า semaphores ในเมื่อคุณสามารถล้างฟังก์ชันได้เมื่อถูกใช้งานแล้ว?
Ivan Čurdinjaković

ทางออกที่ดีมาก! โซลูชันนี้ยังทำงานได้ดีกว่าวิธีการปิด "ข้อเสียเปรียบ" เล็กน้อยเพียงอย่างเดียวคือคุณต้องซิงค์ชื่อฟังก์ชันให้ตรงกันหากชื่อเปลี่ยนไป
Lionel

5
ปัญหาของสิ่งนี้คือถ้ามีการอ้างอิงถึงฟังก์ชันอื่นที่ใดที่หนึ่ง (เช่นมันถูกส่งผ่านเป็นอาร์กิวเมนต์และซ่อนไว้ในตัวแปรอื่นที่ใดที่หนึ่ง - เช่นเดียวกับในการเรียกไปsetInterval()) การอ้างอิงจะทำซ้ำฟังก์ชันเดิมเมื่อถูกเรียก
Ted Hopp

@TedHopp - นี่คือการปฏิบัติพิเศษสำหรับกรณีเหล่านั้น
vsync

1
ใช่นั่นเหมือนกับคำตอบของ Bunykในหัวข้อนี้ นอกจากนี้ยังคล้ายกับการปิด (ตามคำตอบของฉัน ) แต่ใช้คุณสมบัติแทนตัวแปรปิด ทั้งสองกรณีค่อนข้างแตกต่างจากแนวทางของคุณในคำตอบนี้
Ted Hopp

25

UnderscoreJs มีฟังก์ชันที่ทำเช่นนั้นคือ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
ดูเหมือนเป็นเรื่องตลกสำหรับฉันที่onceยอมรับข้อโต้แย้ง คุณสามารถทำsquareo = _.once(square); console.log(squareo(1)); console.log(squareo(2));และได้รับสำหรับการโทรทั้ง1 squareoฉันเข้าใจถูกไหม
aschmied

@aschmied คุณถูกต้อง - ผลลัพธ์ของอาร์กิวเมนต์ชุดแรกของการโทรจะถูกจดจำและส่งคืนสำหรับการเรียกอื่น ๆ ทั้งหมดโดยไม่คำนึงถึงพารามิเตอร์เนื่องจากฟังก์ชันพื้นฐานจะไม่ถูกเรียกอีก ในกรณีเช่นนั้นฉันไม่แนะนำให้ใช้_.onceวิธีนี้ ดูjsfiddle.net/631tgc5f/1
asawyer

1
@aschmied หรือฉันเดาว่าใช้การโทรแยกกันหนึ่งครั้งต่อชุดอาร์กิวเมนต์ ฉันไม่คิดว่านี่มีไว้สำหรับการใช้งานแบบนั้นจริงๆ
asawyer

1
มีประโยชน์ถ้าคุณใช้_; ฉันจะไม่แนะนำให้ขึ้นอยู่กับไลบรารีทั้งหมดสำหรับโค้ดเล็กน้อยเช่นนี้
Joe Shanahan

11

เมื่อพูดถึงตัวแปรคงนี่ก็เหมือนกับตัวแปรการปิด:

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

once(); once(); 

จากนั้นคุณสามารถรีเซ็ตฟังก์ชั่นได้หากต้องการ:

once.done = false;


3

คุณสามารถมีฟังก์ชัน "ลบตัวเอง" ได้

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

    Once = undefined;
}

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

แต่นี่อาจไม่ใช่คำตอบที่ดีที่สุดหากคุณไม่ต้องการกลืนกินข้อผิดพลาด

คุณสามารถทำได้:

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

    Once = function(){};
}

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

ฉันต้องการให้มันทำงานเหมือนตัวชี้อัจฉริยะหากไม่มีองค์ประกอบจากประเภท A ก็สามารถเรียกใช้งานได้หากมีองค์ประกอบ A อย่างน้อยหนึ่งรายการฟังก์ชันจะไม่สามารถเรียกใช้งานได้

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

    // do stuff
}

1
ฉันต้องการให้มันทำงานเหมือนตัวชี้อัจฉริยะหากไม่มีองค์ประกอบจากประเภท A ก็สามารถเรียกใช้งานได้หากมีองค์ประกอบ A อย่างน้อยหนึ่งรายการฟังก์ชันจะไม่สามารถเรียกใช้งานได้
vlio20

@VladIoffe นั่นไม่ใช่สิ่งที่คุณถาม
Shmiddty

การดำเนินการนี้จะใช้ไม่ได้หากOnceส่งผ่านเป็นการโทรกลับ (เช่นsetInterval(Once, 100)) ฟังก์ชันเดิมจะยังคงถูกเรียกใช้
Ted Hopp


2

จากเพื่อนบางคนชื่อ Crockford ... :)

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

1
นี่เป็นสิ่งที่ดีถ้าคุณคิดว่าTypeError: Cannot read property 'apply' of nullดี นั่นคือสิ่งที่คุณได้รับในครั้งที่สองที่คุณเรียกใช้ฟังก์ชันที่ส่งคืน
Ted Hopp

2

invalidateฟังก์ชันที่ใช้ซ้ำได้ซึ่งทำงานร่วมกับ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);

ลองใช้ JSBin: https://jsbin.com/vicipar/edit?js,console

รูปแบบของคำตอบจาก Bunyk


1

นี่คือตัวอย่าง JSFiddle - http://jsfiddle.net/6yL6t/

และรหัส:

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

คุณสามารถทำได้:

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

และจะทำงานเพียงครั้งเดียว :)


1

ตั้งค่าเริ่มต้น:

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

หากคุณต้องการให้สามารถใช้ฟังก์ชันนี้ซ้ำได้ในอนาคตสิ่งนี้จะทำงานได้ดีตามรหัสของ ed Hopp ด้านบน (ฉันรู้ว่าคำถามเดิมไม่ได้เรียกร้องให้มีคุณสมบัติพิเศษนี้!):

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

ผลลัพธ์มีลักษณะดังนี้:

Hello World!
Reset
Hello World!

1

มัณฑนากรที่เรียบง่ายเขียนง่ายเมื่อคุณต้องการ

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

โดยใช้:

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

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


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

หากคุณใช้ Ramda คุณสามารถใช้ฟังก์ชัน"ครั้งเดียว"ได้

คำพูดจากเอกสารประกอบ:

ครั้งเดียวฟังก์ชั่น (a …→ b) → (a …→ b) PARAMETERS เพิ่มใน v0.1.0

ยอมรับฟังก์ชั่น fn และส่งคืนฟังก์ชันที่ป้องกันการเรียกใช้ fn ซึ่ง fn สามารถถูกเรียกได้เพียงครั้งเดียวไม่ว่าฟังก์ชันที่ส่งคืนจะถูกเรียกใช้กี่ครั้งก็ตาม ค่าแรกที่คำนวณจะส่งคืนในการเรียกใช้ครั้งต่อ ๆ ไป

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

0

ทำให้ง่ายที่สุด

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

คุณสามารถเห็นผลลัพธ์

ผลลัพธ์ของสคริปต์


หากคุณอยู่ในโมดูลให้ใช้thisแทนwindow
Shreeketh K

0

JQuery อนุญาตให้เรียกใช้ฟังก์ชันเพียงครั้งเดียวโดยใช้วิธีที่หนึ่ง () :

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>

การใช้งานโดยใช้วิธี JQuery บน () :

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>

การใช้งานโดยใช้ JS ดั้งเดิม:

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

เป็นการปฏิบัติที่ไม่ดีในการสร้างมลพิษให้กับขอบเขตทั่วโลก (aka window)
vlio20

ฉันเห็นด้วยกับคุณ แต่อาจมีใครบางคนได้รับบางอย่างไป
atw

วิธีนี้ใช้ไม่ได้ เมื่อรหัสนั้นถูกเรียกใช้ครั้งแรกฟังก์ชันจะถูกสร้างขึ้น จากนั้นเมื่อฟังก์ชันถูกเรียกใช้งานและ global ถูกตั้งค่าเป็นเท็จ แต่ยังสามารถเรียกใช้ฟังก์ชันในครั้งต่อไปได้
trincot

ไม่ได้ตั้งค่าเป็นเท็จที่ใดก็ได้
atw

-2

อันนี้มีประโยชน์ในการป้องกันลูปไม่สิ้นสุด (โดยใช้ jQuery):

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

หากคุณกังวลเกี่ยวกับมลพิษของเนมสเปซให้เปลี่ยนสตริงแบบสุ่มแบบยาวสำหรับ "doIt"


-2

ช่วยป้องกันการดำเนินการที่เหนียว

var done = false;

function doItOnce(func){
  if(!done){
    done = true;
    func()
  }
  setTimeout(function(){
    done = false;
  },1000)
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.