¿Cómo evitar variables globales en JavaScript?

84

Todos sabemos que las variables globales no son las mejores prácticas. Pero hay varios casos en los que es difícil codificar sin ellos. ¿Qué técnicas utiliza para evitar el uso de variables globales?

Por ejemplo, dado el siguiente escenario, ¿cómo no usaría una variable global?

Código JavaScript:

var uploadCount = 0;

window.onload = function() {
    var frm = document.forms[0];

    frm.target = "postMe";
    frm.onsubmit = function() {
        startUpload();
        return false;
    }
}

function startUpload() {
    var fil = document.getElementById("FileUpload" + uploadCount);

    if (!fil || fil.value.length == 0) {
        alert("Finished!");
        document.forms[0].reset();
        return;
    }

    disableAllFileInputs();
    fil.disabled = false;
    alert("Uploading file " + uploadCount);
    document.forms[0].submit();
}

Marcado relevante:

<iframe src="test.htm" name="postHere" id="postHere"
  onload="uploadCount++; if(uploadCount > 1) startUpload();"></iframe>

<!-- MUST use inline JavaScript here for onload event
     to fire after each form submission. -->

Este código proviene de un formulario web con varios <input type="file">. Carga los archivos de uno en uno para evitar grandes solicitudes. Lo hace PUBLICANDO en el iframe, esperando la respuesta que activa la carga del iframe y luego activa el siguiente envío.

No tiene que responder a este ejemplo específicamente, solo lo proporciono como referencia a una situación en la que no puedo pensar en una forma de evitar las variables globales.

Josh Stodola
fuente
3
Utilice la expresión de función inmediatamente invocada (IIFE) Puede leer más aquí: codearsenal.net/2014/11/…

Respuestas:

69

La forma más fácil es envolver su código en un cierre y exponer manualmente solo aquellas variables que necesita globalmente al alcance global:

(function() {
    // Your code here

    // Expose to global
    window['varName'] = varName;
})();

Para abordar el comentario de Crescent Fresh: para eliminar las variables globales del escenario por completo, el desarrollador necesitaría cambiar una serie de cosas asumidas en la pregunta. Se parecería mucho más a esto:

Javascript:

(function() {
    var addEvent = function(element, type, method) {
        if('addEventListener' in element) {
            element.addEventListener(type, method, false);
        } else if('attachEvent' in element) {
            element.attachEvent('on' + type, method);

        // If addEventListener and attachEvent are both unavailable,
        // use inline events. This should never happen.
        } else if('on' + type in element) {
            // If a previous inline event exists, preserve it. This isn't
            // tested, it may eat your baby
            var oldMethod = element['on' + type],
                newMethod = function(e) {
                    oldMethod(e);
                    newMethod(e);
                };
        } else {
            element['on' + type] = method;
        }
    },
        uploadCount = 0,
        startUpload = function() {
            var fil = document.getElementById("FileUpload" + uploadCount);

            if(!fil || fil.value.length == 0) {    
                alert("Finished!");
                document.forms[0].reset();
                return;
            }

            disableAllFileInputs();
            fil.disabled = false;
            alert("Uploading file " + uploadCount);
            document.forms[0].submit();
        };

    addEvent(window, 'load', function() {
        var frm = document.forms[0];

        frm.target = "postMe";
        addEvent(frm, 'submit', function() {
            startUpload();
            return false;
        });
    });

    var iframe = document.getElementById('postHere');
    addEvent(iframe, 'load', function() {
        uploadCount++;
        if(uploadCount > 1) {
            startUpload();
        }
    });

})();

HTML:

<iframe src="test.htm" name="postHere" id="postHere"></iframe>

No necesita un controlador de eventos en línea en el <iframe>, aún se activará en cada carga con este código.

Respecto al evento de carga

