Função em JavaScript que pode ser chamada apenas uma vez


116

Preciso criar uma função que possa ser executada apenas uma vez, em cada vez após a primeira não será executada. Eu sei de C ++ e Java sobre variáveis ​​estáticas que podem fazer o trabalho, mas gostaria de saber se existe uma maneira mais elegante de fazer isso.


~ 8 anos depois, criei uma solicitação de recurso para meus decoradores lib ( github.com/vlio20/utils-decorators ) para ter um decorador que faria exatamente isso ( github.com/vlio20/utils-decorators/issues/77 )
vlio20

Respostas:


221

Se por "não será executado" você quiser dizer "não fará nada quando for chamado mais de uma vez", você pode criar um encerramento:

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

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

Em resposta a um comentário de @Vladloffe (agora excluído): Com uma variável global, outro código poderia redefinir o valor do sinalizador "executado" (qualquer nome que você escolher para ele). Com um encerramento, outro código não tem como fazer isso, seja acidental ou deliberadamente.

Como outras respostas aqui apontam, várias bibliotecas (como Underscore e Ramda ) têm uma pequena função de utilidade (normalmente chamada once()[*] ) que aceita uma função como um argumento e retorna outra função que chama a função fornecida exatamente uma vez, independentemente de como muitas vezes a função retornada é chamada. A função retornada também armazena em cache o primeiro valor retornado pela função fornecida e o retorna nas chamadas subsequentes.

No entanto, se você não estiver usando essa biblioteca de terceiros, mas ainda quiser essa função de utilitário (em vez da solução nonce que ofereci acima), é fácil de implementar. A versão mais legal que vi é esta postada por David Walsh :

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

Eu estaria inclinado a mudar fn = null;para fn = context = null;. Não há razão para o fechamento manter uma referência a contextuma vez fnque foi chamado.

[*] Esteja ciente, entretanto, que outras bibliotecas, como esta extensão do Drupal para jQuery , podem ter uma função chamada once()que faz algo bastante diferente.


1
funciona muito bem, você pode explicar o login por trás disso, como var executado = false; obras
Amr.Ayoub

@EgyCode - Isso é bem explicado na documentação do MDN sobre encerramentos .
Ted Hopp de

desculpe, eu quis dizer lógica, nunca entendi var booleano e como funciona nesse caso executado
Amr.Ayoub

1
@EgyCode - Em certos contextos (como em uma ifexpressão de teste de instrução), o JavaScript espera um dos valores trueou falsee o fluxo do programa reage de acordo com o valor encontrado quando a expressão é avaliada. Operadores condicionais como ==sempre avaliam um valor booleano. Uma variável também pode conter trueou false. (Para mais informações, consulte a documentação on boolean , truthy e Falsey .)
Ted Hopp

Não entendo porque o executado não é reiniciado a cada nova chamada. Alguém pode explicar isso, por favor?
Juanse Cora

64

Substitua-o por uma função NOOP (sem operação) reutilizável .

// 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: Como isso é deselegante? Novamente, é muito limpo, requer menos código e não requer uma nova variável para cada função que deve ser desabilitada. Um "noop" é projetado exatamente para esse tipo de situação.
I Hate Lazy,

1
@fableal: Acabei de ver a resposta do hakra. Então, faça um novo fechamento e variável toda vez que precisar fazer isso em uma nova função? Você tem uma definição muito engraçada de "elegante".
I Hate Lazy,

2
De acordo com a resposta de um assistente, você só precisava fazer _.once (foo) ou _.once (bar), e as funções em si não precisam estar cientes de serem executadas apenas uma vez (sem necessidade de noop e sem necessidade de o * = noop).
fableal

7
Não é realmente a melhor solução. Se você estiver passando essa função como um retorno de chamada, ela ainda poderá ser chamada várias vezes. Por exemplo: setInterval(foo, 1000)- e isso já não funciona mais. Você está apenas substituindo a referência no escopo atual.
um gato de

1
invalidateFunção reutilizável que funciona com setIntervaletc.: Jsbin.com/vicipar/1/edit?js,console
Q20

32

Aponte para uma função vazia depois de chamada:

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>


Ou, assim:

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
Esta solução está muito mais no espírito de uma linguagem altamente dinâmica como Javascript. Por que definir semáforos, quando você pode simplesmente esvaziar a função depois de usada?
Ivan Čurdinjaković

Solução muito boa! Essa solução também tem um desempenho melhor do que a abordagem de fechamento. A única "desvantagem" menor é que você precisa manter o nome da função em sincronia se o nome mudar.
Lionel de

5
O problema com isso é que se houver outra referência à função em algum lugar (por exemplo, foi passada como um argumento e armazenada em outra variável em algum lugar - como em uma chamada para setInterval()), a referência repetirá a funcionalidade original quando chamada.
Ted Hopp,

@TedHopp - aqui está um tratamento especial para esses casos
vsync

