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.

vlio20
fonte
~ 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.

Ted Hopp
fonte
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
}
Eu odeio preguiçoso
fonte
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)

vsync
fonte
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;
    };
  };
advogado
fonte
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;
Bunyk
fonte
4
var quit = false;

function something() {
    if(quit) {
       return;
    } 
    quit = true;
    ... other code....
}
Diodeus - James MacFarlane
fonte
Veja a resposta de Ted Hopp. É uma forma de definir o escopo das variáveis ​​de nível de função.
Diodeus - James MacFarlane,
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
}
Shmiddty
fonte
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;
    }
  }
})()
Database_Query
fonte
2

De um cara chamado Crockford ... :)

function once(func) {
    return function () {
        var f = func;
        func = null;
        return f.apply(
            this,
            arguments
        );
    };
}
Jowen
fonte
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

Q20
fonte
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 :)

Aldekein
fonte
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;
    };
}
andlrc
fonte
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)
}
orcamano
fonte
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!
CMP
fonte
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
Pery Mimon
fonte
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

Andrew Feng
fonte
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; */
RegarBoy
fonte
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
David
fonte
0

mantenha-o o mais simples possível

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

Você pode ver o resultado

resultado do script

Shreeketh K
fonte
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>

Nikita
fonte
-1
if (!window.doesThisOnce){
  function myFunction() {
    // do something
    window.doesThisOnce = true;
  };
};
atw
fonte
É 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.

Elliot Gorokhovsky
fonte
-2

Ajuda a prevenir a execução pegajosa

var done = false;

function doItOnce(func){
  if(!done){
    done = true;
    func()
  }
  setTimeout(function(){
    done = false;
  },1000)
}
Gleb Dolzikov
fonte