Aquí hay un caso de prueba que demuestra que no necesita un onloadevento en línea . Esto depende de hacer referencia a un archivo (/emptypage.php) en el mismo servidor; de lo contrario, debería poder pegarlo en una página y ejecutarlo.

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <title>untitled</title>
</head>
<body>
    <script type="text/javascript" charset="utf-8">
        (function() {
            var addEvent = function(element, type, method) {
                if('addEventListener' in element) {
                    element.addEventListener(type, method, false);
                } else if('attachEvent' in element) {
                    element.attachEvent('on' + type, method);

                    // If addEventListener and attachEvent are both unavailable,
                    // use inline events. This should never happen.
                } else if('on' + type in element) {
                    // If a previous inline event exists, preserve it. This isn't
                    // tested, it may eat your baby
                    var oldMethod = element['on' + type],
                    newMethod = function(e) {
                        oldMethod(e);
                        newMethod(e);
                    };
                } else {
                    element['on' + type] = method;
                }
            };

            // Work around IE 6/7 bug where form submission targets
            // a new window instead of the iframe. SO suggestion here:
            // http://stackoverflow.com/q/875650
            var iframe;
            try {
                iframe = document.createElement('<iframe name="postHere">');
            } catch (e) {
                iframe = document.createElement('iframe');
                iframe.name = 'postHere';
            }

            iframe.name = 'postHere';
            iframe.id = 'postHere';
            iframe.src = '/emptypage.php';
            addEvent(iframe, 'load', function() {
                alert('iframe load');
            });

            document.body.appendChild(iframe);

            var form = document.createElement('form');
            form.target = 'postHere';
            form.action = '/emptypage.php';
            var submit = document.createElement('input');
            submit.type = 'submit';
            submit.value = 'Submit';

            form.appendChild(submit);

            document.body.appendChild(form);
        })();
    </script>
</body>
</html>

La alerta se activa cada vez que hago clic en el botón Enviar en Safari, Firefox, IE 6, 7 y 8.

falta de párpados
fuente
O proporcione algún tipo de acceso. Estoy de acuerdo.
Escenario superior
3
Es útil cuando las personas votan en contra para explicar su razonamiento para votar en contra.
párpados
6
No voté en contra. Sin embargo, decir window ['varName'] = varName es lo mismo que hacer una declaración var global fuera del cierre. var foo = "bar"; (function() { alert(window['foo']) })();
Josh Stodola
Respondiste al título, no a la pregunta. Eso no me gustó. Poner el lenguaje de cierre dentro del contexto de las referencias del controlador de eventos en línea (como se trata de la esencia de la pregunta) hubiera sido más agradable.
Crescent Fresh
1
Crescent Fresh, respondí la pregunta. Los supuestos de la pregunta deberían ser rediseñados para evitar funciones globales. Esta respuesta toma, como guía, los supuestos de la pregunta (un controlador de eventos en línea, por ejemplo) y le permite al desarrollador elegir solo los puntos de acceso globales necesarios en lugar de que todo esté en el alcance global.
párpados
59

Sugiero el patrón del módulo .