1
Sim, é exatamente como a resposta de Bunyk neste tópico. Também é semelhante a um fechamento (como na minha resposta ), mas usando uma propriedade em vez de uma variável de fechamento. Ambos os casos são bastante diferentes da sua abordagem nesta resposta.
Ted Hopp,

25

UnderscoreJs tem uma função que faz isso, 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
Parece-me engraçado onceaceitar argumentos. Você poderia fazer squareo = _.once(square); console.log(squareo(1)); console.log(squareo(2));e receber 1para ambas as chamadas para squareo. Estou entendendo isso direito?
interrogado em

@aschmied Você está correto - o resultado do conjunto de argumentos da primeira chamada será memomizado e retornado para todas as outras chamadas, independentemente do parâmetro, pois a função subjacente nunca é chamada novamente. Em casos como esse, não sugiro usar o _.oncemétodo. Consulte jsfiddle.net/631tgc5f/1
asawyer,

1
@aschmied Ou eu acho que usar uma chamada separada para uma vez por conjunto de argumentos. Não acho que seja realmente destinado a esse tipo de uso.
advogado de

1
Útil se você já estiver usando _; Eu não recomendaria depender de toda a biblioteca para um pequeno trecho de código.
Joe Shanahan

11

Falando sobre variáveis ​​estáticas, isso é um pouco como a variante de encerramento:

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

once(); once(); 

Você pode redefinir uma função se desejar:

once.done = false;


3

Você poderia simplesmente ter a função "remover a si mesmo"

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

    Once = undefined;
}

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

Mas essa pode não ser a melhor resposta se você não quiser engolir erros.

Você também pode fazer isso:

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

    Once = function(){};
}

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

Preciso que funcione como smart pointer, se não houver elementos do tipo A ele pode ser executado, se houver um ou mais elementos A a função não poderá ser executada.

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

    // do stuff
}

1
Preciso que funcione como ponteiro inteligente, se não houver elementos do tipo A ele pode ser executado, se houver um ou mais elementos A a função não pode ser executada.
vlio20

@VladIoffe Não foi isso que você perguntou.
Shmiddty,

Isso não funcionará se Oncefor passado como um retorno de chamada (por exemplo, setInterval(Once, 100)). A função original continuará a ser chamada.
Ted Hopp

2

tente isso

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

2

De um cara chamado Crockford ... :)

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

1
Isso é ótimo se você acha que TypeError: Cannot read property 'apply' of nullé ótimo. Isso é o que você obtém na segunda vez que invoca a função retornada.
Ted Hopp

2

invalidateFunção reutilizável que funciona com 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);

Experimente no JSBin: https://jsbin.com/vicipar/edit?js,console

Variação de resposta de Bunyk


1

Aqui está um exemplo de JSFiddle - http://jsfiddle.net/6yL6t/

E o código:

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

Você poderia fazer:

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

E será executado apenas uma vez :)


1

Configuração inicial:

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

Se você usa Node.js ou escreve JavaScript com browserify, considere o módulo npm "uma vez" :

var once = require('once')

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

1

Se você quiser reutilizar a função no futuro, isso funciona bem com base no código de ed Hopp acima (percebi que a pergunta original não exigia esse recurso extra!):

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

A saída é semelhante a:

Hello World!
Reset
Hello World!

1

decorador simples e fácil de escrever quando você precisa

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

usando:

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

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

0

Tentando usar a função de sublinhado "uma vez":

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

http://underscorejs.org/#once


nah, é muito feio quando você começa a chamá-lo com argumentos.
vsync de

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

Se estiver usando Ramda, você pode usar a função "uma vez" .

Uma citação da documentação:

uma vez Função (a… → b) → (a… → b) PARÂMETROS adicionados na v0.1.0

Aceita uma função fn e retorna uma função que protege a invocação de fn de forma que fn só possa ser chamado uma vez, não importa quantas vezes a função retornada seja chamada. O primeiro valor calculado é retornado nas invocações subsequentes.

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

0

mantenha-o o mais simples possível

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

Você pode ver o resultado

resultado do script


Se você estiver dentro do módulo, basta usar em thisvez dewindow
Shreeketh K

0

JQuery permite chamar a função apenas uma vez usando o método 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>

Implementação usando o método JQuery em () :

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>

Implementação usando JS nativo:

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

É uma má prática poluir o escopo global (também conhecido como janela)
vlio20

Eu concordo com você, mas alguém pode tirar algo disso.
atw

Isso não funciona. Quando esse código é executado pela primeira vez, a função é criada. Então, quando a função é chamada, ela é executada e o global é definido como falso, mas a função ainda pode ser chamada na próxima vez.
trincot de

Não é definido como falso em nenhum lugar.
atw

-2

Este é útil para prevenir loops infinitos (usando jQuery):

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

Se você está preocupado com a poluição do namespace, substitua "doIt" por uma string longa e aleatória.


-2

Ajuda a prevenir a execução pegajosa

var done = false;

function doItOnce(func){
  if(!done){
    done = true;
    func()
  }
  setTimeout(function(){
    done = false;
  },1000)
}
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.