El alcance y el contexto en JavaScript son exclusivos del lenguaje, gracias en parte a la flexibilidad que aportan. Cada función tiene un contexto y alcance variables diferentes. Estos conceptos subyacen a algunos patrones de diseño poderosos en JavaScript. Sin embargo, esto también genera una gran confusión para los desarrolladores. A continuación se revelan exhaustivamente las diferencias entre contexto y alcance en JavaScript, y cómo los utilizan varios patrones de diseño.
contexto vs alcance
Lo primero que hay que aclarar es que contexto y alcance son conceptos diferentes. A lo largo de los años, he notado que muchos desarrolladores a menudo confunden estos dos términos y describen incorrectamente uno como el otro. Para ser justos, estos términos se han vuelto muy confusos.
Cada llamada a función tiene un alcance y un contexto asociados. Básicamente, el alcance se basa en funciones y el contexto se basa en objetos. En otras palabras, el alcance está relacionado con el acceso de variables en cada llamada a función y cada llamada es independiente. El contexto es siempre el valor de la palabra clave this, que es una referencia al objeto que llama al código ejecutable actual.
alcance variable
Las variables se pueden definir en ámbitos locales o globales, lo que da como resultado el acceso a variables en tiempo de ejecución desde diferentes ámbitos. Las variables globales deben declararse fuera del cuerpo de la función, existir durante todo el proceso en ejecución y se puede acceder a ellas y modificarlas en cualquier ámbito. Las variables locales solo se definen dentro del cuerpo de la función y tienen un alcance diferente para cada llamada de función. Este tema es la asignación, evaluación y operación de valores solo dentro de la llamada, y no se puede acceder a los valores fuera del alcance.
Actualmente, JavaScript no admite el alcance a nivel de bloque. El alcance a nivel de bloque se refiere a la definición de variables en bloques de declaraciones, como declaraciones if, declaraciones de cambio, declaraciones de bucle, etc. Esto significa que no se puede acceder a las variables fuera del bloque de declaraciones. Actualmente, se puede acceder a cualquier variable definida dentro de un bloque de declaración fuera del bloque de declaración. Sin embargo, esto cambiará pronto, ya que la palabra clave let se agregó oficialmente a la especificación ES6. Úselo en lugar de la palabra clave var para declarar variables locales como alcance a nivel de bloque.
"este" contexto
El contexto suele depender de cómo se llama una función. Cuando se llama a una función como método en un objeto, esto se establece en el objeto en el que se llama al método:
Copie el código de código de la siguiente manera:
objeto var = {
foo: función(){
alerta(este === objeto);
}
};
objeto.foo(); // verdadero
El mismo principio se aplica al llamar a una función para crear una instancia de un objeto usando el nuevo operador. Cuando se llama de esta manera, el valor de this se establecerá en la instancia recién creada:
Copie el código de código de la siguiente manera:
función foo(){
alerta(esto);
}
foo() // ventana
nuevo foo() // foo
Al llamar a una función independiente, se establecerá en el contexto global o en el objeto de ventana (si está en un navegador) de forma predeterminada. Sin embargo, si la función se ejecuta en modo estricto ("usar estricto"), el valor de esto se establecerá en indefinido de forma predeterminada.
Contexto de ejecución y cadena de alcance.
JavaScript es un lenguaje de subproceso único, lo que significa que sólo puede hacer una cosa a la vez en el navegador. Cuando el intérprete de JavaScript ejecuta código inicialmente, primero utiliza de forma predeterminada el contexto global. Cada llamada a una función crea un nuevo contexto de ejecución.
A menudo se produce confusión aquí. El término "contexto de ejecución" aquí significa alcance, no contexto como se analizó anteriormente. Este es un nombre deficiente, pero el término está definido por la especificación ECMAScript y no tiene más remedio que cumplirla.
Cada vez que se crea un nuevo contexto de ejecución, se agrega a la parte superior de la cadena de alcance y se convierte en la pila de ejecución o de llamadas. El navegador siempre se ejecuta en el contexto de ejecución actual en la parte superior de la cadena de alcance. Una vez completado, (el contexto de ejecución actual) se elimina de la parte superior de la pila y el control se devuelve al contexto de ejecución anterior. Por ejemplo:
Copie el código de código de la siguiente manera:
función primero(){
segundo();
función segunda(){
tercero();
función tercera(){
cuatro();
función cuarta(){
//hacer algo
}
}
}
}
primero();
La ejecución del código anterior hará que las funciones anidadas se ejecuten de arriba a abajo hasta la cuarta función. En este momento, la cadena de alcance de arriba a abajo es: cuarto, tercero, segundo, primero, global. La cuarta función puede acceder a variables globales y a cualquier variable definida en la primera, segunda y tercera función como sus propias variables. Una vez que la cuarta función complete la ejecución, el cuarto contexto se eliminará de la parte superior de la cadena de alcance y la ejecución volverá a la tercera función. Este proceso continúa hasta que todo el código haya completado la ejecución.
Los conflictos de nombres de variables entre diferentes contextos de ejecución se resuelven ascendiendo en la cadena de alcance, de local a global. Esto significa que las variables locales con el mismo nombre tienen mayor prioridad en la cadena de alcance.
En pocas palabras, cada vez que intenta acceder a una variable en el contexto de ejecución de la función, el proceso de búsqueda siempre comienza desde el propio objeto variable. Si la variable que está buscando no se encuentra en su propio objeto variable, continúe buscando en la cadena de alcance. Subirá en la cadena de alcance y examinará cada objeto variable de contexto de ejecución para encontrar un valor que coincida con el nombre de la variable.
cierre
Se forma un cierre cuando se accede a una función anidada fuera de su definición (alcance) para que pueda ejecutarse después de que regrese la función externa. (El cierre) mantiene (en la función interna) el acceso a variables locales, argumentos y declaraciones de funciones en la función externa. La encapsulación nos permite ocultar y proteger el contexto de ejecución del alcance externo, al tiempo que expone la interfaz pública a través de la cual se pueden realizar más operaciones. Un ejemplo simple se ve así:
Copie el código de código de la siguiente manera:
función foo(){
var local = 'variable privada';
devolver barra de funciones(){
regresar local;
}
}
var getLocalVariable = foo();
getLocalVariable() // variable privada
Uno de los tipos de cierres más populares es el conocido patrón modular. Te permite burlarte de miembros públicos, privados y privilegiados:
Copie el código de código de la siguiente manera:
var Módulo = (función(){
var propiedad privada = 'foo';
función método privado (argumentos) {
//hacer algo
}
devolver {
propiedad pública: "",
método público: función (argumentos) {
//hacer algo
},
Método privilegiado: función (argumentos) {
método privado(argumentos);
}
}
})();
En realidad, los módulos son algo similares a los singleton: agregan un par de paréntesis al final y los ejecutan inmediatamente después de que el intérprete termina de interpretarlos (ejecute la función inmediatamente). Los únicos miembros externos disponibles del contexto de ejecución de cierre son los métodos y propiedades públicos en el objeto devuelto (como Module.publicMethod). Sin embargo, todas las propiedades y métodos privados existirán durante todo el ciclo de vida del programa, porque el contexto de ejecución está protegido (cierres) y la interacción con las variables se realiza a través de métodos públicos.
Otro tipo de cierre se denomina expresión de función IIFE de invocación inmediata, que no es más que una función anónima autoinvocada en el contexto de la ventana.
Copie el código de código de la siguiente manera:
función (ventana) {
var a = 'foo', b = 'barra';
función privada(){
//hacer algo
}
ventana.Módulo = {
público: función(){
//hacer algo
}
};
})(este);
Esta expresión es muy útil para proteger el espacio de nombres global. Todas las variables declaradas dentro del cuerpo de la función son variables locales y persisten durante todo el entorno de ejecución mediante cierres. Esta forma de encapsular el código fuente es muy popular tanto para programas como para marcos, y generalmente expone una única interfaz global para interactuar con el mundo exterior.
Llama y aplica
Estos dos métodos simples, integrados en todas las funciones, permiten que las funciones se ejecuten en un contexto personalizado. La función de llamada requiere una lista de parámetros, mientras que la función de aplicación le permite pasar los parámetros como una matriz:
Copie el código de código de la siguiente manera:
usuario de función (primero, último, edad) {
//hacer algo
}
usuario.call(ventana, 'John', 'Doe', 30);
usuario.apply(ventana, ['John', 'Doe', 30]);
El resultado de la ejecución es el mismo, la función de usuario se llama en el contexto de la ventana y se proporcionan los mismos tres parámetros.
ECMAScript 5 (ES5) introdujo el método Function.prototype.bind para controlar el contexto, que devuelve una nueva función que está vinculada permanentemente al primer argumento del método de vinculación, independientemente de cómo se llame la función. Corrige el contexto de la función mediante cierres. Aquí hay una solución para los navegadores que no la admiten:
Copie el código de código de la siguiente manera:
if(!('vincular' en Function.prototype)){
Función.prototipo.bind = función(){
var fn = esto, contexto = argumentos[0], args = Array.prototype.slice.call(argumentos, 1);
función de retorno(){
devolver fn.apply(contexto, argumentos);
}
}
}
Se usa comúnmente en pérdida de contexto: orientado a objetos y procesamiento de eventos. Esto es necesario porque el método addEventListener del nodo siempre mantiene el contexto de ejecución de la función como el nodo al que está vinculado el controlador de eventos, lo cual es importante. Sin embargo, si utiliza técnicas avanzadas orientadas a objetos y necesita mantener el contexto de la función de devolución de llamada como una instancia del método, debe ajustar manualmente el contexto. Esta es la conveniencia que brinda bind:
Copie el código de código de la siguiente manera:
función MiClase(){
this.element = document.createElement('div');
this.element.addEventListener('hacer clic', this.onClick.bind(esto), falso);
}
MyClass.prototype.onClick = función(e){
//hacer algo
};
Al mirar hacia atrás en el código fuente de la función de vinculación, es posible que observe la siguiente línea de código relativamente simple, que llama a un método en Array:
Copie el código de código de la siguiente manera:
Array.prototype.slice.call(argumentos, 1);
Curiosamente, es importante tener en cuenta aquí que el objeto de argumentos no es en realidad una matriz; sin embargo, a menudo se describe como un objeto similar a una matriz, muy parecido a la lista de nodos (el resultado devuelto por el método document.getElementsByTagName()). Contienen atributos de longitud y los valores se pueden indexar, pero aún no son matrices porque no admiten métodos de matriz nativos como cortar y empujar. Sin embargo, dado que se comportan de manera similar a las matrices, los métodos de matriz pueden llamarse y secuestrarse. Si desea ejecutar métodos de matriz en un contexto similar a una matriz, siga el ejemplo anterior.
Esta técnica de llamar a métodos de otros objetos también se aplica a los orientados a objetos, al emular la herencia clásica (herencia de clases) en JavaScript:
Copie el código de código de la siguiente manera:
MiClase.prototipo.init = función(){
// llama al método init de la superclase en el contexto de la instancia "MyClass"
MySuperClass.prototype.init.apply(esto, argumentos);
}
Podemos reproducir este poderoso patrón de diseño llamando a métodos de la superclase (MySuperClass) en instancias de la subclase (MyClass).
en conclusión
Es muy importante comprender estos conceptos antes de comenzar a aprender patrones de diseño avanzados, ya que el alcance y el contexto juegan un papel importante y fundamental en el JavaScript moderno. Ya sea que hablemos de cierres, orientación a objetos y herencia o de varias implementaciones nativas, el contexto y el alcance juegan un papel importante. Si su objetivo es dominar el lenguaje JavaScript y obtener una comprensión profunda de sus componentes, el alcance y el contexto deben ser su punto de partida.
Suplemento del traductor
La función de vinculación implementada por el autor está incompleta. No se pueden pasar parámetros al llamar a la función devuelta por vinculación. El siguiente código soluciona este problema:
Copie el código de código de la siguiente manera:
if(!('vincular' en Function.prototype)){
Función.prototipo.bind = función(){
var fn = esto, contexto = argumentos[0], args = Array.prototype.slice.call(argumentos, 1);
función de retorno(){
return fn.apply(context, args.concat(argumentos));//arreglado
}
}
}