YAHOO.myProject.myModule = function () {

    //"private" variables:
    var myPrivateVar = "I can be accessed only from within YAHOO.myProject.myModule.";

    //"private" method:
    var myPrivateMethod = function () {
        YAHOO.log("I can be accessed only from within YAHOO.myProject.myModule");
    }

    return  {
        myPublicProperty: "I'm accessible as YAHOO.myProject.myModule.myPublicProperty."
        myPublicMethod: function () {
            YAHOO.log("I'm accessible as YAHOO.myProject.myModule.myPublicMethod.");

            //Within myProject, I can access "private" vars and methods:
            YAHOO.log(myPrivateVar);
            YAHOO.log(myPrivateMethod());

            //The native scope of myPublicMethod is myProject; we can
            //access public members using "this":
            YAHOO.log(this.myPublicProperty);
        }
    };

}(); // the parens here cause the anonymous function to execute and return
Erenon
fuente
3
Haré +1 porque entiendo esto y es extremadamente útil, pero aún no tengo claro qué tan efectivo sería esto en una situación en la que solo uso una variable global. Corrígeme si me equivoco, pero al ejecutar esta función y devolverla, el objeto devuelto se almacena en YAHOO.myProject.myModule, que es una variable global. ¿Derecho?
Josh Stodola
11
@Josh: la variable global no es mala. Las variables globales_S_ son malas. Mantenga el recuento de globales lo menos posible.
erenon
Toda la función anónima se ejecutaría cada vez que quisiera acceder a una de las propiedades / métodos públicos de los 'módulos', ¿verdad?
UpTheCreek
@UpTheCreek: no, no lo hará. Se ejecuta solo una vez, cuando el programa encuentra el cierre () en la última línea, y el objeto devuelto se asignará a la propiedad myModule con el cierre que contiene myPrivateVar y myPrivateMethod.
erenon
Hermoso, exactamente lo que estaba buscando también. Esto me permite separar la lógica de mi página de mi manejo de eventos jquery. Una pregunta, con un ataque XSS, un atacante aún tendría acceso a YAHOO.myProject.myModule, ¿correcto? ¿No sería mejor dejar la función externa sin nombre y poner un (); al final, alrededor del $ (documento). ¿Listo también? ¿Quizás una modificación del meta objeto de las propiedades YAHOO.myProct.myModule? Acabo de invertir bastante tiempo en la teoría js y estoy tratando de encajarla ahora.
Dale
8

En primer lugar, es imposible evitar JavaScript global, siempre habrá algo pendiente del alcance global. Incluso si crea un espacio de nombres, lo cual sigue siendo una buena idea, ese espacio de nombres será global.

Sin embargo, existen muchos enfoques para no abusar del alcance global. Dos de los más simples son usar el cierre o, dado que solo tiene una variable de la que necesita realizar un seguimiento, simplemente configúrela como una propiedad de la función en sí (que luego puede tratarse como una staticvariable).

Cierre

var startUpload = (function() {
  var uploadCount = 1;  // <----
  return function() {
    var fil = document.getElementById("FileUpload" + uploadCount++);  // <----

    if(!fil || fil.value.length == 0) {    
      alert("Finished!");
      document.forms[0].reset();
      uploadCount = 1; // <----
      return;
    }

    disableAllFileInputs();
    fil.disabled = false;
    alert("Uploading file " + uploadCount);
    document.forms[0].submit();
  };
})();

* Tenga en cuenta que el incremento de uploadCountocurre internamente aquí

Propiedad de la función

var startUpload = function() {
  startUpload.uploadCount = startUpload.count || 1; // <----
  var fil = document.getElementById("FileUpload" + startUpload.count++);

  if(!fil || fil.value.length == 0) {    
    alert("Finished!");
    document.forms[0].reset();
    startUpload.count = 1; // <----
    return;
  }

  disableAllFileInputs();
  fil.disabled = false;
  alert("Uploading file " + startUpload.count);
  document.forms[0].submit();
};

No estoy seguro de por qué uploadCount++; if(uploadCount > 1) ...es necesario, ya que parece que la condición siempre será cierta. Pero si necesita acceso global a la variable, entonces el método de propiedad de la función que describí anteriormente le permitirá hacerlo sin que la variable sea realmente global.

<iframe src="test.htm" name="postHere" id="postHere"
  onload="startUpload.count++; if (startUpload.count > 1) startUpload();"></iframe>

Sin embargo, si ese es el caso, entonces probablemente debería usar un objeto literal o instanciado y hacer esto de la manera normal de OO (donde puede usar el patrón del módulo si le apetece).

Justin Johnson
fuente
1
Absolutamente puede evitar el alcance global. En su ejemplo de 'Cierre', simplemente elimine 'var startUpload =' desde el principio, y esa función estará completamente encerrada, sin accesibilidad a nivel global. En la práctica, muchas personas prefieren exponer una sola variable, dentro de la cual se incluyen referencias a todo lo demás
derrylwc
1
@derrylwc En este caso, startUploades esa única variable a la que te refieres. Eliminar var startUpload = del ejemplo de cierre significa que la función interna nunca podrá ejecutarse porque no hay ninguna referencia a ella. La cuestión de evitar la contaminación de alcance global se refiere a la contra variable interna, uploadCountque es utilizada por startUpload. Además, creo que el OP está tratando de evitar contaminar cualquier alcance fuera de este método con la uploadCountvariable utilizada internamente .
Justin Johnson
Si el código dentro del cierre anónimo agrega al cargador como un detector de eventos, por supuesto, "podrá ejecutarse" siempre que ocurra el evento apropiado.
Damian Yerrick
6

A veces tiene sentido tener variables globales en JavaScript. Pero no los deje colgando directamente de la ventana de esa manera.

En su lugar, cree un único objeto de "espacio de nombres" para contener sus globales. Para obtener puntos de bonificación, ponga todo allí, incluidos sus métodos.

Nosredna
fuente
¿Cómo puedo hacer eso? crear un único objeto de espacio de nombres para contener mis globales?
p.matsinopoulos
5
window.onload = function() {
  var frm = document.forms[0];
  frm.target = "postMe";
  frm.onsubmit = function() {
    frm.onsubmit = null;
    var uploader = new LazyFileUploader();
    uploader.startUpload();
    return false;
  }
}

function LazyFileUploader() {
    var uploadCount = 0;
    var total = 10;
    var prefix = "FileUpload";  
    var upload = function() {
        var fil = document.getElementById(prefix + uploadCount);

        if(!fil || fil.value.length == 0) {    
            alert("Finished!");
            document.forms[0].reset();
            return;
         }

        disableAllFileInputs();
        fil.disabled = false;
        alert("Uploading file " + uploadCount);
        document.forms[0].submit();
        uploadCount++;

        if (uploadCount < total) {
            setTimeout(function() {
                upload();
            }, 100); 
        }
    }

    this.startUpload = function() {
        setTimeout(function() {
            upload();
        }, 100);  
    }       
}
ChaosPandion
fuente
¿Cómo incremento el uploadCount dentro de mi onloadcontrolador en el iframe? Esto es crucial.
Josh Stodola
1
OK, veo lo que hiciste aquí. Desafortunadamente esto no es lo mismo. Esto activa todas las cargas por separado, pero al mismo tiempo (técnicamente, hay 100 ms entre ellas). La solución actual los carga secuencialmente, lo que significa que la segunda carga no comienza hasta que se completa la primera. Es por eso que onloadse requiere el controlador en línea . La asignación programática del controlador no funciona porque solo se activa la primera vez. El controlador en línea se activa cada vez (por el motivo que sea).
Josh Stodola
Sin embargo, todavía voy a hacer +1, porque veo que este es un método eficaz para ocultar una variable global
Josh Stodola
Puede crear un iframe para cada carga para mantener las devoluciones de llamada individuales.
Justin Johnson
1
Creo que esta es, en última instancia, una gran respuesta, un poco nublada por los requisitos del ejemplo en particular. Básicamente, la idea es crear una plantilla (objeto) y usar 'nuevo' para instanciarlo. Esta es probablemente la mejor respuesta porque realmente evita una variable global, es decir, sin compromiso
PandaWood
3

Otra forma de hacer esto es crear un objeto y luego agregarle métodos.

var object = {
  a = 21,
  b = 51
};

object.displayA = function() {
 console.log(object.a);
};

object.displayB = function() {
 console.log(object.b);
};

De esta manera, solo se expone el objeto 'obj' y los métodos que se le atribuyen. Es equivalente a agregarlo en el espacio de nombres.

Ajay Poshak
fuente
2

Algunas cosas estarán en el espacio de nombres global, es decir, cualquier función que esté llamando desde su código JavaScript en línea.

En general, la solución es envolver todo en un cierre:

(function() {
    var uploadCount = 0;
    function startupload() {  ...  }
    document.getElementById('postHere').onload = function() {
        uploadCount ++;
        if (uploadCount > 1) startUpload();
    };
})();

y evitar el controlador en línea.

Palanqueta
fuente
El controlador en línea es necesario en esta situación. Cuando el formulario se envía a iframe, su controlador de carga configurado mediante programación no se activará.
Josh Stodola
1
@Josh: ¿de verdad? iframe.onload = ...no es equivalente a <iframe onload="..."?
Crescent Fresh
2

El uso de cierres puede estar bien para proyectos pequeños o medianos. Sin embargo, para proyectos grandes, es posible que desee dividir su código en módulos y guardarlos en diferentes archivos.

Por lo tanto, escribí el complemento jQuery Secret para resolver el problema.

En su caso, con este complemento, el código se parecería al siguiente.

JavaScript:

// Initialize uploadCount.
$.secret( 'in', 'uploadCount', 0 ).

// Store function disableAllFileInputs.
secret( 'in', 'disableAllFileInputs', function(){
  // Code for 'disable all file inputs' goes here.

// Store function startUpload
}).secret( 'in', 'startUpload', function(){
    // 'this' points to the private object in $.secret
    // where stores all the variables and functions
    // ex. uploadCount, disableAllFileInputs, startUpload.

    var fil = document.getElementById( 'FileUpload' + uploadCount);

    if(!fil || fil.value.length == 0) {
        alert( 'Finished!' );
        document.forms[0].reset();
        return;
    }

    // Use the stored disableAllFileInputs function
    // or you can use $.secret( 'call', 'disableAllFileInputs' );
    // it's the same thing.
    this.disableAllFileInputs();
    fil.disabled = false;

    // this.uploadCount is equal to $.secret( 'out', 'uploadCount' );
    alert( 'Uploading file ' + this.uploadCount );
    document.forms[0].submit();

// Store function iframeOnload
}).secret( 'in', 'iframeOnload', function(){
    this.uploadCount++;
    if( this.uploadCount > 1 ) this.startUpload();
});

window.onload = function() {
    var frm = document.forms[0];

    frm.target = "postMe";
    frm.onsubmit = function() {
        // Call out startUpload function onsubmit
        $.secret( 'call', 'startUpload' );
        return false;
    }
}

Marcado relevante:

<iframe src="test.htm" name="postHere" id="postHere" onload="$.secret( 'call', 'iframeOnload' );"></iframe>

Abra su Firebug , no encontrará globales en absoluto, ni siquiera la función :)

Para obtener la documentación completa, consulte aquí .

Para ver una página de demostración, consulte esto .

Código fuente en GitHub .

ben
fuente
1

Utilice cierres. Algo como esto le da un alcance diferente al global.

(function() {
    // Your code here
    var var1;
    function f1() {
        if(var1){...}
    }

    window.var_name = something; //<- if you have to have global var
    window.glob_func = function(){...} //<- ...or global function
})();
NilColor
fuente
Mi enfoque en el pasado ha sido definir un objeto de variables globales específico y adjuntar todas las variables globales a ese. ¿Cómo envolvería esto en un cierre? Lamentablemente, el límite del cuadro de comentarios no me permite incrustar código típico.
David Edwards
1

Para "asegurar" variables globales induviduales:

function gInitUploadCount() {
    var uploadCount = 0;

    gGetUploadCount = function () {
        return uploadCount; 
    }
    gAddUploadCount= function () {
        uploadCount +=1;
    } 
}

gInitUploadCount();
gAddUploadCount();

console.log("Upload counter = "+gGetUploadCount());

Soy un novato en JS, actualmente estoy usando esto en un proyecto. (agradezco cualquier comentario y crítica)

Koenees
fuente
1

Lo uso de esta manera:

{
    var globalA = 100;
    var globalB = 200;
    var globalFunc = function() { ... }

    let localA = 10;
    let localB = 20;
    let localFunc = function() { ... }

    localFunc();
}

Para todos los ámbitos globales use 'var', y para los ámbitos locales use 'let'.

Ashwin
fuente