You Dont Know JS (Esp)
You Dont Know JS (Esp)
Índice
Introducción
I- Up & Going
0- Prefacio
1- En la programación
1.1 Código
1.2 Inténtalo tú mismo
1.3 Operadores
1.4 Valores y Tipos
1.5 Comentarios del Código
1.6 Variables
1.7 Bloques
1.8 Condicionales
1.9 Bucles
1.10 Funciones
1.11 Scope (Ámbito)
1.12 Práctica
1.13 - Revisión
2- En Javascript
2.1 Valores y Tipos
2.2 Variables
2.3 Condicionales
2.4 Modo estricto
2.5 Funciones como Valores
2.6 Identificador This
2.7 Prototypes
2.8 Lo Viejo y Lo Nuevo
2.9 Non-JavaScript
2.10 Revisión
3- En YDKJS
3.1 Scope & Closures
3.2 This & Object Prototypes
3.3 Tipos & Gramática
3.4 Async & Performance
3.5 ES6 & Más allá
3.6 Revisión
II- Scope & Closures
0- Prefacio
1- ¿Qué es el Scope?
1.1 Teoría del Compilador
1.2 Entendiendo el Scope
1.3 Scopes Anidados
1.4 Errores
1.5 Revisión
2- Lexical Scope
2.1 Tiempo de Lex
2.2 Trucos léxicos
2.3 Revisión
3- Function vs. Block Scope
3.1 Ámbito de las funciones
3.2 Ocultación en el ámbito común
3.3 Funciones como ámbitos
3.4 Bloques como ámbitos
3.5 Revisión (TL; DR)
4- Hoisting
4.1 ¿El Huevo o la Gallina?
4.2 El compilador pega de nuevo
4.3 Funciones Primero
4.4 Revisión
5- Scope Closure
5.1 Ilustración
5.2 Nitty Gritty
5.3 Ahora puedo ver
5.4 Loops + Closure
5.5 Módulos
5.6 Revisión
6- Scope Dinámico
7- Ámbito de bloque de Polyfilling
7.1 Traceur
7.2 Bloques implícitos vs. explícitos
7.3 Rendimiento
8- Lexical-this
III- this & Object Prototypes
0- Prefacio
1- this o That?
1.1 ¿Porque this?
1.2 Confusiones
1.3 ¿Que es this?
1.4 Revisión
2- this, todo tiene sentido ahora!
2.1 Sitio de llamada
2.2 Nada más que reglas
2.3 Todo en orden
Introducción
Esta es una traducción de la serie de libros de You Don't Know JS (book series), la cual es
una serie de 6 libros que navegan profundamente en los mecanismos básicos y avanzados
del lenguaje JavaScript. La primera edición de la serie está ahora completa.
Si deseas hacer alguna corrección o aporte (ya que seguro podría tener errores de
traducción o interpretación), puedes hacer un fork al repo principal y solicitar un pull-
request, apreciaré dicha ayuda!
Aunque ésta es una serie de libros de Javascript Avanzado, tambien puede empezarlos con
un nivel básico.
Tal vez fue una lengua extranjera, como italiano o alemán. O tal vez un editor de gráficos,
como Photoshop. O una técnica de cocción o carpintería o una rutina de ejercicios. Quiero
que recuerdes esa sensación cuando finalmente lo conseguiste: el momento que alumbró
tu bombilla. Cuando las cosas pasaban de borrosas a cristalinas, cuando lo dominabas
veías o entendías la diferencia entre sustantivos masculinos y femeninos en francés.
¿Como se sintió? Bastante asombroso, ¿verdad?
Ahora quiero que vayas un poco más lejos en tu memoria antes de que aprendieras una
nueva habilidad. ¿Cómo se sintió eso? Probablemente un poco intimidante y tal vez un
poco frustrante, ¿verdad? En un momento, no sabíamos las cosas que sabemos ahora y
eso está bien; Todos empezamos en alguna parte. Aprender material nuevo es una
aventura emocionante, especialmente si usted está buscando aprender el tema de manera
eficiente.
Enseño muchas clases de codificación a principiantes. Los estudiantes que toman mis
clases a menudo han tratado de aprender por sí mismos temas como HTML o JavaScript
por medio de la lectura de blogs o copiar y pegar código, pero no han sido capaces de
dominar realmente el material que les permita codificar el resultado deseado. Y debido a
que no comprenden realmente los entresijos de ciertos temas de codificación, no pueden
escribir código de gran alcance o depurar su propio trabajo, ya que realmente no entienden
lo que está sucediendo.
Siempre creo en enseñar mis clases de la manera correcta, es decir, enseño estándares
web, marcado semántico, código bien comentado y otras buenas prácticas. Cubro el tema
de una manera minuciosa para explicar los comos y los porqués, sin sacar el código para
copiar y pegar. Cuando te esfuerzas en comprender tu código, creas mejor trabajo y te
vuelves mejor en lo que haces. El código ya no es solo tu trabajo, es tu oficio. Esta es la
razón por la que amo Up & Going. Kyle nos lleva a una inmersión profunda a través de la
sintaxis y la terminología para dar una gran introducción a JavaScript sin cortes. Este libro
no navega sobre la superficie del lenguaje, sino que realmente nos permite entender los
conceptos que estaremos escribiendo.
Cuanto más se exponen a JavaScript, más claro se vuelve. Palabras como closures,
objetos y métodos pueden parecer fuera de alcance para usted ahora, pero este libro
ayudará a entender estos términos con claridad. Quiero que mantenga esos dos
sentimientos en mente antes y después de que aprenda algo al comenzar este libro. Puede
parecer desalentador, pero has elegido este libro porque estás empezando un viaje
impresionante para perfeccionar tu conocimiento. Up & Going es el comienzo de nuestro
camino hacia la comprensión de la programación. Disfrute de los momentos bombilla!
Jenn Lukas
Jennlukas.com, @jennlukas
Consultora de front-end
0- Prefacio
Estoy seguro de que se dio cuenta, pero "JS" en el título de la serie de libros no es una
abreviatura de palabras utilizadas para maldecir sobre JavaScript, aunque maldecir a las
peculiaridades del lenguaje es algo con lo que probablemente todos se pueden identificar.
Desde los primeros días de la web, JavaScript ha sido una tecnología fundamental que ha
impulsado la experiencia interactiva en torno al contenido que consumimos. Mientras que
el apuntador parpadeante y los molestos avisos emergentes podrían ser donde comenzó
JavaScript, casi dos décadas después, la tecnología y la capacidad de JavaScript ha
crecido en muchos ámbitos de magnitud, y pocos dudan de su importancia en el corazón
de la plataforma de software más ampliamente disponible: La web.
Pero como lenguaje, ha sido perpetuamente un blanco para mucha crítica, debido en parte
a su "herencia", pero más aún debido a su filosofía de diseño. Incluso el nombre evoca,
como Brendan Eich una vez lo dijo, "el hermano bebe estúpido" comparado junto a su
hermano mayor más maduro "Java". Pero el nombre es simplemente un accidente de la
política y el marketing. Los dos idiomas son muy diferentes en muchos aspectos
importantes. "JavaScript" está relacionado con "Java" como "Carnival" es a "Car".
Mientras que JavaScript es quizás uno de los idiomas más fáciles de poner en
funcionamiento, sus excentricidades hacen que el dominio sólido del lenguaje sea una
ocurrencia mucho menos común que en muchos otros idiomas. Cuando se necesita un
conocimiento bastante profundo de un lenguaje como C o C++ para escribir un programa a
gran escala, la producción a gran escala de JavaScript puede, y con frecuencia lo es,
apenas se raya con la superficie de lo que el lenguaje puede realmente hacer.
Es simultáneamente un lenguaje sencillo y fácil de usar que tiene un amplio atractivo y una
colección compleja y matizada de mecánica del lenguaje que sin un estudio cuidadoso
escapará a la verdadera comprensión, incluso de los más experimentados desarrolladores
de JavaScript.
Misión
Si bien este subconjunto ha sido conocido como "The Good Parts", le pediría a usted,
querido lector, que lo considere "The Easy Parts", "The Safe Parts" o incluso "The
Incomplete Parts".
No estoy contento, ni debes estar, en parar una vez que algo funciona correctamente, y no
saber realmente por qué. Le desafío a viajar por ese "camino costoso" y abrazar todo lo que
JavaScript es y puede hacer. Con ese conocimiento, ninguna técnica, ningún framework,
ningún acrónimo popular de moda, estará más allá de su comprensión.
Estos libros toman partes específicas del lenguaje que son las comúnmente mal
entendidas o no comprendidas, y se sumerge muy profundamente y exhaustivamente en
ellas. Usted debe termina la lectura con una firme confianza en su comprensión, no sólo de
lo teórico, sino lo práctico "lo que necesita saber".
El JavaScript que usted conoce ahora mismo es probablemente la partes dada a usted por
otros que han sido "quemados" por una comprensión incompleta. Ese JavaScript es sólo
una sombra del verdadero lenguaje. Realmente no sabes JavaScript, pero si crees en esta
serie, lo harás. Sigue leyendo, amigos. JavaScript te espera.
Resumen
Nota: Muchos de los ejemplos en este libro asumen los entornos de motor de JavaScript
modernos (y futuros), como ES6. Es posible que algunos códigos no funcionen como se
describe si se ejecutan en motores anteriores (pre-ES6).
1- En la programación
Bienvenido a la serie You Do not Know JS (YDKJS).
Este libro comienza explicando los principios básicos de la programación a un nivel muy
alto. Está pensado principalmente si usted está comenzando YDKJS con poca o ninguna
experiencia de programación, y está mirando a estos libros para ayudarle a comenzar a lo
largo de un camino a la comprensión de la programación a través del lente de JavaScript.
El capítulo 1 debe ser abordado como un resumen rápido de las cosas que usted querrá
aprender más y practicar para entrar en la programación. También hay muchos otros
fantásticos recursos de introducción a la programación que pueden ayudarle a profundizar
en estos temas y le animo a aprender de ellos además de este capítulo.
Una vez que se sienta cómodo con los conceptos básicos generales de programación, el
Capítulo 2 le ayudará a familiarizarse con el sabor de la programación de JavaScript. El
Capítulo 2 presenta lo que es JavaScript, pero de nuevo, no es una guía completa - ¡eso se
lo dejamos al resto de los libros de YDKJS!
Si ya está bastante cómodo con JavaScript, primero eche un vistazo al Capítulo 3 como un
breve vistazo de lo que puede esperar de YDKJS, luego salte siga adelante!
1.1 Código
Empecemos desde el principio.
Declaraciones
a = b * 2;
Los caracteres a y b se llaman variables (vea "Variables"), que son como simples cajas
en las que puede almacenar cualquiera de sus cosas. En los programas, las variables
contienen valores (como el número 42) que debe usar el programa. Piense en ellos como
marcadores de posición simbólicos para los valores mismos.
Por el contrario, el 2 es sólo un valor en sí mismo, llamado un valor literal, porque está solo
sin ser almacenado en una variable.
Los caracteres = y * son operadores (ver "Operadores") - realizan acciones con valores y
variables tales como asignación y multiplicación matemática.
La mayoría de las declaraciones en JavaScript terminan con un punto y coma ( ; ) al final.
Los programas son sólo colecciones de muchas declaraciones de este tipo, que en
conjunto describen todos los pasos que se requieren para realizar el propósito de su
programa.
Expresiones
Por ejemplo:
a = b * 2;
Esta declaración de una expresión no es muy común o útil, ya que en general no tendría
ningún efecto en el funcionamiento del programa: recuperaría el valor de b y lo
multiplicaría por 2, pero luego no haría nada con ese resultado.
Una sentencia de expresión más común es una instrucción de expresión de llamada (véase
"Funciones"), ya que la sentencia completa es la expresión de llamada de la función en sí:
alert( a );
Ejecutar un programa
Declaraciones como a = b * 2 son útiles para los desarrolladores al leer y escribir, pero
en realidad no están en una forma en que la computadora pueda entender directamente.
Así que una utilidad especial en la computadora (ya sea un intérprete o un compilador) se
utiliza para traducir el código que escribe en comandos que una computadora puede
entender.
Para otros idiomas, la traducción se hace antes de tiempo, llamada compilación del código,
por lo que cuando el programa se ejecuta más tarde, lo que está en ejecución es en realidad
las instrucciones de la computadora ya compilada listo para ejecutarse.
Normalmente, se afirma que JavaScript se interpreta, porque el código fuente de JavaScript
se procesa cada vez que se ejecuta. Pero eso no es del todo exacto. El motor de JavaScript
en realidad compila el programa sobre la marcha y luego ejecuta inmediatamente el código
compilado.
Nota: Para obtener más información sobre la compilación de JavaScript, consulte los dos
primeros capítulos del título Scope & Closures de esta serie.
1.2 Inténtalo tú mismo
Este capítulo va a introducir cada concepto de programación con simples fragmentos de
código, todos escritos en JavaScript (obviamente!).
Sugerencia: Normalmente, puede iniciar la consola del programador con un acceso directo
de teclado o desde un elemento de menú. Para obtener información más detallada acerca
del inicio y el uso de la consola en su navegador favorito, consulte "Dominar de la consola
de herramientas de desarrollo" (http://blog.teamtreehouse.com/mastering-developer-tools-
console). Para escribir varias líneas en la consola a la vez, use <shift> + <enter> para pasar
a la nueva línea siguiente. Una vez que pulse <enter> por sí mismo, la consola ejecutará
todo lo que acaba de escribir.
1 a = 21;
2
3 b = a * 2;
4
5 console.log( b );
Escribir el código anterior en la consola en Chrome debería producir algo como lo siguiente:
Vamos, prueba. La mejor manera de aprender la programación es empezar a codificar!
Salida
Usted puede haber adivinado, pero eso es exactamente cómo imprimimos el texto (aka
salida al usuario) en la consola del navegador. Hay dos características de esa afirmación
que debemos explicar.
En primer lugar, la parte log (b) se denomina llamada de función (véase "Funciones"). Lo
que pasa es que estamos entregando la variable b a esa función, que le pide tomar el valor
de b e imprimirlo en la consola.
Otra forma de crear resultados que puede ver es ejecutar una instrucción de alert(..). Por
ejemplo:
alert( b );
Si ejecuta eso, notará que en lugar de imprimir la salida a la consola, muestra un cuadro
emergente "Aceptar" con el contenido de la variable b. Sin embargo, usar console.log (...)
generalmente hará que el aprendizaje sobre la codificación y ejecución de sus programas
en la consola sea más fácil que usar la alert (..), ya que puede generar muchos valores a la
vez sin interrumpir la interfaz del navegador.
Entrada
Mientras estamos discutiendo la salida, también puede preguntarse sobre la entrada (es
decir, recibir información del usuario).
La forma más común que ocurre es que la página HTML muestre elementos de formulario
(como cuadros de texto) en un usuario al que puedan escribir y, a continuación, utilizar JS
para leer esos valores en las variables del programa.
Pero hay una manera más fácil de obtener información para fines sencillos de aprendizaje
y demostración, como la que estará haciendo a lo largo de este libro. Utilice la función de
prompt (..):
Como ya habrás adivinado, el mensaje que pasas al prompt (..) - en este caso, "Por favor,
dime tu edad:" - se imprime en el popup.
Para mantener las cosas simples mientras estamos aprendiendo conceptos básicos de
programación, los ejemplos en este libro no requerirán entrada. Pero ahora que has visto
cómo usar el prompt (..), si quieres desafiarte puedes intentar usar la entrada en tus
exploraciones de los ejemplos.
1.3 Operadores
Los operadores son la forma en que realizamos acciones sobre variables y valores. Ya
hemos visto dos operadores de JavaScript, el = y el * .
Advertencia: Esto puede parecer un orden inverso extraño para especificar asignación. En
lugar de a = 42, algunos podrían preferir voltear el orden para que el valor de origen esté a
la izquierda y la variable de destino a la derecha, como 42 -> a (JavaScript no válido).
Desafortunadamente, la forma ordenada a = 42, y variaciones similares, es bastante
frecuente en los lenguajes de programación modernos. Si se siente antinatural, sólo pase
algún tiempo ensayando y el orden en su mente lo acostumbrará a ella.
Considere:
1 a = 2;
2 b = a + 1;
Siempre debe declarar la variable por nombre antes de usarla. Pero sólo es necesario
declarar una variable una vez para cada ámbito (ver "Alcance"); Se puede utilizar tantas
veces como sea necesario. Por ejemplo:
1 var a = 20;
2
3 a = a + 1;
4 a = a * 2;
5
6 console.log( a ); // 42
Asignación: = como en a = 2.
Matemáticas: + (adición), - (sustracción), * (multiplicación) y / (división), como en a *
3.
Asignación Compuesta : + =, - =, * =, y / = son operadores compuestos que combinan
una operación matemática con asignación, como en a + = 2 (igual que a = a + 2).
Incremento / Decremento: ++ (incremento), - (decremento), como en a ++ (similar a a =
a + 1).
Acceso a la propiedad del objeto:. Como en console.log (). Los objetos son valores que
contienen otros valores en determinadas ubicaciones denominadas propiedades.
obj.a significa un valor de objeto llamado obj con una propiedad del nombre a. Las
Nota: Para obtener más detalles y cobertura de los operadores que no se mencionan aquí,
consulte las "Expresiones y operadores" de Mozilla Developer Network (MDN)
(https://developer.mozilla.org/en-US/docs/Web/JavaScript / Guía /
Expresiones_y_Operadores).
1.4 Valores y Tipos
Si le preguntas a un empleado en una tienda de teléfono cuánto cuesta un teléfono
determinado, y dicen "noventa y nueve, noventa y nueve" (es decir, $ 99.99), te están dando
una cifra numérica real que representa lo que vas a necesitar pagar (más impuestos) para
comprarlo. Si usted quiere comprar dos de esos teléfonos, puede hacer fácilmente las
matemáticas mentales para duplicar ese valor para obtener $ 199.98 por su costo base.
Si ese mismo empleado coge otro teléfono similar, pero dice que es "gratis", no le están
dando un número, sino otro tipo de representación de su costo esperado ($ 0.00) - la
palabra "free ."
Cuando más tarde pregunte si el teléfono incluye un cargador, esa respuesta sólo podría
haber sido "sí" o "no".
Los valores que se incluyen directamente en el código fuente se llaman literales. Los
literales de cadenas están rodeados por comillas dobles "..." o comillas simples ('...') - la
única diferencia es la preferencia estilística. El número y los literales booleanos se
presentan como es (es decir, 42, true, etc.).
Considere:
1 "I am a string";
2 'I am also a string';
3
4 42;
5
6 true;
7 false;
Más allá de los tipos de valores string / number / boolean, es común que los lenguajes de
programación proporcionen arrays, objetos, funciones y más. Cubriremos mucho más
sobre valores y tipos a lo largo de este capítulo y el siguiente.
JavaScript proporciona varias facilidades diferentes para forzar la coerción entre tipos. Por
ejemplo:
1 var a = "42";
2 var b = Number( a );
3
4 console.log( a ); // "42"
5 console.log( b ); // 42
El uso de Number (..) (una función incorporada) como se muestra es una coerción explícita
de cualquier otro tipo al tipo de número. Eso debería ser bastante sencillo.
Pero un tema polémico es lo que sucede cuando se intenta comparar dos valores que ya no
son del mismo tipo, lo que requeriría coerción implícita.
Al comparar la cadena "99.99" con el número 99.99, la mayoría de la gente estaría de
acuerdo en que son equivalentes. Pero no son exactamente iguales, ¿verdad? Es el mismo
valor en dos representaciones diferentes, dos tipos diferentes. Se podría decir que son
"vagamente iguales", ¿no?
Así que si usas el operador == loose igual para hacer la comparación "99.99" == 99.99,
JavaScript convertirá el lado izquierdo "99.99" a su número equivalente 99.99. La
comparación entonces se convierte en 99.99 == 99.99, que es por supuesto verdad.
Si bien está diseñado para ayudarlo, la coerción implícita puede crear confusión si no se ha
tomado el tiempo para aprender las reglas que rigen su comportamiento. La mayoría de los
desarrolladores de JS nunca lo tienen, por lo que la sensación común es que la coerción
implícita es confusa y daña los programas con errores inesperados, por lo que debe
evitarse. Incluso a veces se llama un defecto en el diseño del lenguaje.
Sin embargo, la coerción implícita es un mecanismo que se puede aprender, y además debe
ser aprendido por cualquiera que desee tomar la programación de JavaScript en serio. ¡No
sólo no confunde una vez que aprende las reglas, él puede realmente hacer sus programas
mejores! El esfuerzo vale la pena.
Nota: Para obtener más información sobre la coerción, consulte el Capítulo 2 de este título
y el Capítulo 4 del título de Tipos y Gramática de esta serie.
1.5 Comentarios del código
El empleado de la tienda telefónica puede anotar algunas notas sobre las características
de un teléfono recién lanzado o sobre los nuevos planes que su empresa ofrece. Estas
notas son sólo para el empleado - no son para los clientes leer. Sin embargo, estas notas
ayudan al empleado a hacer su trabajo mejor documentando de los comos y los porqués
de lo que ella debe decir a los clientes.
Una de las lecciones más importantes que puede aprender sobre la escritura de código es
que no es sólo para la computadora. El código es mucho más, si no más, para el
desarrollador tanto como lo es para el compilador.
Usted debe esforzarse no sólo en escribir programas que funcionen correctamente, sino
programas que tienen sentido cuando se examinan. Puede recorrer un largo camino en ese
esfuerzo eligiendo buenos nombres para sus variables (ver "Variables") y funciones (ver
"Funciones").
Pero otra parte importante es los comentarios del código. Éstos son pedacitos de texto en
su programa que se insertan puramente para explicar cosas a un ser humano. El intérprete
/ compilador siempre ignorará estos comentarios.
Hay un montón de opiniones sobre lo que debe ser un buen código comentado; No
podemos definir reglas universales absolutas. Pero algunas observaciones y directrices son
muy útiles:
Considere:
El // comentario de una sola línea es apropiado si vas a poner un comentario justo encima
de una sola sentencia, o incluso al final de una línea. Todo en la línea después de // se trata
como el comentario (por lo tanto es ignorado por el compilador), todo el camino hasta el
final de la línea. No hay ninguna restricción a lo que puede aparecer dentro de un
comentario de una sola línea.
Considere:
Otros idiomas enfatizan tipos para valores en lugar de variables. La tipificación débil,
también conocida como escritura dinámica, permite que una variable contenga cualquier
tipo de valor en cualquier momento. Se suele citar como un beneficio para la flexibilidad del
programa al permitir que una única variable represente un valor sin importar la forma de
tipo que el valor pueda tomar en un momento dado en el flujo lógico del programa.
JavaScript utiliza el último enfoque, el tipo dinámico, lo que significa que las variables
pueden contener valores de cualquier tipo sin ningún tipo de aplicación.
La variable amount comienza teniendo el número 99.99, y luego tiene el resultado del
número de la cantidad * 2, que es 199.98.
El primer comando console.log (..) tiene que coaccionar implícitamente ese valor numérico
a una cadena para imprimirlo.
De cualquier manera, notará que amount tiene un valor de ejecución que cambia a lo largo
del programa, ilustrando el propósito principal de las variables: administrar el estado del
programa.
En otras palabras, state es el seguimiento de los cambios a los valores a medida que su
programa se ejecuta.
Otro uso común de variables es para centralizar la configuración de valores. Esto se llama
más comúnmente constantes, cuando usted declara una variable con un valor y la
intención es que el valor no cambie a través del programa.
Usted declara estas constantes, a menudo en la parte superior de un programa, por lo que
es conveniente para usted tener un lugar para ir a alterar un valor si es necesario. Por
convención, las variables JavaScript como constantes suelen ser mayúsculas, con guiones
bajos _ entre varias palabras.
La variable TAX_RATE es sólo constante por convención - no hay nada especial en este
programa que impide que se cambie. Pero si la ciudad eleva la tasa del impuesto a las
ventas al 9%, podemos actualizar fácilmente nuestro programa estableciendo el valor
TAX_RATE asignado a 0,09 en un lugar, en lugar de buscar y encontrar muchas ocurrencias
del valor 0.08 puesto a lo largo del programa y actualizandolo en todas ellas .
1 // as of ES6:
2 const TAX_RATE = 0.08;
3
4 var amount = 99.99;
5
6 // ..
Las constantes son útiles al igual que las variables con valores sin cambios, excepto que
las constantes también evitan el cambio accidental de valor en algún otro lugar después de
la configuración inicial. Si intentó asignar un valor diferente a TAX_RATE después de esa
primera declaración, su programa rechazaría el cambio (y en modo estricto, fallará con un
error; consulte "Modo estricto" en el Capítulo 2).
Por cierto, ese tipo de "protección" contra los errores es similar al tipo de tipificación
estática, por lo que puede ver por qué los tipos estáticos en otros idiomas pueden ser
atractivos!
Nota: Para obtener más información sobre cómo se pueden utilizar diferentes valores en
las variables en sus programas, consulte el título Tipos y Gramática de esta serie.
1.7 Bloques
El cliente de la tienda telefónica debe pasar por una serie de pasos para completar la
compra mientras compra su nuevo teléfono.
Del mismo modo, en código a menudo es necesario agrupar una serie de declaraciones en
conjunto, que a menudo llamamos un bloque. En JavaScript, un bloque se define
envolviendo una o más instrucciones dentro de un par de llaves {..} . Considere:
Este tipo de bloque independiente general {..} es válido, pero no es tan comúnmente
visto en los programas de JS. Por lo general, los bloques se adjuntan a alguna otra
instrucción de control, como una instrucción if (consulte "Condicionales") o un bucle
(consulte "Loops"). Por ejemplo:
Vamos a explicar las declaraciones if en la siguiente sección, pero como puede ver, el
bloque {..} con sus dos declaraciones se adjunta a if (amount > 10) ; Las
declaraciones dentro del bloque sólo se procesarán si pasa el condicional.
Nota: A diferencia de la mayoría de otras sentencias como console.log(amount); , una
instrucción de bloque no necesita un punto y coma (;) para concluirlo.
1.8 Condicionales
"¿Desea agregar los protectores de pantalla adicionales a su compra, por $ 9.99?" El
empleado de la tienda telefónica le ha pedido que tome una decisión. Y puede que necesite
consultar primero el estado actual de su cartera o cuenta bancaria para responder a esa
pregunta. Pero, obviamente, esto es sólo una simple pregunta "sí o no".
La instrucción if requiere una expresión entre los paréntesis () que puede tratarse
como verdadero o falso. En este programa, hemos proporcionado la expresión
amount < bank_balance , que de hecho se evaluará a true o false dependiendo de la
Como discutimos en "Valores y Tipos" anteriormente, los valores que no son ya de un tipo
esperado son a menudo coaccionados a ese tipo. La sentencia if espera un booleano,
pero si se pasa algo que no sea ya booleano, la coerción se producirá.
JavaScript define una lista de valores específicos que se consideran "falseados" porque
cuando se coaccionan a un booleano, se convierten en falsos - estos incluyen valores
como 0 y "" . Cualquier otro valor que no esté en la lista de "falsos" es automáticamente
"verdad" - cuando se coacciona a un booleano se convierten en verdaderos. Los valores
Truthy incluyen cosas como 99.99 y "free". Vea "Truthy & Falsy" en el Capítulo 2 para más
información.
Los condicionales existen en otras formas además del if . Por ejemplo, la instrucción
switch puede usarse como una abreviatura para una serie de instrucciones if..else
(vea el Capítulo 2). Los bucles (ver "Loops") usan un condicional para determinar si el bucle
debe continuar o detenerse.
Nota: Para obtener información más detallada sobre las coerciones que pueden ocurrir
implícitamente en las expresiones de prueba de condicionales, consulte el Capítulo 4 del
título de Tipos y Gramática de esta serie.
1.9 Bucles
Durante las horas pico, hay una lista de espera para los clientes que necesitan hablar con
el empleado de la tienda telefónica. Aunque todavía hay personas en esa lista, sólo tiene
que seguir sirviendo al próximo cliente.
Repetir un conjunto de acciones hasta que cierta condición falla - en otras palabras, repetir
sólo mientras la condición se mantiene - es el trabajo de los bucles de programación; Los
bucles pueden tomar diferentes formas, pero todos satisfacen este comportamiento
básico.
Un bucle incluye la condición de prueba, así como un bloque (normalmente como {..} ).
Cada vez que se ejecuta el bloque del bucle, se llama iteración.
Por ejemplo, el bucle while y las formas de bucle do..while ilustran el concepto de
repetir un bloque de sentencias hasta que una condición ya no se evalúa como verdadera:
A veces usted está haciendo un bucle con el propósito de contar un cierto conjunto de
números, como de 0 a 9 (diez números). Puede hacerlo estableciendo una variable de
iteración de bucle como i en el valor 0 e incrementándola en 1 cada iteración.
Advertencia: Por una variedad de razones históricas, los lenguajes de programación casi
siempre cuentan las cosas de manera cero, es decir, comenzando con 0 en lugar de 1. Si no
está familiarizado con ese modo de pensar, puede ser muy confuso al principio. Tómese su
tiempo para practicar el conteo empezando por 0 para sentirse más cómodo con él.
1 var i = 0;
2
3 // a `while..true` loop would run forever, right?
4 while (true) {
5 // stop the loop?
6 if ((i <= 9) === false) {
7 break;
8 }
9
10 console.log( i );
11 i = i + 1;
12 }
13 // 0 1 2 3 4 5 6 7 8 9
Advertencia: esto no es una forma práctica en la que desearía utilizar sus bucles. Se
presenta aquí sólo con fines ilustrativos.
Mientras que un while (o do..while ) puede realizar la tarea manualmente, hay otra
forma sintáctica llamada un bucle for sólo para ese propósito:
Como puede ver, en ambos casos el condicional i <= 9 es verdadero para las primeras 10
iteraciones ( i de valores de 0 a 9) de cualquier forma de bucle, pero se convierte en falso
una vez que i es un valor 10.
El bucle for tiene tres cláusulas: la cláusula de inicialización ( var i = 0 ), la cláusula
de prueba condicional ( i <= 9 ) y la cláusula de actualización ( i = i + 1 ). Así que si
vas a contar con tus iteraciones de bucle, es una forma más compacta ya menudo más
fácil de entender y escribir.
Existen otros formularios de bucle especializados que tienen la intención de iterar sobre
valores específicos, como las propiedades de un objeto (véase el capítulo 2), donde la
prueba condicional implícita es justamente si se han procesado todas las propiedades. El
concepto de "bucle hasta que falla una condición" no importa cuál sea la forma del bucle.
1.10 Funciones
El empleado de la tienda de teléfonos probablemente no lleva consigo una calculadora
para calcular los impuestos y la cantidad final de la compra. Esa es una tarea que necesita
definir una vez y reutilizar una y otra vez. Las probabilidades son, que la empresa tenga un
registro de pago (computadora, tableta, etc.) con esas "funciones" incorporadas.
Del mismo modo, su programa casi seguramente querrá dividir las tareas del código en
piezas reutilizables, en lugar de repetir repetidamente repetidamente (juego de palabras!).
La forma de hacerlo es definir una función.
Una función es generalmente una sección con el nombre del código que puede ser
"llamada" por ese nombre, y el código dentro de ella se ejecutará cada vez que se llama.
Considere:
1 function printAmount() {
2 console.log( amount.toFixed( 2 ) );
3 }
4
5 var amount = 99.99;
6
7 printAmount(); // "99.99"
8
9 amount = amount * 2;
10
11 printAmount(); // "199.98"
1 function printAmount(amt) {
2 console.log( amt.toFixed( 2 ) );
3 }
4
5 function formatAmount() {
6 return "$" + amount.toFixed( 2 );
7 }
8
9 var amount = 99.99;
10
11 printAmount( amount * 2 ); // "199.98"
12
13 amount = formatAmount();
14 console.log( amount ); // "$99.99"
Las funciones se usan a menudo para el código que se planea llamar varias veces, pero
también pueden ser útiles sólo para organizar fragmentos de código relacionados en las
colecciones con nombre, incluso si sólo planea llamarlas una vez.
Considere:
Un nombre de variable tiene que ser único dentro del mismo ámbito - no puede haber dos
variables diferentes una al lado del otro. Pero el mismo nombre de variable podría aparecer
en diferentes ámbitos.
1 function one() {
2 // ésta `a` solo pertenece a la funcion `one()`
3 var a = 1;
4 console.log( a );
5 }
6
7 function two() {
8 // ésta `a` solo pertenece a la funcion `two()`
9 var a = 2;
10 console.log( a );
11 }
12
13 one(); // 1
14 two(); // 2
También, un scope puede estar anidado dentro de otro scope, apenas como si un payaso
en una fiesta del cumpleaños sopla encima de un globo dentro de otro globo. Si un scope
está anidado dentro de otro, el código dentro del ámbito más interno puede acceder a las
variables desde cualquiera de los ámbitos.
Considere:
1 function outer() {
2 var a = 1;
3
4 function inner() {
5 var b = 2;
6
7 // puede acceder a ambos `a` y `b` aqui
8 console.log( a + b ); // 3
9 }
10
11 inner();
12
13 // solo puede acceder a `a` aqui
14 console.log( a ); // 1
15 }
16
17 outer();
Las reglas de ámbito léxico dicen que el código en un ámbito puede acceder a la variable
a de ese ámbito o de cualquier ámbito fuera de él.
Por lo tanto, el código dentro de la función inner() tiene acceso a las variables a y b ,
pero el código en outer() sólo tiene acceso a a - no puede acceder a b porque esa
variable sólo está dentro de inner() .
Con esto en mente, intentemos practicar algunos de los conceptos que hemos aprendido
aquí en este capítulo. Daré los "requisitos", y lo intentarás primero. Luego consulte la lista
de códigos que hay a continuación para ver cómo me llegué a ella.
OK, adelante. Intentalo. ¡No eche un vistazo a mi lista de códigos hasta que lo haya
intentado usted mismo!
Nota: Debido a que este es un libro de JavaScript, obviamente voy a resolver el ejercicio de
práctica en JavaScript. Pero usted puede hacerlo en otro idioma por ahora si se siente más
cómodo.
Estos actúan como bloques de construcción. Para construir una torre alta, empiece primero
colocando el bloque en la parte superior del bloque en la parte superior del bloque. Lo
mismo ocurre con la programación. Éstos son algunos de los elementos fundamentales de
la programación:
Los comentarios de código son una manera eficaz de escribir un código más legible, lo que
hace que su programa sea más fácil de entender, mantener y corregir más tarde si hay
problemas.
¡Estoy emocionado de que estés bien en tu camino de aprender a codificar, ahora! Seguid
así. No olvide consultar otros recursos de programación para principiantes (libros, blogs,
formación en línea, etc.). Este capítulo y este libro son un gran comienzo, pero son sólo una
breve introducción.
El siguiente capítulo revisará muchos de los conceptos de este capítulo, pero desde una
perspectiva más específica de JavaScript, que resaltará la mayoría de los principales
temas que se abordan con más detalle en el resto de la serie.
2- En Javascript
En el capítulo anterior, introduje los componentes básicos de la programación, como
variables, bucles, condicionales y funciones. Por supuesto, todo el código mostrado ha sido
en JavaScript. Pero en este capítulo, queremos centrarnos específicamente en las cosas
que debes saber sobre JavaScript para ponerte en marcha como desarrollador de JS.
Especialmente si eres nuevo en JavaScript, deberías pasar un buen rato revisando los
conceptos y ejemplos de código aquí varias veces. Cualquier buena base se pone ladrillo
por ladrillo, así que no espere que usted lo entienda inmediatamente todo con el primer
paso.
Nota: Como dije en el capítulo 1, usted debe probar definitivamente todo este código usted
mismo mientras lee y trabaja a través de este capítulo. Tenga en cuenta que parte del
código aquí asume las capacidades introducidas en la versión más reciente de JavaScript
en el momento de escribir este documento (comúnmente conocido como "ES6" para la 6 ª
edición de ECMAScript - el nombre oficial de la especificación JS). Si está utilizando un
navegador anterior a ES6, es posible que el código no funcione. Debe utilizarse una
actualización reciente de un navegador moderno (como Chrome, Firefox o IE).
2.1 Valores y Tipos
Como hemos afirmado en el Capítulo 1, JavaScript ha escrito valores, no las variables
escritas. Están disponibles los siguientes tipos (de datos) incorporados:
string
number
boolean
null y undefined
object
JavaScript proporciona un tipo de operador que puede examinar un valor y decirle qué tipo
es:
1 var a;
2 typeof a; // "undefined"
3
4 a = "hello world";
5 typeof a; // "string"
6
7 a = 42;
8 typeof a; // "number"
9
10 a = true;
11 typeof a; // "boolean"
12
13 a = null;
14 typeof a; // "object" -- weird, bug
15
16 a = undefined;
17 typeof a; // "undefined"
18
19 a = { b: "c" };
20 typeof a; // "object"
El valor de retorno del operador typeof es siempre uno de los seis valores (siete de ES6! -
el "tipo symbol ") en string . Es decir, typeof "abc" devuelve "string" , no string .
Observe cómo en este fragmento la variable a contiene cada tipo diferente de valor, y que
a pesar de las apariencias, typeof a no está pidiendo el "tipo de a ", sino más bien para
el "tipo del valor actualmente en a ". Sólo los valores tienen tipos en JavaScript; Las
variables son solo contenedores simples para esos valores.
Advertencia: Este es un error de larga data en JS, pero uno que es probable que nunca va a
ser arreglado. Demasiado código en la Web tiene este error y por lo tanto, arreglarlo
causaría mucho más errores!
Objetos
1 var obj = {
2 a: "hello world",
3 b: 42,
4 c: true
5 };
6
7 obj.a; // "hello world"
8 obj.b; // 42
9 obj.c; // true
10
11 obj["a"]; // "hello world"
12 obj["b"]; // 42
13 obj["c"]; // true
Puede ser útil pensar visualmente en este valor obj :
Se puede acceder a las propiedades con notación de puntos (es decir, obj.a ) o con
notación de paréntesis (es decir, obj["a"] ). La notación de puntos es más corta y por lo
general más fácil de leer, y por lo tanto se prefiere cuando es posible.
1 var obj = {
2 a: "hello world",
3 b: 42
4 };
5
6 var b = "a";
7
8 obj[b]; // "hello world"
9 obj["b"]; // 42
Nota: Para obtener más información sobre objetos JavaScript, consulte el título This &
Prototype Objects de esta serie, específicamente el Capítulo 3.
Hay un par de otros tipos de valores con los que comúnmente interactúas en los
programas JavaScript: array y function . Pero en lugar de ser tipos incorporados
apropiados, estos deberían ser pensados más como subtipos - versiones especializadas
del tipo object .
Arrays
Un array es un objeto que contiene valores (de cualquier tipo) particularmente no en las
propiedades/claves con nombre, sino en posiciones numéricamente indexadas. Por
ejemplo:
1 var arr = [
2 "hello world",
3 42,
4 true
5 ];
6
7 arr[0]; // "hello world"
8 arr[1]; // 42
9 arr[2]; // true
10 arr.length; // 3
11
12 typeof arr; // "object"
Nota: Los lenguajes que empiezan a contar a cero, como JS, utilizan 0 como el índice del
primer elemento de la matriz.
Debido a que los arrays son objetos especiales (como typeof implica), también pueden
tener propiedades, incluyendo la propiedad de longitud actualizada automáticamente.
Teóricamente podría utilizar un array como un objeto normal con sus propias propiedades
con nombre, o podría usar un objeto, pero sólo le daría propiedades numéricas ( 0 , 1 ,
etc.) similares a un array. Sin embargo, esto generalmente se considera un uso inadecuado
de los tipos respectivos.
El mejor enfoque y más natural es usar arrays para valores numéricamente posicionados y
usar objetos para propiedades con nombre.
Funciones
El otro subtipo de objeto que usará en todos sus programas JS es una función:
1 function foo() {
2 return 42;
3 }
4
5 foo.bar = "hello world";
6
7 typeof foo; // "function"
8 typeof foo(); // "number"
9 typeof foo.bar; // "string"
De nuevo, las funciones son un subtipo de objetos - typeof devuelve "function" , lo que
implica que una función es un tipo principal - y por lo tanto puede tener propiedades, pero
normalmente sólo se utilizan las propiedades del objeto function (como foo.bar ) en
limitadas casos.
Nota: Para obtener más información sobre los valores de JS y sus tipos, consulte los dos
primeros capítulos del título de Tipos y Gramática de esta serie.
Por ejemplo:
1 var a = "hello world";
2 var b = 3.14159;
3
4 a.length; // 11
5 a.toUpperCase(); // "HELLO WORLD"
6 b.toFixed(4); // "3.1416"
En pocas palabras, hay una forma de envoltorio del objeto String (capitalizar S ),
normalmente llamada "nativa", que se empareja con el tipo de cadena primitiva; Es este
contenedor de objetos que define el método toUpperCase() en su prototipo.
Cuando se utiliza un valor primitivo como "hello world" como un objeto haciendo
referencia a una propiedad o método (por ejemplo, a.toUpperCase() en el fragmento
anterior), JS automáticamente "encaja" el valor a su contraparte wrapper (escondido bajo
cubierta).
Un valor de string puede ser envuelto por un objeto String , un number puede ser
envuelto por un objeto Number y un boolean puede ser envuelto por un objeto Boolean .
En la mayoría de las veces, no es necesario preocuparse ni utilizar directamente estas
formas de envoltorio de objetos en los valores - se prefieren las formas de valores
primitivos en prácticamente todos los casos y JavaScript se encargará del resto por usted.
Comparación de valores
Hay dos tipos principales de comparación de valores que necesitará hacer en sus
programas JS: igualdad y desigualdad. El resultado de cualquier comparación es un valor
estrictamente booleano (verdadero o falso), independientemente de qué tipos de valores se
comparan.
Coerción
La coerción no es mala, ni tiene que ser sorprendente. De hecho, la mayoría de los casos
puede construir con la coerción de tipo de forma bastante sensata y comprensible, e
incluso se puede utilizar para mejorar la legibilidad de su código. Pero no vamos a ir
mucho más lejos en ese debate - El Capítulo 4 del título Tipos y Gramática de esta serie
cubre todos los lados.
1 var a = "42";
2
3 var b = Number( a );
4
5 a; // "42"
6 b; // 42 -- the number!
1 var a = "42";
2
3 var b = a * 1; // "42" implicitly coerced to 42 here
4
5 a; // "42"
6 b; // 42 -- the number!
Truthy y Falsy
null , undefined
false
Cualquier valor que no esté en esta lista falsa es " true ". Estos son algunos ejemplos:
"hello"
42
true
{ } , { a: 42 } (objects)
Es importante recordar que un valor no booleano sólo sigue esta coerción " true " / "
false " si es realmente coaccionado a un booleano. No es tan difícil confundirse con una
Igualdad
Hay cuatro operadores de igualdad: == , === , != , Y !== . Las forma ! son, por
supuesto, las versiones simétricas "no iguales" de sus contrapartes; La no igualdad no debe
confundirse con la desigualdad.
La diferencia entre == y === normalmente se caracteriza por que == comprueba la
igualdad de valor y === comprueba tanto el valor como la igualdad de tipo. Sin embargo,
esto es inexacto. La manera correcta de caracterizarlos es que == chequea por igualdad
de valor con coerción permitida, y === comprueba la igualdad de valor sin permitir
coerción; === se llama a menudo "igualdad estricta" por esta razón.
1 var a = "42";
2 var b = 42;
3
4 a == b; // true
5 a === b; // false
En la comparación a == b , JS advierte que los tipos no coinciden, por lo que pasa por
una serie ordenada de pasos para coaccionar uno o ambos valores a un tipo diferente
hasta que los tipos coincidan, donde entonces se puede comprobar una igualdad de valor
simple .
Si piensas en ello, hay dos formas posibles de que a == b puedan dar true a través de
coerción. O la comparación podría terminar como 42 == 42 o podría ser "42" == "42" .
Entonces, ¿cuál es?
alejados de == . Creo que esta visión es muy miope. Creo que == es una poderosa
herramienta que ayuda a su programa, si usted se toma el tiempo para aprender cómo
funciona.
No vamos a cubrir todos los detalles básicos de cómo la coerción en comparaciones
funciona aquí. Mucho de él es bastante sensible, pero hay algunos casos importantes a
tener cuidado. Puede leer la sección 11.9.3 de la especificación ES5 (http://www.ecma-
international.org/ecma-262/5.1/) para ver las reglas exactas, y le sorprenderá lo sencillo
que es este mecanismo , En comparación con todo el bombo negativo que lo rodea.
Para reducir un montón de detalles a unos pocos takeaways simples, y ayudarle a saber si
utilizar == o === en varias situaciones, aquí están mis reglas simples:
Si un valor (aka lado) en una comparación podría ser el valor true o false , evite
== y utilice === .
Si cualquier valor en una comparación podría ser uno de estos valores específicos ( 0 ,
"" , o [] - matriz vacía), evite == y utilice === .
En todos los demás casos, es seguro utilizar == . No sólo es seguro, sino que en
muchos casos simplifica su código de una manera que mejora la legibilidad.
A lo que estas reglas se reducen es a exigir que usted piense críticamente sobre su código y
sobre qué tipos de valores pueden venir a través de las variables que se comparan por
igualdad. Si puede estar seguro acerca de los valores, y == es seguro, úselo! Si no puede
estar seguro acerca de los valores, utilice === . Es así de simple.
El != No-igualdad forma pares con == , y el !== pares de forma con === . Todas las
reglas y observaciones que acabamos de discutir se mantienen simétricamente para estas
comparaciones sin igualdad.
Debe tomar nota especial de las reglas de comparación == y === si está comparando
dos valores no primitivos, como objetos (incluyendo funciones y arrays). Debido a que esos
valores se mantienen en realidad por referencia, las comparaciones == y ===
simplemente comprobarán si las referencias coinciden, no cualquier cosa acerca de los
valores subyacentes.
Por ejemplo, las matrices se coaccionan por defecto a strings simplemente uniendo todos
los valores con comas ( , ) . Podría pensar que dos matrices con el mismo contenido
serían == iguales, pero no lo son:
1 var a = [1,2,3];
2 var b = [1,2,3];
3 var c = "1,2,3";
4
5 a == c; // true
6 b == c; // true
7 a == b; // false
Nota: Para obtener más información acerca de las reglas de comparación de igualdad,
consulte la especificación ES5 (sección 11.9.3) y consulte también el Capítulo 4 del título
de Tipos y Gramática de esta serie; Consulte el Capítulo 2 para obtener más información
acerca de valores versus referencias.
Desigualdad
Los operadores < , > , <= , y >= se utilizan para la desigualdad, a la que se hace
referencia en la especificación como "comparación relacional". Normalmente se utilizarán
con valores comparables como números. Es fácil entender que 3 < 4 .
Pero los valores string de JavaScript también se pueden comparar por desigualdad,
usando reglas alfabéticas típicas ( "bar" < "foo" ).
Considere:
1 var a = 41;
2 var b = "42";
3 var c = "43";
4
5 a < b; // true
6 b < c; // true
¿Qué pasa aquí? En la sección 11.8.5 de la especificación ES5, se dice que si ambos
valores en la comparación < son strings, como ocurre con b < c , la comparación se
hace lexicográficamente (aka alfabéticamente como un diccionario). Pero si uno o ambos
no es una cadena, como ocurre con a < b , entonces ambos valores son forzados a ser
números y se produce una comparación numérica típica.
El gotcha más grande que se puede encontrar aquí con comparaciones entre tipos de valor
potencialmente diferentes - recuerde, no hay formas de "desigualdad estricta" para usar - es
cuando uno de los valores no se puede convertir en un número válido, como:
1 var a = 42;
2 var b = "foo";
3
4 a < b; // false
5 a > b; // false
6 a == b; // false
Espera, ¿cómo pueden ser falsas las tres comparaciones? Debido a que el valor b está
siendo coaccionado al "valor de número inválido" NaN en las comparaciones < y > , y la
especificación dice que NaN no es ni mayor ni menor que cualquier otro valor.
La comparación == falla por una razón diferente. a == b podría fallar si se interpreta
como 42 == NaN o "42" == "foo" - como hemos explicado anteriormente, el primero es
el caso.
Nota: Para obtener más información sobre las reglas de comparación de desigualdades,
consulte la sección 11.8.5 de la especificación ES5 y consulte también el Capítulo 4 del
título Tipos y Gramática de esta serie.
2.2 Variables
En JavaScript, los nombres de las variables (incluidos los nombres de las funciones) deben
ser identificadores válidos. Las reglas estrictas y completas para caracteres válidos en
identificadores son un poco complejas cuando se consideran caracteres no tradicionales
como Unicode. Sin embargo, si sólo se consideran caracteres alfanuméricos ASCII típicos,
las reglas son simples.
Un identificador debe comenzar con a-z , A-Z , $ o _ . Entonces puede contener
cualquiera de esos caracteres más los números 0-9 .
Nota: Para obtener más información sobre las palabras reservadas, consulte el Apéndice A
del título Tipos y Gramática de esta serie.
Utilice la palabra clave var para declarar una variable que pertenecerá al ámbito de la
función actual o al ámbito global si está en el nivel superior fuera de cualquier función.
Hoisting (Elevación)
Siempre que aparezca una var dentro de un ámbito, esa declaración se toma para
pertenecer a todo el ámbito y es accesible en todas partes.
Técnicamente, este proceso se explica con más precisión por la forma en que se compila el
código, pero podemos omitir esos detalles por ahora.
Considerar:
1 var a = 2;
2
3 foo(); // funciona porque la declaracion `foo()` esta "hoisted" (elevad
4 // la función y después es declarada
5
6 function foo() {
7 a = 3;
8
9 console.log( a ); // 3
10
11 var a; // la declaración esta "hoisted" (elevada) en la parte s
12 }
13
14 console.log( a ); // 2
Ámbitos anidados
Cuando declara una variable, ella estará disponible en cualquier parte de ese ámbito, así
como en cualquier ámbito inferior/interno. Por ejemplo:
1 function foo() {
2 var a = 1;
3
4 function bar() {
5 var b = 2;
6
7 function baz() {
8 var c = 3;
9
10 console.log( a, b, c ); // 1 2 3
11 }
12
13 baz();
14 console.log( a, b ); // 1 2
15 }
16
17 bar();
18 console.log( a ); // 1
19 }
20
21 foo();
Observe que c no está disponible dentro de bar() , porque se declara sólo dentro del
ámbito interno baz() y que b no está disponible para foo() por la misma razón.
1 function foo() {
2 a = 1; // `a` no esta formalmente declarada con `var`
3 }
4
5 foo();
6 a; // 1 -- oops, variable automatica global :(
Esta es una muy mala práctica. ¡No lo hagas! Siempre declare formalmente sus variables.
1 function foo() {
2 var a = 1;
3
4 if (a >= 1) {
5 let b = 2;
6
7 while (b < 5) {
8 let c = b * 2;
9 b++;
10
11 console.log( a + c );
12 }
13 }
14 }
15
16 foo();
17 // 5 7 9
manera más refinada, lo que puede hacer que su código sea mucho más fácil de mantener
con el tiempo.
Nota: Para obtener más información sobre los ámbitos, consulte el título Scope & Closures
de esta serie. Consulte el título ES6 & Beyond de esta serie para obtener más información
sobre el ámbito de bloque let .
2.3 Condicionales
Además de la declaración if que introdujimos brevemente en el Capítulo 1, JavaScript
proporciona algunos otros mecanismos condicionales a los que deberíamos echar un
vistazo.
1 if (a == 2) {
2 // do something
3 }
4 else if (a == 10) {
5 // do another thing
6 }
7 else if (a == 42) {
8 // do yet another thing
9 }
10 else {
11 // fallback to here
12 }
1 switch (a) {
2 case 2:
3 // do something
4 break;
5 case 10:
6 // do another thing
7 break;
8 case 42:
9 // do yet another thing
10 break;
11 default:
12 // fallback to here
13 }
El break es importante si sólo desea que se ejecute la instrucción(es) en un case (caso).
Si omite el break de un case , y ese case coincide o se ejecuta, la ejecución continuará
con las declaraciones del case siguiente, independientemente de la coincidencia de ese
case . A veces, este llamado "caer a través (fall through)" es útil/deseado:
1 switch (a) {
2 case 2:
3 case 10:
4 // some cool stuff
5 break;
6 case 42:
7 // other stuff
8 break;
9 default:
10 // fallback
11 }
1 var a = 42;
2
3 var b = (a > 41) ? "hello" : "world";
4
5 // similar to:
6
7 // if (a > 41) {
8 // b = "hello";
9 // }
10 // else {
11 // b = "world";
12 // }
Si la expresión de prueba ( a > 41 aquí) se evalúa como verdadera, se obtiene la primera
cláusula ( "hola" ), de lo contrario se obtiene la segunda cláusula ( "mundo" ).
El operador condicional no tiene que ser utilizado en una asignación, pero eso es
definitivamente el uso más común.
Nota: Para obtener más información sobre las condiciones de prueba y otros patrones de
switch y ? : , Vea el título Tipos y Gramática de esta serie.
2.4 Modo estricto
ES5 agregó un "modo estricto" al lenguaje, que aprieta las reglas para ciertos
comportamientos. Generalmente, estas restricciones se consideran como mantener el
código a un conjunto de directrices más seguro y más apropiado. Además, la adhesión al
modo estricto hace que su código en general sea más optimizable por el motor. El modo
estricto es un gran triunfo para el código, y deberías usarlo para todos tus programas.
Puede optar por el modo estricto para una función individual, o un archivo entero,
dependiendo de donde se puso el modo estricto:
1 function foo() {
2 "use strict";
3
4 // este código esta en modo estricto
5
6 function bar() {
7 // este código esta en modo estricto
8 }
9 }
10
11 // este código no esta en modo estricto
1 "use strict";
2
3 function foo() {
4 // este código esta en modo estricto
5
6 function bar() {
7 // este código esta en modo estricto
8 }
9 }
10
11 // este código esta en modo estricto
Una diferencia clave (¡mejora!) con el modo estricto es rechazar la declaración implícita de
variables globales automáticas al omitir la variable:
1 function foo() {
2 "use strict"; // activa el modo etsricto
3 a = 1; // `var` olvidada, es igual a ReferenceError
4 }
5
6 foo();
No sólo el modo estricto mantendrá su código en un camino más seguro, y no sólo hará
que su código sea más optimizable, sino que también representa la dirección futura del
lenguaje. Sería más fácil para ti acostumbrarte al modo estricto ahora que seguir
poniéndolo en el código, ¡sólo será más difícil cambiarlo más tarde!
Nota: Para obtener más información sobre el modo estricto, consulte el Capítulo 5 del título
Tipos y Gramática de esta serie.
2.5 Funciones como Valores
Hasta ahora, hemos discutido las funciones como el principal mecanismo de scope en
JavaScript. Puede recordar la sintaxis típica de la declaración de funciones de la siguiente
manera:
1 function foo() {
2 // ..
3 }
Aunque no parezca obvio de esa sintaxis, foo es básicamente sólo una variable en el
ámbito exterior que incluye una referencia a la función que se está declarando. Es decir, la
función en sí es un valor, al igual que sería 42 o [1,2,3] .
Esto puede sonar como un concepto extraño al principio, así que tómese un momento para
reflexionar sobre él. No sólo puede pasar un valor (argumento) a una función, sino que una
función en sí puede ser un valor que se asigna a variables, o se pasa a, o se devuelve a
otras funciones.
Como tal, un valor de función debe ser pensado como una expresión, al igual que cualquier
otro valor o expresión.
Considerar:
Para obtener más información, consulte el título Scope & Closures de esta serie.
Hay otra forma de ejecutar una expresión de función, que normalmente se denomina
expresión de función inmediatamente invocada (IIFE):
1 (function IIFE(){
2 console.log( "Hello!" );
3 })();
4 // "Hello!"
El exterior (..) que rodea la expresión de función (función IIFE () {..}) es sólo un
matiz de la gramática JS necesaria para evitar que se trate como una declaración de
función normal.
Eso puede parecer extraño, pero no es tan extraño como a primera vista. Considere las
similitudes entre foo y IIFE aquí:
1 function foo() { .. }
2
3 // `foo` function reference expression,
4 // then `()` executes it
5 foo();
6
7 // expresión de función `IIFE`,
8 // entonces `()` lo ejecuta
9 (function IIFE(){ .. })();
Como se puede ver, la lista de la función (IIFE () {..}) antes de su ejecución () es
esencialmente el mismo que incluye foo antes de su ejecución () ; En ambos casos, la
referencia de función se ejecuta con () inmediatamente después de ella.
Debido a que un IIFE es sólo una función y las funciones crean un ámbito variable, el
uso de un IIFE de esta manera se utiliza a menudo para declarar variables que no
afectarán al código circundante fuera del IIFE :
1 var a = 42;
2
3 (function IIFE(){
4 var a = 10;
5 console.log( a ); // 10
6 })();
7
8 console.log( a ); // 42
Puede pensar en el closure como una forma de "recordar" y seguir accediendo al ámbito de
una función (y sus variables) incluso una vez que la función ha terminado de ejecutarse.
Considerar:
1 function makeAdder(x) {
2 // El parámetro `x` es una variable interna
3
4 // función interna `add ()` usa `x`, así que
5 // tiene un "closure" sobre él
6 function add(y) {
7 return y + x;
8 };
9
10 return add;
11 }
No se preocupe si esto parece extraño y confuso al principio - puede serlo! Tomará mucha
práctica entenderlo completamente.
Pero confía en mí, una vez que lo hagas, es una de las técnicas más poderosas y útiles en
toda la programación. Definitivamente vale la pena el esfuerzo para dejar que su cerebro
cocine a fuego lento los closures poco a poco. En la siguiente sección, tendremos un poco
más de práctica con los closures.
Modules
El uso más común de los closures en JavaScript es el patrón module. Los modules le
permiten definir detalles de implementación privados (variables, funciones) ocultos del
mundo exterior, así como una API pública accesible desde el exterior.
Considere:
1 function User(){
2 var username, password;
3
4 function doLogin(user,pw) {
5 username = user;
6 password = pw;
7
8 // do the rest of the login work
9 }
10
11 var publicAPI = {
12 login: doLogin
13 };
14
15 return publicAPI;
16 }
17
18 // create a `User` module instance
19 var fred = User();
20
21 fred.login( "fred", "12Battery34!" );
La función User() sirve como un ámbito externo que contiene las variables username y
password , así como la función interna doLogin() ; Estos son todos los detalles internos
privados de este módulo de usuario que no se puede acceder desde el mundo exterior.
Advertencia: No estamos llamando a new User() aquí, a propósito, a pesar del hecho de
que probablemente parece más común a la mayoría de los lectores. User() es sólo una
función, no una clase a instanciar, por lo que se llama normalmente. El uso de new sería
inapropiado y realmente desperdiciaría recursos.
Ejecutar User() crea una instancia del módulo User - se crea un nuevo ámbito y, por lo
tanto, una copia completamente nueva de cada una de estas variables/funciones internas.
Asignamos esta instancia a fred . Si ejecutamos User() de nuevo, obtendremos una
nueva instancia completamente distinta de fred .
PublicAPI es un objeto con una propiedad/método en él, login , que es una referencia a
Es por eso que podemos llamar a fred.login(..) - lo mismo que llamar a la interna
doLogin(..) - y todavía puede acceder a las variables internas de username y
password .
Hay una buena probabilidad de que con sólo este breve vistazo al closures y el patrón
module, algunos de ellos sean todavía un poco confusos. ¡Está bien! Toma un poco de
trabajo para envolver su cerebro alrededor de él.
A partir de aquí, vaya a leer el título Scope & Closures de esta serie para una exploración
mucho más profunda.
2.6 Identificador This
Otro concepto muy comúnmente mal entendido en JavaScript es el identificador this .
Una vez más, hay un par de capítulos sobre él de esta serie, así que aquí vamos a introducir
brevemente el concepto.
Aunque a menudo parece que this está relacionado con "patrones orientados a objetos",
en JS este es un mecanismo diferente.
Si una función tiene una referencia this dentro de ella, esa referencia this
normalmente apunta a un object . Pero el object al que apunta depende de cómo se
llamó la función.
Es importante darse cuenta de que this no se refiere a la función en sí, y ése es el error
más común.
1 function foo() {
2 console.log( this.bar );
3 }
4
5 var bar = "global";
6
7 var obj1 = {
8 bar: "obj1",
9 foo: foo
10 };
11
12 var obj2 = {
13 bar: "obj2"
14 };
15
16 // --------
17
18 foo(); // "global"
19 obj1.foo(); // "obj1"
20 foo.call( obj2 ); // "obj2"
21 new foo(); // undefined
Hay cuatro reglas para cómo se establece this , y se muestran en las cuatro últimas
líneas de ese fragmento.
En pocas palabras: para entender a qué se refiere this , hay que examinar cómo se llamó
la función en cuestión. Será una de esas cuatro maneras que acabamos de mostrar, y eso
responderá a lo que es this .
Nota: Para obtener más información al respecto, consulte los capítulos 1 y 2 del título This
y Objetos Prototipos de esta serie.
2.7 Prototypes
El mecanismo prototype en JavaScript es bastante complicado. Sólo le echaremos un
vistazo aquí. Usted querrá pasar un montón de tiempo revisando los capítulos 4-6 del título
de this y los objetos prototipos de esta serie para todos los detalles.
Considere:
1 var foo = {
2 a: 42
3 };
4
5 // crear `bar` y vincularlo a` foo`
6 var bar = Object.create( foo );
7
8 bar.b = "hello world";
9
10 bar.b; // "hello world"
11 bar.a; // 42 <-- delegado a `foo`
Este vínculo puede parecer una característica extraña del lenguaje. La forma más común
de usar esta característica -y yo diría, abusada- es tratar de emular/falsificar un mecanismo
de "clase" con "herencia".
Pero una forma más natural de aplicar prototipos es un patrón llamado "delegación de
comportamiento", en el que intencionalmente diseña sus objetos vinculados para poder
delegar de uno a otro las partes del comportamiento necesario.
Entonces, ¿qué hacemos con las cosas nuevas? ¿Sólo tenemos que esperar alrededor de
unos años o décadas para que todos los viejos navegadores se desvanezcan en la
oscuridad?
Esta es la forma en que la gente que piensa acerca de esta situación, pero en realidad no es
un enfoque saludable para JS.
Hay dos técnicas principales que puede utilizar para "llevar" las cosas nuevas de
JavaScript a los navegadores más antiguos: polyfilling y transpiling.
Polyfilling
Por ejemplo, ES6 define una utilidad denominada Number.isNaN(..) para proporcionar
una verificación exacta y sin errores en los valores NaN , despreciando la utilidad original
isNaN(..) . Pero es fácil "polifilear" la utilidad para que pueda empezar a utilizarlo en su
Considere:
1 if (!Number.isNaN) {
2 Number.isNaN = function isNaN(x) {
3 return x !== x;
4 };
5 }
Nota : El chequeo que hacemos aquí se aprovecha de una peculiaridad con valores NaN ,
que es que son el único valor en todo el lenguaje que no es igual a sí mismo. Así que el
valor NaN es el único que haría que x !== x sea true .
No todas las nuevas características son completamente polifilables. A veces la mayor parte
del comportamiento puede ser polifileado, pero todavía hay pequeñas desviaciones. Usted
debe ser muy, muy cuidadoso en la aplicación de un polyfill que haga usted mismo, para
asegurarse de que se adhieren a la especificación tanto como sea posible.
O mejor aún, utilice un conjunto de polyfills ya probado en los que puede confiar, como los
proporcionados por ES5-Shim (https://github.com/es-shims/es5-shim) y ES6-Shim (https:
// Github.com/es-shims/es6-shim).
Transpiling
Por lo tanto, la mejor opción es utilizar una herramienta que convierte su código más
reciente en equivalentes de código antiguo. Este proceso es comúnmente llamado
"transpiling", un término para transformar + compilar.
Hay varias razones importantes por las que debe preocuparse por el transpiling:
La nueva sintaxis agregada al lenguaje está diseñada para hacer que su código sea
más legible y mantenible. Los equivalentes más viejos son a menudo mucho más
enrevesados. Debería preferir escribir una sintaxis nueva y más limpia, no sólo para
usted sino para todos los demás miembros del equipo de desarrollo.
Si transpila sólo para los navegadores más antiguos, pero sirve la nueva sintaxis a los
navegadores más recientes, podrá aprovechar las optimizaciones del rendimiento del
navegador con la nueva sintaxis. Esto también permite a los fabricantes del navegador
tener más código del mundo real para probar sus implementaciones y optimizaciones.
El uso de la nueva sintaxis anticipadamente permite que se pruebe de forma más
robusta en el mundo real, lo que proporciona retroalimentación al comité JavaScript
(TC39). Si los problemas se encuentran a tiempo, pueden ser cambiados/arreglados
antes de que los errores de diseño de lenguaje se conviertan en permanentes.
Aquí está un ejemplo rápido de transpiling. ES6 agrega una característica llamada "valores
de parámetros predeterminados". Se parece a esto:
1 function foo(a = 2) {
2 console.log( a );
3 }
4
5 foo(); // 2
6 foo( 42 ); // 42
Simple, ¿verdad? ¡Útil, también! Pero es una nueva sintaxis que no es válida en motores
pre-ES6. Entonces, ¿qué hará un transpiler con ese código para que funcione en entornos
más antiguos?
1 function foo() {
2 var a = arguments[0] !== (void 0) ? arguments[0] : 2;
3 console.log( a );
4 }
Como puede ver, comprueba si el valor de los arguments[0] es void 0 (aka undefined
), y si es así, proporciona el valor predeterminado 2 ; De lo contrario, asigna lo que se
pasó.
Además de poder utilizar ahora la sintaxis más agradable incluso en los navegadores más
antiguos, ver el código transpilado explica realmente el comportamiento previsto más
claramente.
Es posible que no se haya dado cuenta sólo de mirar la versión de ES6 que undefined es
el único valor que no se puede pasar explícitamente en un parámetro de valor
predeterminado, pero el código transpilado lo hace mucho más claro.
El último detalle importante a destacar sobre los transpiladores es que ahora deben ser
considerados como una parte estándar del ecosistema y proceso de desarrollo de JS. JS
va a seguir evolucionando, mucho más rápidamente que antes, por lo que cada pocos
meses se añadirá nueva sintaxis y nuevas características.
Hay bastantes transpilers grandes para que usted elija de. Aquí hay algunas buenas
opciones en el momento de escribir esto:
El JavaScript más común que no se encuentra en JavaScript es la API DOM . Por ejemplo:
Por supuesto, cada uno de estos temas merece una cobertura mucho mayor de lo que has
visto aquí, pero es por eso que tienen capítulos y libros dedicados a ellos durante el resto
de esta serie. Después de sentirse bastante cómodo con los conceptos y las muestras de
código en este capítulo, el resto de la serie lo espera para profundizar y conocer el lenguaje
profundamente.
El último capítulo de este libro resumirá brevemente cada uno de los otros títulos de la serie
y los otros conceptos que cubren, además de lo que ya hemos explorado.
3- En YDKJS
¿De qué trata esta serie? En pocas palabras, se trata de tomar en serio la tarea de aprender
todas las partes de JavaScript, no sólo un subconjunto del lenguaje que alguien llamó "las
partes buenas", y no cualquier cantidad mínima que necesita saber para hacer su trabajo.
La serie You Do not Know JS (YDKJS) está en marcando contraste con los enfoques
típicos para aprender JS, y es esa la diferencia de casi cualquier otro libro JS que lea. Te
desafía a ir más allá de tu zona de confort y a hacer las más profundas, y preguntas de
"por qué" para cada comportamiento que encuentres. ¿Estás preparado para ese desafío?
Voy a utilizar este capítulo final para resumir brevemente qué esperar del resto de los libros
de la serie, y cómo ir más eficazmente sobre la construcción de una base solida de
aprendizaje de JS en la parte superior de YDKJS.
3.1 Scope & Closures
Quizás una de las cosas más fundamentales que necesitará para llegar a un entendimiento
rápido es cómo el scope de las variables realmente funcionan en JavaScript. No basta con
tener creencias anecdóticas difusas sobre el scope.
El título Scope & Closures comienza por desacreditar la idea errónea común de que JS es
un "lenguaje interpretado" y por lo tanto no se compila. Nope.
El motor JS compila su código justo antes (y a veces durante!) la ejecución. Así que
buscamos una comprensión más profunda de la aproximación del compilador a nuestro
código para entender cómo encuentra y trata las declaraciones de variables y funciones. A
lo largo del camino, vemos la metáfora típica para la gestión del scope de variables en JS,
"Hoisting".
Esta comprensión crítica del "alcance léxico" es en lo que entonces basamos nuestra
exploración de los closures para el último capítulo del libro. Los closures son tal vez el
concepto más importante en todos los de JS, pero si no ha comprendido con firmeza cómo
funciona el scope, el closure probablemente permanecerá fuera de su alcance.
Muy relacionado con la palabra clave this está el mecanismo de prototipo de objeto, que
es una cadena de búsqueda de propiedades, similar a cómo se encuentran las variables de
alcance de léxico. Pero envuelto en los prototipos es el otro gran error sobre JS: la idea de
emular (fake) clases y (lo que se llama "prototipo") la herencia.
Lo que está en cuestión es si es mejor ignorar el desajuste y pretender que lo que está
implementando es "herencia", o si es más apropiado aprender y abrazar cómo funciona
realmente el prototipo del sistema. Esta última es más apropiadamente llamada
"delegación de comportamiento".
Las afirmaciones que hago con respecto a la delegación versus la herencia vienen no de
una aversión al lenguaje y su sintaxis, sino del deseo de ver la verdadera capacidad del
lenguaje adecuadamente apalancado y la interminable confusión y frustración por fin
borrada.
Pero el caso que hago con respecto a los prototipos y la delegación es mucho más
complicado que lo que voy a disfrutar aquí. Si estás listo para reconsiderar todo lo que
piensas que sabes sobre las "clases" de JavaScript y la "herencia", te ofrezco la
oportunidad de "tomar la píldora roja" (Matrix 1999) y echa un vistazo a los capítulos 4-6
del Título This & de los prototipos de esta serie.
3.3 Tipos & Gramática
El tercer título de esta serie se centra principalmente en abordar otro tema muy polémico: la
coerción de tipos. Tal vez ningún tema causa más frustración en los desarrolladores de JS
que cuando se habla de las confusiones que rodean la coerción implícita.
Con mucho, la sabiduría convencional es que la coerción implícita es una "parte mala" del
lenguaje y debe ser evitada a toda costa. De hecho, algunos han ido tan lejos como para
llamarlo un "defecto" en el diseño del lenguaje. De hecho, hay herramientas cuyo trabajo
completo es no hacer nada, sino escanear su código y quejarse si está haciendo algo
incluso remotamente como coerción.
Pero, ¿es la coerción realmente tan confusa, tan mala, tan traicionera, que tu código está
condenado desde el principio si lo usas?
Yo digo que no. Después de haber entendido cómo los tipos y valores realmente funcionan
en los Capítulos 1-3, el Capítulo 4 asume este debate y explica completamente cómo
funciona la coerción, en todos sus rincones y grietas. Vemos exactamente qué partes de la
coerción realmente son sorprendentes y qué partes realmente tienen sentido completo si se
les da el tiempo para aprender.
¿Quieres seguir lo que dice la multitud, o estás dispuesto a dejar todas las suposiciones a
un lado y mirar la coerción con una nueva perspectiva? El título Tipos y Gramática de esta
serie coaccionará su pensamiento.
3.4 Async & Performance
Los tres primeros títulos de esta serie se centran en la mecánica básica del lenguaje, pero el
cuarto título se ramifica ligeramente para cubrir los patrones de la mecánica del lenguaje
para gestionar la programación asincrónica. La asincronía no sólo es crítica para el
rendimiento de nuestras aplicaciones, sino que se está convirtiendo cada vez más en un
factor crítico en la capacidad de escritura y mantenimiento.
El libro comienza primero por aclarar una gran cantidad de terminología y confusión de
conceptos en torno a cosas como "asíncrona", "paralela" y "concurrente", y explica en
profundidad cómo estas cosas se aplican y no se aplican a JS.
Para abordar estas dos grandes deficiencias, ES6 introduce dos nuevos mecanismos (y, de
hecho, patrones): las promesas y los generadores (promises y generators)
Las promesas son una envoltura independiente del tiempo alrededor de un "valor futuro",
que le permite razonar y componer independientemente de si el valor está listo o no. Por
otra parte, resuelven eficazmente las ediciones del trust de IoC encaminando devoluciones
de llamada a través de un mecanismo confiables y componibles de la promesa.
Los generadores introducen un nuevo modo de ejecución para las funciones JS, con lo que
el generador puede pausarse en los puntos de rendimiento y reanudarse asincrónicamente
más tarde. La capacidad de pausa y reanudación permite que el código de búsqueda
secuencial y secuencial en el generador se procese asincrónicamente detrás de escena.
Haciendo esto, abordamos las confusiones de devoluciones de llamada no lineales, no
locales y, por lo tanto, hacemos que nuestro código asincrónico sincronice el código para
ser más razonable.
Si las promesas y los generadores tienen que ver con la expresión de patrones que
permiten que nuestros programas funcionen mejor al mismo tiempo y así lograr más
procesamiento en un período más corto, JS tiene muchas otras facetas de optimización de
rendimiento que vale la pena explorar.
El capítulo 5 profundiza en temas como el paralelismo de programas con los Workers Web
y el paralelismo de datos con SIMD, así como técnicas de optimización de bajo nivel como
ASM.js. El Capítulo 6 echa un vistazo a la optimización del rendimiento desde la
perspectiva de técnicas de benchmarking apropiadas, incluyendo por qué tipo de
rendimiento preocuparse y qué ignorar.
Escribir JavaScript significa efectivamente escribir código que puede romper las
restricciones de estar ejecutado dinámicamente en una amplia gama de navegadores y
otros entornos. Requiere una gran cantidad de intrincada y detallada planificación y
esfuerzo de nuestras partes para llevar un programa de "funciona" a "funciona bien".
El título Async & Performance está diseñado para ofrecerte todas las herramientas y
habilidades que necesitas para escribir código JavaScript razonable y eficaz.
3.5 ES6 & Más allá
No importa cuánto sientas que has dominado JavaScript hasta este punto, la verdad es
que JavaScript nunca va a dejar de evolucionar, y además, la tasa de evolución está
aumentando rápidamente. Este hecho es casi una metáfora para el espíritu de esta serie,
para abrazar que nunca sabremos completamente todas las partes de JS, porque tan
pronto como lo domines todo, habrá cosas nuevas online que vas a necesitar aprender.
Este título está dedicado tanto a las visiones a corto y mediano plazo de donde se dirige el
lenguaje, no sólo las cosas conocidas como ES6, sino las cosas probables del más allá.
Mientras que todos los títulos de esta serie abarcan el estado de JavaScript en el momento
de escribir este artículo, que está a medio camino con la adopción de ES6, el foco principal
en la serie ha sido más en ES5. Ahora, queremos dirigir nuestra atención a ES6, ES7 y ...
ES6 & Beyond comienza dividiendo el material de hormigón del paisaje ES6 en varias
categorías clave, incluyendo nueva sintaxis, nuevas estructuras de datos (colecciones) y
nuevas capacidades de procesamiento y API. Cubrimos cada una de estas nuevas
características de ES6, en diversos niveles de detalle, incluyendo la revisión de detalles que
se mencionan en otros libros de esta serie.
Algunas cosas emocionantes ES6 para mirar hacia adelante es leer acerca de:
desestructuración, valores de parámetros por defecto, símbolos, métodos concisos,
propiedades calculadas, funciones de flecha, bloque de alcance, promesas, generadores,
iteradores, módulos, proxies, weakmaps y mucho, mucho más! Phew, ES6 packs un buen
golpe!
La primera parte del libro es una hoja de ruta para todas las cosas que usted necesita
aprender para prepararse para lo nuevo y lo mejorado que va a escribir JavaScript y
explorarlo en los próximos dos años.
La última parte del libro da vuelta la atención brevemente a la mirada en las cosas que
podemos probablemente esperar ver en el futuro próximo de Javascript. La realización más
importante aquí es que después de ES6, JS es probable que vaya a evolucionar
característica por característica en lugar de versión por versión, lo que significa que
podemos esperar para ver estas cosas en un futuro cercano mucho más pronto de lo que
pueda imaginar.
El futuro de JavaScript es brillante. ¿No es hora de que empecemos a aprenderlo?
3.6 Revisión
La serie YDKJS está dedicada a la proposición de que todos los desarrolladores de JS
pueden y deben aprender todas las partes de este gran lenguaje. La opinión de alguna
persona, las premisas sobre el framework y el plazo del proyecto no deben ser la excusa
para que nunca aprendas y entiendas profundamente el JavaScript.
Tomamos cada área importante de enfoque en el lenguaje y dedicar un libro corto pero
muy denso para explorar todas las partes de ella que tal vez pensó que sabía pero
probablemente no lo hizo plenamente.
"You Do not Know JS" no es una crítica o un insulto. Es una comprensión de que todos
nosotros, yo incluido, debemos llegar a un acuerdo. Aprender JavaScript no es un objetivo
final, sino un proceso. No sabemos JavaScript, todavía. ¡Pero lo haremos!
II- Scope & Closures
Prefacio
Tuve el honor de escribir el prólogo del primer libro, Scope & Closures, de la serie You Do not
Know JS de Kyle Simpson. Le ruego que compre el libro, definitivamente vale la pena leerlo
sin importar su habilidad o experiencia, pero también he incluido el prólogo abajo.
Cuando yo era un niño pequeño, a menudo me gustaba separar las cosas y volver a
juntarlas. Teléfonos móviles antiguos, equipos de música Hi-Fi y cualquier otra cosa que
pudiera poner en mis manos. Yo era demasiado joven para usar realmente estos
dispositivos, pero cada vez que se rompían, me preguntaba instantáneamente si podía
averiguar cómo funcionaba.
Recuerdo que una vez vi una placa de circuito para una radio vieja. Tenía este extraño tubo
largo con alambre de cobre envuelto alrededor de él. No pude resolver su propósito, pero
inmediatamente entré en el modo de investigación. ¿Qué hace? ¿Por qué está en una
radio? No se parece a las otras partes de la placa de circuito, ¿por qué? ¿Por qué tiene
cobre envuelto alrededor? ¿Qué sucede si retiro el cobre? Ahora sé que era una antena de
bucle, hecha por el cable de cobre envolvente alrededor de una barra de ferrita, que se
utilizan a menudo en radios de transistores.
¿Alguna vez se convirtió en adicto a averiguar todas las respuestas a cada pregunta 'por
qué'? La mayoría de los niños lo hacen. De hecho, es probablemente mi cosa favorita
acerca de los niños - su deseo de aprender.
Y eso es exactamente por lo que estoy tan entusiasmado con la serie de libros de Kyle "You
Do not Know JS". Porque tiene razón. No conozco JS. Utilizo Javascript en el dia a dia, de
adentro hacia fuera y lo he hecho por muchos años, pero lo entiendo realmente? No. Claro,
entiendo mucho y a menudo leo las especificaciones y las listas de correo, pero no, no
entiendo tanto .
Scope y Closures, es un comienzo brillante de la serie. Está muy bien dirigido a personas
como yo (y espero que hacia ti también), no enseña JavaScript como si nunca lo hubieras
usado, y te hace darte cuenta de lo poco que sabes sobre el funcionamiento interno.
Así que espero que disfruten de este libro, pero más que la forma en que Kyle piensa
críticamente acerca de cómo funciona cada pequeño fragmento del lenguaje, fluirá en su
forma de pensar y flujo de trabajo en general. En lugar de utilizar la antenna, averiguar
cómo y por qué funciona.
0- Prefacio
Estoy seguro de que se dio cuenta, pero "JS" en el título de la serie de libros no es una
abreviatura de palabras utilizadas para maldecir sobre JavaScript, aunque maldecir a las
peculiaridades del lenguaje es algo con lo que probablemente todos se pueden identificar.
Desde los primeros días de la web, JavaScript ha sido una tecnología fundamental que ha
impulsado la experiencia interactiva en torno al contenido que consumimos. Mientras que
el apuntador parpadeante y los molestos avisos emergentes podrían ser donde comenzó
JavaScript, casi dos décadas después, la tecnología y la capacidad de JavaScript ha
crecido en muchos ámbitos de magnitud, y pocos dudan de su importancia en el corazón
de la plataforma de software más ampliamente disponible: La web.
Pero como lenguaje, ha sido perpetuamente un blanco para mucha crítica, debido en parte
a su "herencia", pero más aún debido a su filosofía de diseño. Incluso el nombre evoca,
como Brendan Eich una vez lo dijo, "el hermano bebe estúpido" comparado junto a su
hermano mayor más maduro "Java". Pero el nombre es simplemente un accidente de la
política y el marketing. Los dos idiomas son muy diferentes en muchos aspectos
importantes. "JavaScript" está relacionado con "Java" como "Carnival" es a "Car".
Mientras que JavaScript es quizás uno de los idiomas más fáciles de poner en
funcionamiento, sus excentricidades hacen que el dominio sólido del lenguaje sea una
ocurrencia mucho menos común que en muchos otros idiomas. Cuando se necesita un
conocimiento bastante profundo de un lenguaje como C o C++ para escribir un programa a
gran escala, la producción a gran escala de JavaScript puede, y con frecuencia lo es,
apenas se raya con la superficie de lo que el lenguaje puede realmente hacer.
Es simultáneamente un lenguaje sencillo y fácil de usar que tiene un amplio atractivo y una
colección compleja y matizada de mecánica del lenguaje que sin un estudio cuidadoso
escapará a la verdadera comprensión, incluso de los más experimentados desarrolladores
de JavaScript.
Misión
Si bien este subconjunto ha sido conocido como "The Good Parts", le pediría a usted,
querido lector, que lo considere "The Easy Parts", "The Safe Parts" o incluso "The
Incomplete Parts".
No estoy contento, ni debes estar, en parar una vez que algo funciona correctamente, y no
saber realmente por qué. Le desafío a viajar por ese "camino costoso" y abrazar todo lo que
JavaScript es y puede hacer. Con ese conocimiento, ninguna técnica, ningún framework,
ningún acrónimo popular de moda, estará más allá de su comprensión.
Estos libros toman partes específicas del lenguaje que son las comúnmente mal
entendidas o no comprendidas, y se sumerge muy profundamente y exhaustivamente en
ellas. Usted debe termina la lectura con una firme confianza en su comprensión, no sólo de
lo teórico, sino lo práctico "lo que necesita saber".
El JavaScript que usted conoce ahora mismo es probablemente la partes dada a usted por
otros que han sido "quemados" por una comprensión incompleta. Ese JavaScript es sólo
una sombra del verdadero lenguaje. Realmente no sabes JavaScript, pero si crees en esta
serie, lo harás. Sigue leyendo, amigos. JavaScript te espera.
Resumen
Nota: Muchos de los ejemplos en este libro asumen los entornos de motor de JavaScript
modernos (y futuros), como ES6. Es posible que algunos códigos no funcionen como se
describe si se ejecutan en motores anteriores (pre-ES6).
1- ¿Qué es el Scope?
Uno de los paradigmas más fundamentales de casi todos los lenguajes de programación
es la capacidad de almacenar valores en variables, y posteriormente recuperar o modificar
esos valores. De hecho, la capacidad de almacenar valores y extraer valores de variables es
lo que da un estado de programa.
Sin tal concepto, un programa podría realizar algunas tareas, pero serían extremadamente
limitados y terriblemente poco interesantes.
Pero, sin embargo, el motor de JavaScript realiza muchos de los mismos pasos, aunque de
formas más sofisticadas de lo que comúnmente se sabe, que cualquier compilador de
lenguaje tradicional.
El motor JavaScript es mucho más complejo que estos tres pasos, al igual que la mayoría
de los compiladores de otros lenguajes. Por ejemplo, en el proceso de análisis y generación
de código, hay ciertamente pasos para optimizar el rendimiento de la ejecución, incluyendo
el colapso de elementos redundantes, etc.
Por lo tanto, estoy pintando sólo con trazos amplios aquí. Pero creo que verá pronto por
qué estos detalles que cubrimos, incluso a un nivel alto, son relevantes.
Por un lado, los motores de JavaScript no obtienen el lujo (como otros compiladores de
lenguaje) de tener un montón de tiempo para optimizar, porque la compilación de
JavaScript no ocurre en un paso de construcción con antelación, como si ocurre con otros
lenguajes.
Digamos, por simplicidad, que cualquier fragmento de JavaScript tiene que ser compilado
antes (normalmente antes de que!) se ejecute. Por lo tanto, el compilador JS tomará el
programa var a = 2; y lo compila primero, y luego esta listo para ejecutarlo,
normalmente de inmediato.
1.2 Entendiendo el Scope
La forma en que abordaremos el aprendizaje sobre el scope es pensar en el proceso en
términos de una conversación. Pero, ¿quién está teniendo la conversación?
El elenco
Para que entiendas completamente cómo funciona JavaScript, debes comenzar a pensar
como Motor (y amigos), hacer las preguntas que hacen y responder a esas preguntas de la
misma manera.
Atrás y adelante
Cuando vea el programa var a = 2; , lo más probable es pensar en eso como una sola
declaración. Pero no es así como nuestro nuevo amigo Motor lo ve. De hecho, Motor ve dos
declaraciones distintas, una que el compilador manejará durante la compilación, y una que
el motor manejará durante la ejecución.
Así que, vamos a desglosar cómo Motor y amigos se acercará al programa var a = 2 ;.
Lo primero que Compilador hará con este programa es realizar lexing para dividirlo en
fichas, que luego analizará en un árbol. Pero cuando Compilador llega a la generación de
código, tratará este programa de manera algo diferente de lo que se supone.
Una suposición razonable sería que el compilador producirá código que podría ser
resumido por este pseudo-código: "Asignar memoria para una variable, etiquetar la a , y
luego pegar el valor 2 en esa variable". Desafortunadamente, eso no es muy exacto.
1. Encuentro var a , Compilador pregunta a Scope para ver si ya existe una variable a
para esa colección de ámbito en particular. Si es así, el compilador omite esta
declaración y sigue adelante. De lo contrario, el compilador solicita a Scope que
declare una nueva variable llamada a para esa colección de ámbito.
2. El Compilador entonces produce código para que el motor ejecute más adelante, para
manejar la asignación a = 2 . El código que Motor ejecuta primero preguntará a
Scope si hay una variable llamada accesible en la colección de alcance actual. Si es
así, el motor utiliza esa variable. Si no, el motor busca en otra parte (vea la sección de
ámbito anidada abajo).
Si finalmente el Motor encuentra una variable, le asigna el valor 2 . ¡Si no, el motor
levantará su mano y gritará hacia fuera un error!
En resumen, se toman dos acciones distintas para una asignación de variables: Primero, el
Compilador declara una variable (si no se ha declarado previamente en el ámbito actual) y
segundo, al ejecutar, Motor busca la variable en Scope y la asigna a ella, si se encuentra.
Compiler Speak
Cuando el Motor ejecuta el código que el Compilador produjo para el paso (2), tiene que
buscar la variable a para ver si se ha declarado, y esta búsqueda está consultando el
Scope. Pero el tipo de Motor de búsqueda que realiza afecta el resultado de la búsqueda.
En nuestro caso, se dice que Motor estaría realizando una búsqueda "LHS" para la variable
a . El otro tipo de búsqueda se llama "RHS".
Apuesto a que usted puede adivinar lo que la "L" y "R" significa. Estos términos representan
"Lado Izquierdo" y "Lado Derecho".
En otras palabras, una búsqueda LHS se hace cuando aparece una variable en el lado
izquierdo de una operación de asignación y una búsqueda RHS se hace cuando aparece
una variable en el lado derecho de una operación de asignación.
En realidad, seamos un poco más precisos. Una búsqueda de RHS es indistinguible, para
nuestros propósitos, de simplemente una mirada hacia arriba del valor de alguna variable,
mientras que la búsqueda de LHS está tratando de encontrar el contenedor de la variable
en sí, para que pueda asignar. De esta manera, RHS no significa realmente "lado derecho de
una asignación" per se, sino que, más exactamente, significa "no a la izquierda".
Siendo ligeramente glib por un momento, también podría pensar "RHS" en su lugar significa
"recuperar su fuente (valor)", lo que implica que RHS significa "ir a buscar el valor de ...".
Cuando yo digo:
console.log( a );
La referencia a es una referencia RHS, porque nada está siendo asignado aun aquí. En su
lugar, estamos buscando para recuperar el valor de a , por lo que el valor se puede pasar a
console.log(..) .
En contraste:
a = 2;
La referencia a aquí es una referencia LHS, porque realmente no nos importa cuál es el
valor actual, simplemente queremos encontrar la variable como un objetivo para la
operación de asignación = 2 .
Nota: LHS y RHS que significan "Lado izquierdo / derecho de una asignación" no significa
necesariamente literalmente "lado izquierdo / derecho del operador de asignación". Hay
varias otras maneras en que ocurren las asignaciones, por lo que es mejor pensar
conceptualmente en ella como: "quién es el objetivo de la asignación (LHS) - estoy
asignando" y "quién es la fuente de la asignación (RHS) - estoy buscando".
1 function foo(a) {
2 console.log( a ); // 2
3 }
4
5 foo( 2 );
La última línea que invoca foo(..) como una llamada de función requiere una referencia
RHS a foo , que significa "ir a buscar el valor de foo , y dármelo". Además, (..)
significa que el valor de foo debe ser ejecutado, por lo que sería en realidad una función!
También hay una referencia RHS para el valor de a , y ese valor resultante se pasa a
console.log(..) . console.log(..) necesita una referencia para ejecutarse. Es una
var foo y foo = function(a) {.... Al hacerlo, sería ser tentador pensar que esta
1 function foo(a) {
2 console.log( a ); // 2
3 }
4
5 foo( 2 );
Imaginemos el intercambio anterior (que procesa este fragmento de código) como una
conversación. La conversación sería algo como esto:
Motor: Hey Scope, tengo una referencia RHS para foo . ¿Has oído hablar de él?
Scope: Por qué? sí, lo he hecho. El Compilador lo declaró hace apenas un segundo. Es
una función. Aqui tienes.
Motor: Genial, gracias! OK, estoy ejecutando foo .
Motor: Hey, Scope, tengo una referencia de LHS para a , alguna vez oído hablar de él?
Scope: Por qué sí?, lo he hecho. El Compilador lo declaró como un parámetro formal para
foo hace poco. Aqui tienes.
Motor: Útil como siempre, Scope. Gracias de nuevo. Ahora, es tiempo para asignar 2 a
a .
Motor: Oye, Scope, siento molestarte de nuevo. Necesito una búsqueda de RHS para la
console . ¿Has oído hablar de él?
Scope: No hay problema, Motor, esto es lo que hago todo el día. Sí, tengo console . Está
integrado. Aqui tienes.
Motor: Yo, Scope. ¿Puedes ayudarme con una referencia de RHS a a . Creo que lo
recuerdo, pero sólo quiero comprobarlo.
Scope: Usted tiene razón, Motor. El mismo tipo, no ha cambiado. Aqui tienes.
...
Examen
Compruebe su comprensión hasta ahora. Asegúrese de jugar con la parte de Motor y tener
una "conversación" con el Scope:
1 function foo(a) {
2 var b = a;
3 return a + b;
4 }
5
6 var c = foo( 2 );
Nota: ¡Consulte la revisión del capítulo para las respuestas del examen!
1.3 Scopes Anidados
Dijimos que Scope es un conjunto de reglas para buscar variables por su nombre de
identificador. Sin embargo, generalmente hay más de un ámbito a considerar.
Así como un bloque o función está anidado dentro de otro bloque o función, los ámbitos
están anidados dentro de otros ámbitos. Por lo tanto, si no se puede encontrar una variable
en el ámbito inmediato, Motor consulta el siguiente ámbito externo que contiene, hasta que
se encuentre o hasta que se alcance el acope más externo (aka, global).
Considere:
1 function foo(a) {
2 console.log( a + b );
3 }
4
5 var b = 2;
6
7 foo( 2 ); // 4
La referencia RHS para b no se puede resolver dentro de la función foo , pero puede
resolverse en el ámbito que lo rodea (en este caso, global).
Motor: "Oye, Scope de foo , alguna vez oído hablar de b ? Tengo una referencia de RHS
para el."
Motor: "Oye, Scope fuera de foo , oh! eres el alcance global, ok cool. ¿Has oído hablar
de b ? Tengo una referencia de RHS para el."
Para visualizar el proceso de resolución anidada de un Scope, quiero que pienses en este
alto edificio.
Usted resuelve las referencias de LHS y RHS mirando su piso actual, y si no lo encuentra,
toma el ascensor al siguiente piso, buscando allí, luego al siguiente, y así sucesivamente.
Una vez que llegas a la planta superior (el alcance global), o bien encuentras lo que estás
buscando, o no lo haces. Pero tienes que parar en algún momento.
1.4 Errores
¿Por qué importa si lo llamamos LHS o RHS?
Debido a que estos dos tipos de consultas se comportan de manera diferente en las
circunstancias en las que la variable aún no ha sido declarada (no se encuentra en ningún
ámbito consultado).
Considere:
1 function foo(a) {
2 console.log( a + b );
3 b = a;
4 }
5
6 foo( 2 );
Cuando la búsqueda RHS ocurre para b la primera vez, no se encuentra. Se dice que esto
es una variable "no declarada", porque no se encuentra en el ámbito.
Si una búsqueda RHS falla al encontrar una variable, en cualquier lugar de los Scopes
anidados, esto resulta en un ReferenceError que está siendo lanzado por el Motor. Es
importante tener en cuenta que el error es del tipo ReferenceError .
Por el contrario, si el Motor está realizando una búsqueda LHS y llega a la planta superior
(ámbito global) sin encontrarlo, y si el programa no se está ejecutando en "modo estricto"
[^note-strictmode], entonces el ámbito global creará una nueva variable de ese nombre en el
ámbito global y la devolverá al Motor.
"No, no había una antes, pero fui útil y creé una para ti".
la resolución Scope tuvo éxito, pero que se ha intentado una acción ilegal/imposible contra
el resultado.
1.5 Revisión
El Scope es un conjunto de reglas que determina dónde y cómo se puede buscar una
variable (identificador). Esta búsqueda puede ser con el propósito de asignar a la variable,
que es una referencia LHS (izquierda), o puede ser con el propósito de recuperar su valor de
referencia, que es un RHS (lado derecho).
1. Primero, var a para declararlo en ese ámbito. Esto se realiza al principio, antes de la
ejecución del código.
2. Después, a = 2 para buscar la variable (referencia LHS) y asignarla si se encuentra.
1 function foo(a) {
2 var b = a;
3 return a + b;
4 }
5
6 var c = foo( 2 );
Hay dos modelos predominantes de cómo funciona el scope. El primero de ellos es, con
mucho, el más común, utilizado por la gran mayoría de lenguajes de programación. Se
llama Ámbito Léxico, y lo examinaremos en profundidad. El otro modelo, que sigue siendo
utilizado por algunos lenguajes (como Bash scripting, algunos modos en Perl, etc.) se
llama Dynamic Scope.
Es este concepto el que proporciona la base para entender qué es el ámbito léxico y de
dónde proviene el nombre.
Para definirlo, el ámbito léxico es el alcance que se define en el tiempo de lexing. En otras
palabras, el ámbito léxico se basa en donde son escritas por usted las variables y los
bloques de ámbito, en tiempo de escritura, y por lo tanto es (en su mayoría) establecidos
invariablemente en el momento en que el lexer procesa su código.
Nota: Vamos a ver en un poco que hay algunas maneras de engañar el ámbito léxico, por lo
tanto, podemos modificarlo después de que el lexer ha pasado, pero esto es algo mal visto.
Se considera la mejor práctica para tratar el ámbito léxico como, de hecho, sólo léxico, y
por lo tanto totalmente invariable en el tiempo por naturaleza.
1 function foo(a) {
2
3 var b = a * 2;
4
5 function bar(c) {
6 console.log( a, b, c );
7 }
8
9 bar(b * 3);
10 }
11
12 foo( 2 ); // 2 4 12
Hay tres ámbitos anidados inherentes en este ejemplo de código. Puede ser útil pensar en
estos ámbitos como burbujas dentro de los demás.
Burbuja 1 abarca el ámbito global, y tiene sólo un identificador en ella: foo .
Burbuja 2 abarca el alcance de foo , que incluye los tres identificadores: a , bar y b .
Las burbujas de alcance se definen por donde se escriben los bloques de alcance, el cual
está anidado uno dentro del otro, etc. En el capítulo siguiente, discutiremos diferentes
unidades de alcance, pero por ahora, supongamos que cada función crea una nueva
Burbuja de alcance.
La burbuja para bar está totalmente contenida dentro de la burbuja para foo , porque (y
sólo porque) es donde elegimos definir las funciones de bar .
Observe que estas burbujas anidadas están anidadas estrictamente. No estamos hablando
de diagramas de Venn donde las burbujas pueden cruzar los límites. En otras palabras,
ninguna burbuja para alguna función puede existir simultáneamente (parcialmente) dentro
de otras dos burbujas de alcance externo, así como ninguna función puede estar
parcialmente dentro de cada una de las dos funciones principales.
Consultas
foo
Nota: Las variables globales también son automáticamente propiedades del objeto global
(ventana en los navegadores, etc.), por lo que es posible hacer referencia a una variable
global no directamente por su nombre léxico, sino indirectamente como una referencia de
propiedad del objeto global.
window.a
Esta técnica da acceso a una variable global que de otro modo sería inaccesible debido a
que se sombreaba. No obstante, no se puede acceder a variables no globales sombreadas.
No importa de dónde se invoque una función, ni siquiera cómo se invoca, su alcance léxico
sólo se define por donde se declaró la función.
JavaScript tiene dos mecanismos. Ambos son igualmente mal vistos en la comunidad en
general como malas prácticas para usar en su código. Pero los argumentos típicos en
contra de ellos a menudo les falta el punto más importante: engañar el alcance léxico
conduce a un rendimiento más pobre.
eval
Evaluando eval(..) (juego de palabras a propósito) en esa luz, debería estar claro cómo
eval(..) le permite modificar el entorno del alcance léxico para hacer trampa y fingir que
1 function foo(str, a) {
2 eval( str ); // cheating!
3 console.log( a, b );
4 }
5
6 var b = 2;
7
8 foo( "var b = 3;", 1 ); // 1 3
La cadena " var b = 3; " es tratada en el punto de la llamada eval(..) , como código
que estuvo allí todo el tiempo. Debido a que el código pasa a declarar una nueva variable
b , modifica el ámbito léxico existente de foo(..) . De hecho, como se mencionó
anteriormente, este código realmente crea la variable b dentro de foo(..) que sombrea
la b que se declaró en el ámbito externo (global).
Nota: En este ejemplo, por simplicidad, la cadena de "código" que pasamos fue un literal
fijo. Pero podría haber sido creada mediante programación agregando caracteres juntos en
función de la lógica de su programa. eval(..) suele utilizarse para ejecutar código
creado dinámicamente, ya que evaluar dinámicamente un código esencialmente estático a
partir de una cadena literal no proporcionaría ningún beneficio real a la creación del código
directamente.
De forma predeterminada, si una cadena de código que eval(..) ejecuta contiene una o
más declaraciones (variables o funciones), esta acción modifica el ámbito léxico existente
en el que reside el eval(..) . Técnicamente, eval(..) puede ser invocado
"indirectamente", a través de varios trucos (que va más allá de nuestra discusión aquí), lo
que hace que en lugar de ejecutar en el contexto del ámbito global, por lo tanto, modificarlo.
Pero en cualquier caso, eval(..) puede en tiempo de ejecución modificar un ámbito
léxico de autor-tiempo.
Hay otras instalaciones en JavaScript que equivalen a un efecto muy similar al eval(.. ).
setTimeout(..) y setInterval(..) pueden tomar una cadena para su primer
argumento respectivo, cuyo contenido se evalúa como el código de una función generada
dinámicamente. Se trata de un comportamiento antiguo, heredado y desactualizado desde
hace mucho tiempo. ¡No lo hagas!
Los casos de uso para la generación dinámica de código dentro de su programa son
increíblemente raros, ya que las degradaciones del rendimiento casi nunca vale la pena la
capacidad.
with
El otro aspecto con el ceño fruncido (y ahora desaprobado!) en JavaScript que engaña el
ámbito léxico es la palabra clave with . Existen múltiples vías válidas que pueden
explicarse, pero voy a elegir aquí para explicarlo desde la perspectiva de cómo interactúa y
afecta el alcance léxico.
característica contra un objeto sin repetir la referencia del objeto mismo cada vez.
Por ejemplo:
1 var obj = {
2 a: 1,
3 b: 2,
4 c: 3
5 };
6
7 // más "tedioso" repetir "obj"
8 obj.a = 2;
9 obj.b = 3;
10 obj.c = 4;
11
12 // atajo "mas fácil"
13 with (obj) {
14 a = 3;
15 b = 4;
16 c = 5;
17 }
Sin embargo, hay mucho más que hacer aquí que sólo un atajo conveniente para el acceso
a la propiedad del objeto. Considere:
1 function foo(obj) {
2 with (obj) {
3 a = 2;
4 }
5 }
6
7 var o1 = {
8 a: 3
9 };
10
11 var o2 = {
12 b: 3
13 };
14
15 foo( o1 );
16 console.log( o1.a ); // 2
17
18 foo( o2 );
19 console.log( o2.a ); // undefined
20 console.log( a ); // 2 -- Oops, leaked global!
En este ejemplo de código, se crean dos objetos o1 y o2 . Uno tiene una propiedad y el
otro no. La función foo(..) toma una referencia de objeto obj como un argumento, y
se llama with (obj) {..} en la referencia. Dentro del bloque with , hacemos lo que
parece ser una referencia léxica normal a una variable a , una referencia LHS de hecho
(ver Capítulo 1), para asignarle el valor de 2 .
Pero luego observamos un efecto secundario peculiar, el hecho de que una variable global
a fue creada por la asignación a = 2 . ¿Cómo puede ser esto posible?
La instrucción with toma un objeto, uno que tiene cero o más propiedades, y trata ese
objeto como si fuera un ámbito léxico completamente separado, por lo tanto las
propiedades del objeto son tratadas como identificadores lexicalmente definidos en ese
"ámbito".
Nota: A pesar de que un bloque with trata un objeto como un ámbito léxico, una
declaración var normal dentro de ese con el bloque no será delimitada con el bloque, sino
el ámbito de la función que contiene.
Mientras que la función eval(...) puede modificar el alcance léxico existente si toma
una cadena de código con una o más declaraciones en ella, la instrucción with crea
realmente un ámbito léxico completamente nuevo desde el aire, desde el objeto al que se
pasa .
Entendido de esta manera, el "ámbito" declarado por la instrucción with cuando pasamos
en o1 era o1 , y "scope" tenía un "identificador" en el que corresponde a la propiedad
o1.a . Pero cuando usamos o2 como el "ámbito", no tenía tal "identificador" en él, por lo
Nota: Además de ser una mala idea de usar, ambos eval(..) y with son afectados
(restringido) por el modo estricto. With es descartada completamente, mientras que
varias formas de eval(...) indirecto o inseguro son rechazadas mientras que retiene la
funcionalidad del core.
Rendimiento
En otras palabras, en el sentido pesimista, la mayoría de las optimizaciones que haría son
inútiles si eval(..) o with están presentes, por lo que simplemente no realiza las
optimizaciones en absoluto.
Su código casi siempre tiende a correr más lento simplemente por el hecho de que incluye
un eval(..) o un with en cualquier parte del código. No importa lo inteligente que sea
el Motor para tratar de limitar los efectos secundarios de estos supuestos pesimistas, no
hay manera de evitar que, sin las optimizaciones, el código funcione más lentamente.
2.3 Revisión
El alcance léxico significa que el alcance se define por las decisiones de tiempo de autor de
donde se declaran las funciones. La fase de lexing de la compilación es esencialmente
capaz de saber dónde y cómo se declaran todos los identificadores y así predecir cómo se
buscarán durante la ejecución.
La desventaja de estos mecanismos es que derrota la capacidad del Motor para realizar
optimizaciones en tiempo de compilación en cuanto a la búsqueda de scope, porque el
Motor tiene que asumir pesimistamente que tales optimizaciones serán inválidas. El código
se ejecutará más lentamente como resultado de usar cualquiera de las funciones. No las
use.
3- Function vs. Block Scope
Como se exploró en el capítulo 2, el alcance consiste en una serie de "burbujas" y cada uno
actúa como un contenedor o cubo, en el que se declaran identificadores (variables,
funciones). Estas burbujas se anidan perfectamente dentro de sí, y esta anidación se define
a la hora del autor.
Pero, ¿qué hace exactamente una nueva burbuja? ¿Es sólo la función? ¿Pueden otras
estructuras en JavaScript crear burbujas de alcance?
3.1 Ámbito de las funciones
La respuesta más común a esas preguntas es que JavaScript tiene un alcance/
ámbito/scope basado en funciones. Es decir, cada función que declara crea una nueva
burbuja para sí misma, pero ninguna otra estructura crea sus propias burbujas de alcance.
Como veremos más adelante, esto no es del todo cierto.
1 function foo(a) {
2 var b = 2;
3
4 // some code
5
6 function bar() {
7 // ...
8 }
9
10 // more code
11
12 var c = 3;
13 }
En este fragmento, la burbuja de alcance para foo(..) incluye los identificadores a , b ,
c y bar . No importa dónde en el ámbito de una declaración aparezca, la variable o
bar(..) tiene su propia burbuja de alcance. Lo mismo ocurre con el ámbito global, que
Debido a que a , b , c , y bar pertenecen todos a la burbuja de alcance de foo(..) ,
no son accesibles fuera de foo(..) . Es decir, el código siguiente resultaría en errores
ReferenceError , ya que los identificadores no están disponibles para el ámbito global:
1 bar(); // fails
2
3 console.log( a, b, c ); // all 3 fail
Sin embargo, todos estos identificadores ( a , b , c , foo y bar ) son accesibles dentro
de foo(..) , y de hecho también disponible dentro de bar(..) (suponiendo que no hay
declaraciones de identificador de sombra dentro de bar(..) ).
El ámbito de la función fomenta la idea de que todas las variables pertenecen a la función
y pueden utilizarse y reutilizarse a lo largo de toda la función (incluso accesibles a los
ámbitos anidados). Este enfoque de diseño puede ser muy útil, y ciertamente puede hacer
pleno uso de la naturaleza "dinámica" de las variables JavaScript para asumir valores de
diferentes tipos según sea necesario.
Por otro lado, si no se toman precauciones cuidadosas, las variables existentes a través de
la totalidad de un ámbito puede conducir a algunas trampas inesperadas.
3.2 Ocultación en el ámbito común
La forma tradicional de pensar acerca de las funciones es que declares una función y
luego agregas código dentro de ella. Pero el pensamiento inverso es igualmente poderoso y
útil: toma cualquier sección arbitraria de código que hayas escrito, y envuelve una
declaración de función alrededor de ella, que en efecto "oculta" el código.
El resultado práctico es crear una burbuja de alcance alrededor del código en cuestión, lo
que significa que cualquier declaración (variable o función) en ese código ahora estará
vinculada al ámbito de la nueva función de empaquetado, en lugar del ámbito de inclusión
anterior. En otras palabras, puede "ocultar" variables y funciones encerrándolas en el
ámbito de una función.
¿Por qué las variables y funciones "ocultas" serían una técnica útil?
Hay una variedad de razones que motivan este ocultamiento basado en el alcance. Ellos
tienden a surgir a partir del principio de diseño de software "Principio de Menor Privilegio",
también llamado a veces "Menos Autoridad" o "Menos Exposición". Este principio establece
que en el diseño de software, como la API para un módulo/objeto, debe exponer sólo lo que
es mínimamente necesario y "ocultar" todo lo demás.
Este principio se extiende a la elección de qué ámbito debe contener variables y funciones.
Si todas las variables y funciones estuvieran en el ámbito global, obviamente serían
accesibles para cualquier ámbito anidado. Pero esto violaría el principio de "Menos ..." en el
que usted está (probablemente) exponiendo muchas variables o funciones que de otra
manera debería mantener privadas, ya que el uso apropiado del código desalentaría el
acceso a esas variables/funciones.
Por ejemplo:
1 function doSomething(a) {
2 b = a + doSomethingElse( a * 2 );
3
4 console.log( b * 3 );
5 }
6
7 function doSomethingElse(a) {
8 return a - 1;
9 }
10
11 var b;
12
13 doSomething( 2 ); // 15
Un diseño más "apropiado" ocultaría estos detalles privados dentro del alcance de
doSomething(...) , tales como:
1 function doSomething(a) {
2 function doSomethingElse(a) {
3 return a - 1;
4 }
5
6 var b;
7
8 b = a + doSomethingElse( a * 2 );
9
10 console.log( b * 3 );
11 }
12
13 doSomething( 2 ); // 15
Evitar colisiones
Otro beneficio de "ocultar" variables y funciones dentro de un ámbito es evitar la colisión no
deseada entre dos identificadores diferentes con el mismo nombre pero con diferentes
usos. Los resultados de la colisión a menudo producen una sobrescritura inesperada de los
valores.
Por ejemplo:
1 function foo() {
2 function bar(a) {
3 i = 3; // Cambiar el `i` en el ámbito de inclusión for-loop
4 console.log( a + i );
5 }
6
7 for (var i=0; i<10; i++) {
8 bar( i * 2 ); // Oops, bucle infinito!
9 }
10 }
11
12 foo();
La asignación dentro de la bar(..) necesita declarar una variable local para usar,
independientemente del nombre de identificador elegido. var i = 3; solucionaría el
problema (y crearía la declaración de "variable sombreada" anteriormente mencionada para
i ). Una opción adicional, no alternativa, es escoger otro nombre de identificador, tal como
Global "Namespaces"
Por ejemplo:
1 var MyReallyCoolLibrary = {
2 awesome: "stuff",
3 doSomething: function() {
4 // ...
5 },
6 doAnotherThing: function() {
7 // ...
8 }
9 };
Gestión de módulos
Otra opción para evitar colisiones es el más moderno enfoque de "módulo", utilizando
cualquiera de los distintos administradores de dependencia. Utilizando estas herramientas,
ninguna biblioteca agrega identificadores al ámbito global, sino que se requiere que su
identificador(es) sean explícitamente importados a otro ámbito específico mediante el uso
de los diversos mecanismos del administrador de dependencias.
Debe observarse que estas herramientas no poseen funcionalidad "mágica" que está
exenta de reglas de alcance léxico. Simplemente usan las reglas de alcance como se
explica aquí para hacer cumplir que no se inyectan identificadores en ningún ámbito
compartido y, en cambio, se mantienen en ámbitos privados no susceptibles a colisiones,
lo que evita colisiones de alcance accidental.
Como tal, puede su código defensivamente, lograr los mismos resultados que los hacen los
administradores de dependencias sin necesidad de utilizarlos, si así lo desea. Consulte el
Capítulo 5 para obtener más información sobre el patrón del módulo.
3.3 Funciones como ámbitos
Hemos visto que podemos tomar cualquier fragmento de código y envolver una función
alrededor de él, y que efectivamente "oculta" cualquier declaración de función o variable
encerrada desde el ámbito externo dentro del ámbito interno de esa función.
Por ejemplo:
1 var a = 2;
2
3 function foo() { // <-- insert this
4
5 var a = 3;
6 console.log( a ); // 3
7
8 } // <-- and this
9 foo(); // <-- and this
10
11 console.log( a ); // 2
Si bien esta técnica "funciona", no es necesariamente ideal. Hay algunos problemas que
introduce. El primero es que tenemos que declarar una función nombrada foo() , lo que
significa que el nombre del identificador foo mismo "contamina" el ámbito de inclusión
(global, en este caso). También tenemos que llamar explícitamente a la función por nombre
( foo() ) para que el código envuelto en realidad se ejecute.
Sería más ideal si la función no necesitaba un nombre (o, más bien, el nombre no
contamine el ámbito de inclusión) y si la función podría ejecutarse automáticamente.
1 var a = 2;
2
3 (function foo(){ // <-- insert this
4
5 var a = 3;
6 console.log( a ); // 3
7
8 })(); // <-- and this
9
10 console.log( a ); // 2
La diferencia clave que podemos observar aquí entre una declaración de función y una
expresión de función se refiere a donde su nombre está enlazado como un identificador.
Compare los dos fragmentos anteriores. En el primer fragmento, el nombre foo está
enlazado en el ámbito de inclusión, y lo llamamos directamente con foo() . En el segundo
snippet, el nombre foo no está enlazado en el ámbito de inclusión, sino que está limitado
sólo dentro de su propia función.
En otras palabras, (function foo() {..}) como una expresión significa que el
identificador foo se encuentra sólo en el ámbito donde se indican los tres puntos ... ,
no en el ámbito externo. Ocultar el nombre foo dentro de sí significa que no contamina el
ámbito de inclusión innecesariamente.
Probablemente esté más familiarizado con las expresiones de función como parámetros
de devolución de llamada, como:
1 setTimeout( function(){
2 console.log("I waited 1 second!");
3 }, 1000 );
Las expresiones de función pueden ser anónimas, pero las declaraciones de funciones no
pueden omitir el nombre, que sería una gramática ilegal en JS.
1. Las funciones anónimas no tienen ningún nombre útil para mostrar en lo stack trace
(errores), lo que puede dificultar la depuración.
2. Sin un nombre, si la función necesita referirse a sí misma, para la recursión, etc., la
referencia deprecated arguments.callee es desafortunadamente requerida. Otro
ejemplo de la necesidad de autorreferencia es cuando una función de controlador de
eventos desea desvincularse después de que se activa.
3. Las funciones anónimas omiten un nombre que es a menudo útil en proporcionar
código más legible/comprensible. Un nombre descriptivo ayuda a auto-documentar el
código en cuestión.
1 var a = 2;
2
3 (function foo(){
4
5 var a = 3;
6 console.log( a ); // 3
7
8 })();
9
10 console.log( a ); // 2
Ahora que tenemos una función como una expresión en virtud de envolverla en un par
( ) , podemos ejecutar esa función añadiendo otro ( ) al final, como
(función foo() {..}) () . El primer par enclosing ( ) hace que la función sea una
Este patrón es tan común, hace unos años la comunidad acordó un término para ello:
IIFE , que significa expresión de función inmediatamente invocada.
Por supuesto, los IIFE no necesitan nombres, necesariamente - la forma más común de
IIFE es usar una expresión de función anónima. Aunque ciertamente menos común,
nombrar un IIFE tiene todos los beneficios antes mencionados sobre expresiones de
función anónimas, por lo que es una buena práctica por adoptar.
1 var a = 2;
2
3 (function IIFE(){
4
5 var a = 3;
6 console.log( a ); // 3
7
8 })();
9
10 console.log( a ); // 2
Hay una ligera variación en el formulario tradicional IIFE, que algunos prefieren:
(function() {..}()) . Mira con atención para ver la diferencia. En la primera forma, la
Estas dos formas son idénticas en funcionalidad. Es puramente una elección estilística que
usted prefiera.
Otra variación en IIFE que es bastante común es usar el hecho de que son, de hecho, sólo
llamadas de función, y pasarlas en argumento(s).
Por ejemplo:
1 var a = 2;
2
3 (function IIFE( global ){
4
5 var a = 3;
6 console.log( a ); // 3
7 console.log( global.a ); // 2
8
9 })( window );
10
11 console.log( a ); // 2
Otra aplicación de este patrón aborda los concerns (nicho menor) de que el identificador
indefinido por defecto podría tener su valor incorrectamente sobrescrito, causando
resultados inesperados. Al nombrar un parámetro indefinido, pero sin pasar ningún valor
para ese argumento, podemos garantizar que el identificador indefinido es de hecho el
valor indefinido en un bloque de código:
1 undefined = true; // El establecimiento de una mina de tierra para el otro
2
3 (function IIFE( undefined ){
4
5 var a;
6 if (a === undefined) {
7 console.log( "Undefined is safe here!" );
8 }
9
10 })();
Otra variación del IIFE invierte el orden de las cosas, donde la función a ejecutar se da en
segundo lugar, después de la invocación y los parámetros para pasarle. Este patrón se
utiliza en el proyecto UMD (Universal Module Definition). Algunas personas lo encuentran
un poco más limpio de entender, aunque es un poco más detallado.
1 var a = 2;
2
3 (function IIFE( def ){
4 def( window );
5 })(function def( global ){
6
7 var a = 3;
8 console.log( a ); // 3
9 console.log( global.a ); // 2
10
11 });
Muchos lenguajes distintos de JavaScript soportan Block Scope, por lo que los
desarrolladores de esos lenguajes están acostumbrados a esa mentalidad, mientras que
aquellos que sólo han trabajado principalmente en JavaScript pueden encontrar el
concepto un poco extraño.
Pero incluso si usted nunca ha escrito una sola línea de código en forma de bloque de
ámbito, usted todavía está probablemente familiarizado con este lenguaje muy común en
JavaScript:
Eso es a lo que se refiere el alcance de bloque. Declarar las variables lo más cerca posible,
lo más local posible, a donde se van a utilizar. Otro ejemplo:
El alcance del bloque es una herramienta para extender el "Principio de Exposición al Menor
Privilegio" anterior desde ocultar información en funciones a ocultar información en
bloques de nuestro código.
¿Por qué contaminar todo el ámbito de una función con la variable i que sólo va a ser (o
sólo debería ser, al menos) utilizada para el bucle for ?
Pero lo que es más importante, los desarrolladores pueden preferir revisarse contra el uso
(re) accidental de variables ajenas a su propósito, como ser emitido un error sobre una
variable desconocida si intenta utilizarlo en el lugar equivocado. Bloquear el ámbito (si
fuera posible) para la variable i haría i disponible sólo para el bucle for , causando
un error si se accede a otro lugar en la función. Esto ayuda a asegurar que las variables no
se vuelvan a utilizar en formas confusas o difíciles de mantener.
Pero, la triste realidad es que, en la superficie, JavaScript no tiene ninguna facilidad para el
alcance del bloque.
with
Hemos aprendido acerca de with en el Capítulo 2. Si bien es un fruncido a construir, es un
ejemplo de (una forma de) ámbito de bloque, en el que el ámbito que se crea desde el
objeto sólo existe para la vida de with con la declaración, y no en el ámbito circundante.
try/catch
Por ejemplo:
1 try {
2 undefined(); // illegal operation to force an exception!
3 }
4 catch (err) {
5 console.log( err ); // works!
6 }
7
8 console.log( err ); // ReferenceError: `err` not found
Como puede ver, err existe sólo en la cláusula catch y lanza un error cuando intenta
hacer referencia a él en otro lugar.
Para evitar estas advertencias innecesarias, algunos devs nombrarán sus variables de
catch err1 , err2 , etc. Otros devs simplemente desactivarán la comprobación de
let
Hasta ahora, hemos visto que JavaScript sólo tiene algunos comportamientos de nicho
extraño que exponen la funcionalidad de ámbito de bloque. Si eso fuera todo lo que
tenemos, y de hecho tuvimos por muchos, muchos años, entonces el alcance de bloque no
sería útil para el desarrollador de JavaScript.
Afortunadamente, ES6 cambia eso, e introduce una nueva palabra clave let que se pone
junto a var como otra forma de declarar variables.
El uso de let para adjuntar una variable a un bloque existente es algo implícito. Puede
confundirte si no estás prestando mucha atención a los bloques que tienen variables de
alcance para ellos, y tienen el hábito de mover bloques alrededor, envolverlos en otros
bloques, etc, a medida que desarrolla y evoluciona código.
Nota: Para otra forma de expresar los ámbitos explícitos de bloque, consulte el Apéndice B.
Sin embargo, las declaraciones hechas con let no se "hoistean" a todo el alcance del
bloque en el que aparecen. Tales declaraciones no "existirán" observables en el bloque
hasta la declaración.
1 {
2 console.log( bar ); // ReferenceError!
3 let bar = 2;
4 }
Garbage Collection
Otra razón de porque bloquear el alcance es útil se refiere a los closures y la Garbage
Collection (recolección de basura) para recuperar la memoria. Lo vamos a ilustrar
brevemente aquí, pero el mecanismo de closure se explica en detalle en el capítulo 5.
Considere:
1 function process(data) {
2 // do something interesting
3 }
4
5 var someReallyBigData = { .. };
6
7 process( someReallyBigData );
8
9 var btn = document.getElementById( "my_button" );
10
11 btn.addEventListener( "click", function click(evt){
12 console.log("button clicked");
13 }, /*capturingPhase=*/false );
Bloquear el alcance puede abordar esta preocupación, por lo que es más claro para el
motor que no es necesario mantener someReallyBigData alrededor:
1 function process(data) {
2 // do something interesting
3 }
4
5 // anything declared inside this block can go away after!
6 {
7 let someReallyBigData = { .. };
8
9 process( someReallyBigData );
10 }
11
12 var btn = document.getElementById( "my_button" );
13
14 btn.addEventListener( "click", function click(evt){
15 console.log("button clicked");
16 }, /*capturingPhase=*/false );
Declarar bloques explícitos para que las variables se enlacen localmente es una poderosa
herramienta que puede agregar a su caja de herramientas de código.
Bucles let
Un caso particular donde let brilla está en el caso del ciclo for como ha sido discutido
previamente.
No sólo deja en el encabezado del for-loop unido el i al cuerpo del for-loop, sino que de
hecho, vuelve a enlazarlo a cada iteración del loop, asegurándose de volver a asignar el
valor desde el final de la iteración del bucle anterior.
1 {
2 let j;
3 for (j=0; j<10; j++) {
4 let i = j; // re-bound for each iteration!
5 console.log( i );
6 }
7 }
La razón por la cual esta vinculación por iteración es interesante será aclarada en el
Capítulo 5 cuando discutamos closures.
Considere:
Vea el Apéndice B para un estilo alterno (más explícito) de alcance de bloque que puede
proporcionar un código más fácil de mantener/refactorizar que es más robusto a estos
escenarios.
const
Además de let , ES6 introduce const , que también crea una variable de ámbito de
bloque, pero cuyo valor es fijo (constante). Cualquier intento de cambiar ese valor en un
momento posterior da como resultado un error.
Pero las funciones no son de ninguna manera la única unidad de ámbito. Block-scope se
refiere a la idea de que las variables y las funciones pueden pertenecer a un bloque
arbitrario (generalmente, cualquier par {..} ) de código, en lugar de sólo a la función de
inclusión.
En ES6, se introduce la palabra clave let (un primo de la palabra clave var ) para
permitir declaraciones de variables en cualquier bloque arbitrario de código.
if(..) {let a = 2; } Declarará una variable a que esencialmente perteneciente a el
Aunque algunos parecen creerlo así, el alcance del bloque no debe ser tomado como un
reemplazo directo del alcance de la función var . Ambas funcionalidades coexisten, y los
desarrolladores pueden y deben utilizar tanto las técnicas de alcance de función como de
ámbito de bloque, donde son apropiadas para producir un código mejor, más legible y más
fácil de mantener.
4- Hoisting
Por ahora, debe estar bastante cómodo con la idea de los scope, y cómo las variables se
adjuntan a diferentes niveles de ámbito dependiendo de dónde y cómo se declaran. Tanto
el ámbito de la función como el de bloque se comportan por las mismas reglas a este
respecto: cualquier variable declarada dentro de un ámbito se adjunta a ese ámbito.
Pero hay un detalle sutil de cómo funciona la inclusión de un ámbito con declaraciones
que aparecen en varios lugares dentro de un ámbito, y ese detalle es lo que examinaremos
aquí.
4.1 ¿El Huevo o la Gallina?
Hay una tentación de pensar que todo el código que se ve en un programa JavaScript se
interpreta línea por línea, de arriba hacia abajo en orden, tal y como se ejecuta el programa.
Si bien eso es sustancialmente cierto, hay una parte de esa suposición que puede conducir
a un pensamiento incorrecto sobre su programa.
1 a = 2;
2
3 var a;
4
5 console.log( a );
1 console.log( a );
2
3 var a = 2;
Es posible que se sienta tentado a asumir que, dado que el fragmento anterior mostraba un
comportamiento de apariencia inferior a superior, tal vez en este fragmento, también se
imprimirá 2 . Otros pueden pensar que ya que la variable a se utiliza antes de que se
declara, esto debe resultar en un ReferenceError .
Por lo tanto, la mejor manera de pensar acerca de las cosas es que todas las declaraciones,
tanto las variables como las funciones, se procesan primero, antes de ejecutar cualquier
parte de su código.
Cuando ve var a = 2 ; , probablemente piensa en eso como una sentencia. Pero
JavaScript realmente piensa en ello como dos declaraciones: var a; Y a = 2 ; . La
primera declaración, la declaración, se procesa durante la fase de compilación. La segunda
sentencia, la asignación, se deja en su lugar para la fase de ejecución.
Nuestro primer fragmento entonces debe ser pensado como si estuviera siendo manejado
igual a esto:
var a;
1 a = 2;
2
3 console.log( a );
var a;
1 console.log( a );
2
3 a = 2;
Por lo tanto, una forma de pensar, de manera metafórica, sobre este proceso, es que las
declaraciones de variables y de funciones se "mueven" desde donde aparecen en el flujo del
código hasta la parte superior del código. Esto da lugar al nombre "Hoisting".
Nota: Solamente las declaraciones son subidas (hoisted), mientras que cualquier
asignación u otra lógica ejecutable se deja en su lugar. Si la subida fuera a reorganizar la
lógica ejecutable de nuestro código, eso podría causar estragos.
1 foo();
2
3 function foo() {
4 console.log( a ); // undefined
5
6 var a = 2;
7 }
Se sube la declaración de la función foo (que en este caso incluye el valor implícito de
ella como una función real), de modo que la llamada en la primera línea pueda ejecutarse.
También es importante tener en cuenta que la subida es por alcance. Así, mientras que
nuestros fragmentos anteriores se simplificaron en que sólo incluían alcance global, la
función foo(..) que ahora estamos examinando muestra que var a se sube a la parte
superior de foo(..) (no, obviamente, a la parte superior del programa). Por lo tanto, el
programa puede interpretarse de la siguiente manera:
1 function foo() {
2 var a;
3
4 console.log( a ); // undefined
5
6 a = 2;
7 }
8
9 foo();
Las declaraciones de funciones se suben, como acabamos de ver. Pero las expresiones
funcionales no se suben.
1 foo(); // TypeError
2 bar(); // ReferenceError
3
4 var foo = function bar() {
5 // ...
6 };
1 var foo;
2
3 foo(); // TypeError
4 bar(); // ReferenceError
5
6 foo = function() {
7 var bar = ...self...
8 // ...
9 }
4.3 Funciones Primero
Se sube tanto las declaraciones de funciones como las declaraciones de variables. Pero un
detalle sutil (que puede aparecer en el código con varias declaraciones "duplicadas") es que
las funciones se suben primero y luego las variables.
Considere:
1 foo(); // 1
2
3 var foo;
4
5 function foo() {
6 console.log( 1 );
7 }
8
9 foo = function() {
10 console.log( 2 );
11 };
1 function foo() {
2 console.log( 1 );
3 }
4
5 foo(); // 1
6
7 foo = function() {
8 console.log( 2 );
9 };
Tenga en cuenta que var foo era la declaración duplicada (y por lo tanto ignorada),
aunque llegó antes de la declaración function foo() ... , porque las declaraciones de
función son subidas antes que las variables normales.
Mientras las declaraciones var multiples/duplicadas se ignoran de forma efectiva, las
declaraciones de función posteriores reemplazan a las anteriores.
1 foo(); // 3
2
3 function foo() {
4 console.log( 1 );
5 }
6
7 var foo = function() {
8 console.log( 2 );
9 };
10
11 function foo() {
12 console.log( 3 );
13 }
Si bien esto todo puede sonar como nada más que curiosidades académicas interesantes,
pone de relieve el hecho de que las definiciones duplicadas en el mismo alcance son una
idea realmente mala y, a menudo llevará a resultados confusos.
Las declaraciones de funciones que aparecen dentro de los bloques normales típicamente
se elevan al ámbito de inclusión, en lugar de ser condicionales como implica este código:
1 foo(); // "b"
2
3 var a = true;
4 if (a) {
5 function foo() { console.log( "a" ); }
6 }
7 else {
8 function foo() { console.log( "b" ); }
9 }
Sin embargo, es importante tener en cuenta que este comportamiento no es fiable y está
sujeto a cambios en futuras versiones de JavaScript, por lo que es mejor evitar declarar
funciones en bloques.
4.4 Revisión
Podemos ser tentados a mirar var a = 2; como una declaración, pero el Motor de
JavaScript no lo ve así. Ve var a y a = 2 como dos sentencias separadas, la primera es
una tarea de fase-compilador y la segunda es una tarea de fase-ejecución.
Las declaraciones mismas son elevadas, pero las asignaciones, incluso las asignaciones
de las expresiones de la función, no se elevan.
Tenga cuidado con las declaraciones duplicadas, especialmente mezcladas entre las
declaraciones normales de var y las declaraciones de funciones - ¡el peligro lo espera si
lo hace!
5- Scope Closure
Llegamos a este punto con, esperanzadamente, una comprensión muy sana y sólida de
cómo funciona el scope/ámbito/alcance.
Sin embargo, si usted tiene preguntas persistentes sobre el alcance léxico, ahora sería un
buen momento para volver y revisar el capítulo 2 antes de proceder.
5.1 Ilustración
Para aquellos que tienen algo de experiencia en JavaScript, pero quizás nunca han
comprendido completamente el concepto de closures, entender el closure puede parecer un
nirvana especial que uno debe esforzarse y sacrificarse por alcanzar.
Recuerdo años atrás cuando tenía una comprensión firme de JavaScript, pero no tenía idea
de qué era closures. La indirecta de que había ese otro lado del lenguaje, que me prometió
aún más capacidad de la que ya poseía, me burlaba y me burlaba. Recuerdo leer a través
del código fuente de los primeros frameworks tratando de entender cómo funciona
realmente. Recuerdo la primera vez que algo del "patrón de módulo" comenzó a emerger en
mi mente. ¡Recuerdo el a-ha! Momentos muy vívidos.
Lo que no sabía en ese entonces, lo que me llevó años entender, y lo que espero poder
impartir, es el secreto: el closure está en todas partes de JavaScript, sólo hay que
reconocerlo y abrazarlo. Closures no son una herramienta especial de opt-in que usted debe
aprender nuevas sintaxis y patrones. No, los closures no son ni siquiera un arma que debes
aprender a manejar y dominar como Luke entrenó en La Fuerza.
Los closures ocurren como resultado de escribir código que se basa en el alcance léxico.
Simplemente suceden. Usted ni siquiera tiene que intencionalmente crear closures para
tomar ventaja de ellos. Los closures se crean y se usan para usted en todo su código. Lo
que le falta es el contexto mental apropiado para reconocer, abrazar y apalancar los
closures por su propia voluntad.
Aquí está una definición de lo que usted necesita saber para entender y reconocer los
closures:
1 function foo() {
2 var a = 2;
3
4 function bar() {
5 console.log( a ); // 2
6 }
7
8 bar();
9 }
10
11 foo();
Este código debe parecer familiar a nuestras discusiones de Nested Scope. La funcion
bar() tiene acceso a la variable a en el ámbito externo de inclusión debido a las reglas
de búsqueda de alcance léxico (en este caso, es una referencia de referencia RHS).
Bueno, técnicamente ... tal vez. Pero con nuestra definición de lo que necesitas saber arriba
... no exactamente. Creo que la manera más precisa de explicar la forma en que bar()
hace referencia a a es a través de las reglas de búsqueda de alcance léxico, y esas reglas
son sólo (una parte importante!) de lo que es el closure.
Desde una perspectiva puramente académica, lo que se dice del fragmento anterior es que
la función bar() tiene un closure sobre el alcance de foo() (e incluso sobre el resto de
los ámbitos a los que tiene acceso, como el alcance global en nuestro caso). Se dice que
bar() se cierra (closes) sobre el alcance de foo() . ¿Por qué? porque bar() aparece
1 function foo() {
2 var a = 2;
3
4 function bar() {
5 console.log( a );
6 }
7
8 return bar;
9 }
10
11 var baz = foo();
12
13 baz(); // 2 -- Whoa, el cierre se acaba de observar, hombre!.
La funcion bar() tiene acceso de alcance léxico al ámbito interno de foo() . Pero
entonces tomamos bar() , la función en sí, y la pasamos como un valor. En este caso,
retornamos el objeto de función propiamente dicho que hace referencia a bar .
Después de ejecutar foo() , asignamos el valor que retorno (nuestra función interna
bar() ) a una variable llamada baz , y luego invocamos baz() , que por supuesto está
invocando nuestra función interna bar() , sólo por una referencia de identificador
diferente.
bar() se ejecuta, por supuesto. Pero en este caso, se ejecuta fuera de su ámbito léxico
declarado.
Pero la "magia" de los closures no deja que esto suceda. Ese alcance interior está, de hecho,
todavía "en uso", y por lo tanto no desaparece. ¿Quién lo usa? La funcion bar() en sí.
En virtud de donde fue declarado, bar() tiene un closure de alcance léxico sobre ese
alcance interno de foo() , que mantiene ese alcance vivo para que bar() le haga una
referencia en cualquier momento posterior.
bar() todavía tiene una referencia a ese ámbito, y esa referencia se llama closure.
Por lo tanto, unos pocos microsegundos más tarde, cuando se invoca la variable baz
(invocando la función interna inicialmente etiquetada bar ), tiene debidamente acceso al
ámbito lexical de autor-tiempo, por lo que puede acceder a la variable tal como
esperábamos.
Por supuesto, cualquiera de las varias maneras en que las funciones pueden ser pasadas
alrededor como valores, y de hecho invocadas en otros lugares, son todos ejemplos de
observación/ejercicio de closures.
1 function foo() {
2 var a = 2;
3
4 function baz() {
5 console.log( a ); // 2
6 }
7
8 bar( baz );
9 }
10
11 function bar(fn) {
12 fn(); // Mira ma, vi el closure!
13 }
Pasamos la función interna baz a la funcion bar() , y llamamos a esa función interna
(etiquetada fn ahora), y cuando lo hacemos, su closure sobre el alcance interno de
foo() se observa, accediendo a a .
1 var fn;
2
3 function foo() {
4 var a = 2;
5
6 function baz() {
7 console.log( a );
8 }
9
10 fn = baz; // asigna `baz` a una variable global
11 }
12
13 function bar() {
14 fn(); // Mira ma, vi el closure!
15 }
16
17 foo();
18
19 bar(); // 2
Cualquiera que sea la facilidad que utilicemos para transportar una función interna fuera
de su ámbito léxico, mantendrá una referencia de alcance a donde originalmente fue
declarada, y dondequiera que la ejecute, se ejercerá ese closure.
5.3 Ahora puedo ver
Los fragmentos de código anteriores son algo académicos y construidos artificialmente
para ilustrar el uso del closures. Pero te prometí algo más que un juguete nuevo. Le prometí
que el closure era algo que aparecía constantemente a su alrededor en su código existente.
Veamos ahora esa verdad.
1 function wait(message) {
2
3 setTimeout( function timer(){
4 console.log( message );
5 }, 1000 );
6
7 }
8
9 wait( "Hello, closure!" );
Closure.
O, si usted esta persuadido por jQuery (o cualquier framework JS, para el caso):
1 function setupBot(name,selector) {
2 $( selector ).click( function activator(){
3 console.log( "Activating: " + name );
4 } );
5 }
6
7 setupBot( "Closure Bot 1", "#bot_1" );
8 setupBot( "Closure Bot 2", "#bot_2" );
No estoy seguro de qué tipo de código escribes, pero regularmente escribo código que es el
responsable de controlar un ejército de drones global lleno de bots de closures, ¡así que
esto es totalmente realista!
(Algunos) bromas aparte, esencialmente siempre y dondequiera que traten las funciones
(que acceden a sus propios ámbitos léxicos respectivos) como valores de primera clase y
los pasan alrededor, es probable que vean las funciones de ejercicio de closures. Sea que
los temporizadores, los manejadores de eventos, las solicitudes de Ajax, la mensajería de
ventanas cruzadas, los workers de la web, o cualquiera de las otras tareas asíncronas (o
síncronas!), Al pasar en una función de devolución de llamada, prepárate para el sling
alrededor de los closures!
Nota: El capítulo 3 introdujo el patrón IIFE. Aunque a menudo se dice que el IIFE (solo) es
un ejemplo de closure observado, estaría algo en desacuerdo, por nuestra definición
anterior.
1 var a = 2;
2
3 (function IIFE(){
4 console.log( a );
5 })();
Este código "funciona", pero no es estrictamente una observación de como funcionan los
closures. ¿Por qué? Debido a que la función (que denominamos "IIFE" aquí) no se ejecuta
fuera de su alcance léxico. Todavía se invoca allí mismo en el mismo ámbito que se
declaró (el ámbito global que también contiene a ). a se encuentra a través de la
búsqueda de alcance léxico normal, no realmente a través del closure.
Si bien el closure podría estar técnicamente ocurriendo a la hora de la declaración, no es
estrictamente observable, y así, como dicen, es un árbol que cae en el bosque sin que nadie
lo oiga.
Ponga este libro abajo en este momento, querido lector. Tengo una tarea para ti. Vaya a
abrir parte de su código JavaScript reciente. Busque sus funciones como valores e
identifique dónde está utilizando el closure y tal vez ni siquiera lo sabía antes.
Esperaré.
¡Ahora lo ves!
5.4 Loops + Closure
El ejemplo canónico más común usado para ilustrar el closure implica el humilde for-loop.
Nota: Los linters a menudo se quejan al poner funciones dentro de bucles, porque los
errores de no entender el closure son muy comunes entre los desarrolladores. Le
explicamos cómo hacerlo correctamente aquí, aprovechando todo el poder de los closures.
Pero esa sutileza se pierde a menudo en los linters y se quejarán de todos modos,
asumiendo que usted no sabe realmente lo que usted está haciendo.
De hecho, si ejecuta este código, obtiene " 6 " impreso 5 veces, en los intervalos de un
segundo.
Huh?
En primer lugar, vamos a explicar de dónde viene 6 . La condición de terminación del bucle
es cuando i no es <= 5 . La primera vez que es el caso es cuando i es 6 . Así pues, la
salida está reflejando el valor final del i después de que el bucle termine.
Esto en realidad parece obvio a primera vista. Las devoluciones de llamada de la función
timer están funcionando bien después de completar el bucle. De hecho, a medida que
Lo que falta es que estamos tratando de implicar que cada iteración del bucle "captura" su
propia copia de i , en el momento de la iteración. Pero, la forma en que funciona el
ámbito, todas las 5 de esas funciones, aunque se definen por separado en cada iteración
del bucle, todas están cerradas sobre el mismo ámbito global compartido, que tiene, de
hecho, sólo un i en él.
Puesto de esa manera, por supuesto todas las funciones comparten una referencia a la
misma i . Algo sobre la estructura del bucle tiende a confundirnos en pensar que hay algo
más sofisticado en el trabajo. No hay. No hay diferencia que si cada uno de los 5 retornos
de tiempo de espera se acaba de declarar uno después de la otra, sin bucle en absoluto.
OK, por lo tanto, de nuevo a nuestra pregunta caliente. ¿Qué falta? Necesitamos más
alcance de closure. Específicamente, necesitamos un nuevo ámbito de closure para cada
iteración del bucle.
Intentemos:
Terminaré el suspenso por ti. Nope. ¿Pero por qué? Ahora obviamente tenemos más
alcance léxico. Cada devolución de llamada de la función de tiempo de espera se está
cerrando realmente sobre su propio alcance de iteración creado respectivamente por cada
IIFE.
No es suficiente tener un alcance para cerrar si ese ámbito está vacío. Mira de cerca.
Nuestro IIFE es sólo un vacío, no hace nada. Necesita algo en él para que sea útil para
nosotros.
Necesita su propia variable, con una copia del valor i en cada iteración.
¡Eureka! ¡Funciona!
Por supuesto, puesto que estos IIFEs son sólo funciones, podemos pasar en i , y
podemos llamarlo j si lo preferimos, o incluso podemos llamarlo i otra vez. De
cualquier manera, el código funciona ahora.
El uso de un IIFE dentro de cada iteración creó un nuevo alcance para cada iteración, lo
que dio a nuestras devoluciones de llamada de función timer la oportunidad de cerrar
sobre un nuevo ámbito para cada iteración, una que tenía una variable con el valor de
iteración adecuado para nosotros acceder.
¡Problema resuelto!
Mire atentamente nuestro análisis de la solución anterior. Utilizamos un IIFE para crear un
nuevo ámbito por iteración. En otras palabras, en realidad necesitábamos un ámbito de
bloque por iteración. El capítulo 3 nos mostró la declaración let , que secuestra un bloque
y declara una variable allí mismo en el bloque.
¡Pero eso no es todo! (En mi mejor voz de Bob Barker). Hay un comportamiento especial
definido para las declaraciones utilizadas en la cabeza de un bucle for . Este
comportamiento indica que la variable se declarará no sólo una vez para el bucle, sino para
cada iteración. Y, por último, se inicializará en cada iteración posterior con el valor desde el
final de la iteración anterior.
1 function foo() {
2 var something = "cool";
3 var another = [1, 2, 3];
4
5 function doSomething() {
6 console.log( something );
7 }
8
9 function doAnother() {
10 console.log( another.join( " ! " ) );
11 }
12 }
Como este código se encuentra en este momento, no hay un closure observable en curso.
Simplemente tenemos algunas variables de datos privadas something y another , y un
par de funciones internas doSomething() y doAnother() , que tienen alcance lexical (y,
por tanto, closure!) Sobre el ámbito interno de foo() .
1 function CoolModule() {
2 var something = "cool";
3 var another = [1, 2, 3];
4
5 function doSomething() {
6 console.log( something );
7 }
8
9 function doAnother() {
10 console.log( another.join( " ! " ) );
11 }
12
13 return {
14 doSomething: doSomething,
15 doAnother: doAnother
16 };
17 }
18
19 var foo = CoolModule();
20
21 foo.doSomething(); // cool
22 foo.doAnother(); // 1 ! 2 ! 3
En primer lugar, CoolModule() es sólo una función, pero tiene que ser invocado para que
haya una instancia de módulo creada. Sin la ejecución de la función externa, la creación
del ámbito interno y los closures no se producirían.
Este valor de retorno del objeto se asigna en última instancia a la variable externa foo , y
luego podemos acceder a los métodos de propiedad en la API, como foo.doSomething() .
Nota: No se requiere que devolvamos un objeto real (literal) de nuestro módulo. Podríamos
regresar una función interna directamente. JQuery es en realidad un buen ejemplo de esto.
Los identificadores jQuery y $ son la API pública para el jQuery "module", pero son,
ellos mismos, sólo una función (que puede tener propiedades, ya que todas las funciones
son objetos).
Las funciones doSomething() y doAnother() tienen closures sobre el ámbito interno del
módulo "instance" (llegado al invocar CoolModule() ). Cuando transportamos esas
funciones fuera del alcance léxico, a través de las referencias de propiedad sobre el objeto
que devolvemos, hemos establecido ahora una condición por la cual el closure puede ser
observado y ejercido.
Para decirlo más sencillamente, hay dos "requisitos" para que el patrón de módulo sea
ejercido:
1. Debe haber una función envolvente externa, y debe invocarse al menos una vez (cada
vez crea una instancia de módulo nuevo).
2. La función de inclusión debe devolver al menos una función interna, por lo que esta
función interna tiene un closure sobre el ámbito privado, y puede acceder y/o modificar
ese estado privado.
nueva instancia de módulo. Una ligera variación en este patrón es cuando sólo te interesa
tener una instancia, un "singleton" de clases:
Los módulos son sólo funciones, por lo que pueden recibir parámetros:
1 function CoolModule(id) {
2 function identify() {
3 console.log( id );
4 }
5
6 return {
7 identify: identify
8 };
9 }
10
11 var foo1 = CoolModule( "foo 1" );
12 var foo2 = CoolModule( "foo 2" );
13
14 foo1.identify(); // "foo 1"
15 foo2.identify(); // "foo 2"
Otra variación ligera pero potente en el patrón de módulo es nombrar el objeto que estás
regresando como tu API pública:
Al conservar una referencia interna al objeto API público dentro de la instancia del módulo,
puede modificar la instancia del módulo desde el interior, incluida la adición y eliminación
de métodos, propiedades y el cambio de sus valores.
Módulos modernos
Los módulos " foo " y " bar " se definen con una función que devuelve una API pública. "
foo " incluso recibe la instancia de " bar " como un parámetro de dependencia, y puede
usarlo en consecuencia.
Pasa algún tiempo examinando estos fragmentos de código para comprender
completamente el poder de los closures puestos a usar para nuestros propios propósitos.
La clave para llevar es que no hay realmente ninguna "magia" particular para los
administradores de módulos. Reúnen ambas características del patrón de módulo I
enumerado anteriormente: invocando un contenedor de definición de función y
manteniendo su valor de retorno como API para ese módulo.
En otras palabras, los módulos son sólo módulos, incluso si usted pone una herramienta
de envoltura amigable encima de ellos.
Módulos Futuros
ES6 agrega soporte de sintaxis de primera clase para el concepto de módulos. Cuando se
carga a través del sistema de módulos, ES6 trata un archivo como un módulo separado.
Cada módulo puede importar otros módulos o miembros específicos de la API, así como
exportar sus propios miembros API públicos.
Nota: Los módulos basados en funciones no son un patrón estáticamente reconocido (algo
que el compilador conoce), por lo que su API semántica no se consideran hasta el
momento de la ejecución. Es decir, puede modificar realmente la API de un módulo durante
el tiempo de ejecución (véase la anterior discusión publicAPI).
Por el contrario, las API del módulo ES6 son estáticas (las API no cambian en tiempo de
ejecución). Dado que el compilador lo sabe, puede (y lo hace!) comprobar durante la
compilación (carga de archivos y) que existe una referencia a un miembro de la API de un
módulo importado. Si no existe la referencia de API, el compilador produce un error
"anticipado" en tiempo de compilación, en lugar de esperar la resolución dinámica en
tiempo de ejecución tradicional (y los errores, si los hay).
Los módulos ES6 no tienen un formato "en línea", sino que deben definirse en archivos
separados (uno por módulo). Los navegadores / motores tienen un "cargador de módulos"
por defecto (que es sobre-escribible, pero eso está bien más allá de nuestra discusión aquí)
que sincronizadamente carga un archivo de módulo cuando se importa.
Considere:
bar.js
1 function hello(who) {
2 return "Let me introduce: " + who;
3 }
4
5 export hello;
foo.js
Nota: Deberían crearse archivos separados " foo.js " y " bar.js ", con el contenido como
se muestra en los dos primeros fragmentos, respectivamente. A continuación, su programa
cargará / importará esos módulos para usarlos, como se muestra en el tercer fragmento.
import importa uno o más miembros de la API de un módulo en el ámbito actual, cada
uno a una variable enlazada ( hello en nuestro caso). module importa una API de
módulo completa a una variable enlazada ( foo , bar en nuestro caso). export exporta
un identificador (variable, función) a la API pública para el módulo actual. Estos operadores
pueden utilizarse tantas veces como sea necesario en la definición de un módulo.
El closure es cuando una función puede recordar y acceder a su alcance léxico incluso
cuando se invoca fuera de su alcance léxico.
Los closure pueden hacernos tropezar, por ejemplo con bucles, si no tenemos cuidado de
reconocerlos y cómo funcionan. Pero también son una herramienta inmensamente
poderosa, permitiendo patrones como módulos en sus diversas formas.
Los módulos requieren dos características clave: 1) invocación de una función de envoltura
externa, para crear el ámbito de inclusión y 2) el valor de retorno de la función de envoltura
debe incluir referencia a al menos una función interna que tiene closure sobre el ámbito
interno privado del envoltorio.
Ahora podemos ver closures alrededor de nuestro código existente, y tenemos la capacidad
de reconocer y aprovecharlos para nuestro propio beneficio!
6- Scope Dinámico
En el capítulo 2, hablamos de "Ámbito dinámico" como un contraste con el modelo "Ámbito
Léxico", que es cómo funciona el ámbito en JavaScript (y de hecho, la mayoría de los otros
lenguajes).
Vamos a examinar brevemente el alcance dinámico, para martillar sobre las diferencias.
Pero, lo que es más importante, el alcance dinámico es realmente un primo cercano a otro
mecanismo ( this ) en JavaScript, que cubrimos en el título "This y los Prototipos de
Objetos" de esta serie de libros.
Como vimos en el capítulo 2, el ámbito léxico es el conjunto de reglas sobre cómo el motor
puede buscar una variable y dónde la encontrará. La característica clave del alcance léxico
es que se define a tiempo de autor, cuando se escribe el código (suponiendo que no lo
engañe con eval() o with ).
El alcance dinámico parece implicar, y por buena razón, que hay un modelo por el cual el
alcance se puede determinar dinámicamente en tiempo de ejecución, en vez de
estaticamente a tiempo de autor. Ése es de hecho el caso. Vamos a ilustrarlo a través del
código:
1 function foo() {
2 console.log( a ); // 2
3 }
4
5 function bar() {
6 var a = 3;
7 foo();
8 }
9
10 var a = 2;
11
12 bar();
El ámbito léxico sostiene que la referencia RHS a un in foo() será resuelta a la variable
global a , lo que dará como resultado el valor 2 .
Por el contrario, el ámbito dinámico no se ocupa de cómo y dónde se declaran las
funciones y los ámbitos, sino de dónde se llaman. En otras palabras, la cadena de alcance
se basa en la pila de llamadas (call-stack), no en la anidación de ámbitos en el código.
1 function foo() {
2 console.log( a ); // 3 (not 2!)
3 }
4
5 function bar() {
6 var a = 3;
7 foo();
8 }
9
10 var a = 2;
11
12 bar();
¿Cómo puede ser posible esto? Debido a que cuando foo() no puede resolver la
referencia de variable para a , en lugar de intensificar la cadena de alcance anidada
(lexical), va hasta la pila de llamadas, para encontrar dónde se llamó foo() . Desde que
foo() fue llamado desde bar() , comprueba las variables en el ámbito de bar() , y
Pero eso es sólo porque probablemente sólo has trabajado en (o al menos profundamente
considerado) código que tiene un alcance léxico. Por lo tanto, el alcance dinámico parece
extraño. Si sólo hubieras escrito código en un lenguaje con un alcance dinámico, parecería
natural, y el ámbito léxico sería la cosa extraña.
Para ser claro, JavaScript no tiene, de hecho, un ámbito dinámico. Tiene alcance léxico.
Llano y simple. Pero el mecanismo de este tipo es como un ámbito dinámico.
Por último: this le importa cómo se llamó una función, lo que demuestra lo
estrechamente relacionado que este mecanismo esta con la idea del alcance dinámico.
Para profundizar más en this , lea el título "this & Object Prototipos".
7- Ámbito de bloque de Polyfilling
En el Capítulo 3, exploramos el Ámbito de Bloque. Vimos que with y la cláusula catch
son ejemplos minúsculos de ámbito de bloque que han existido en JavaScript al menos
desde la introducción de ES3.
Pero, ¿qué pasaría si quisieramos utilizar el ámbito de bloque en los entornos pre-ES6?
1 {
2 let a = 2;
3 console.log( a ); // 2
4 }
5
6 console.log( a ); // ReferenceError
Esto funcionará muy bien en entornos ES6. Pero, ¿podemos hacerlo con las versiones
anteriores de ES6? catch es la respuesta.
1 try{throw 2}catch(a){
2 console.log( a ); // 2
3 }
4
5 console.log( a ); // ReferenceError
Whoa! Ese es un código feo, extraño. Vemos un try / catch que parece arrojar un error
forzosamente, pero el "error" que lanza es sólo un valor 2 , y luego la declaración de
variables que lo recibe está en la cláusula catch(a) . Mente: explota!.
Así es, la cláusula catch tiene un alcance de bloque, lo que significa que puede usarse
como un polyfill para el ámbito de bloque en entornos pre-ES6.
"Pero ...", dices. "... nadie quiere escribir un código así de feo!" Es verdad. Nadie escribe
(algunos) el código generado por el compilador de CoffeeScript, tampoco. Ese no es el
punto.
El punto es que las herramientas pueden transpilar el código ES6 para trabajar en entornos
pre-ES6. Puede escribir código utilizando el ámbito de bloque y beneficiarse de dicha
funcionalidad y dejar que una herramienta de creación de pasos se encargue de producir
código que funcionará de verdad cuando se implementa.
Éste es realmente el camino de migración preferido para todos (ahem, la mayoría) de ES6:
usar un transpilador de código para tomar código ES6 y producir código compatible con
ES5 durante la transición de pre-ES6 a ES6.
7.1 Traceur
Google mantiene un proyecto llamado "Traceur" [^ note-traceur], que tiene la tarea de
transponer las funciones de ES6 en pre-ES6 (en su mayoría ES5, pero no todas!) Para uso
general. El comité TC39 se basa en esta herramienta (y otros) para probar la semántica de
las características que especifican.
1 {
2 try {
3 throw undefined;
4 } catch (a) {
5 a = 2;
6 console.log( a );
7 }
8 }
9
10 console.log( a );
Por lo tanto, con el uso de estas herramientas, podemos empezar a aprovechar el ámbito
del bloque sin importar si estamos apuntando a ES6 o no, porque try / catch ha
estado alrededor (y trabajado de esta manera) desde los días de ES3.
7.2 Bloques implícitos vs. explícitos
En el capítulo 3, hemos identificado algunos peligros potenciales para la capacidad de
mantenimiento / refactorabilidad del código cuando introducimos el ámbito de bloque.
¿Hay otra manera de aprovechar el ámbito de bloque pero para reducir este inconveniente?
Considere esta forma alternativa de let , llamada "let block" o "let statement" (en
contraste con "let declarations" de antes).
1 let (a = 2) {
2 console.log( a ); // 2
3 }
4
5 console.log( a ); // ReferenceError
Como patrón, refleja el enfoque que muchas personas adoptan en el scope de funciones
cuando mueven / elevan manualmente todas sus declaraciones var a la parte superior
de la función. El let-statement los coloca allí en la parte superior del bloque por intención, y
si no usas las declaraciones diseminadas, tus declaraciones de alcance de bloque son algo
más fáciles de identificar y mantener.
Tenemos dos opciones. Podemos formatearlo usando la sintaxis válida de ES6 y un poco
de disciplina en el código:
1 /*let*/ { let a = 2;
2 console.log( a );
3 }
4
5 console.log( a ); // ReferenceError
Pero, las herramientas están destinadas a resolver nuestros problemas. Por lo tanto, la otra
opción es escribir bloques explícitos de instrucciones y dejar que una herramienta los
convierta en código válido de trabajo.
Por lo tanto, he construido una herramienta llamada "let-er" [^ note-let_er] para abordar sólo
este problema. Let-er es un transpilador de código de build-step, pero su única tarea es
encontrar formularios let-statement y transpilarlos. Dejará solo cualquiera del resto de su
código, incluyendo cualquier let-declaraciones. Puede usar let-er como el primer paso del
transpilador ES6 y, a continuación, pasar su código a través de algo como Traceur si es
necesario.
1 {
2 let a = 2;
3 console.log( a );
4 }
5
6 console.log( a ); // ReferenceError
Por lo tanto, puede empezar a utilizar let-er de inmediato y orientar todos los entornos pre-
ES6, y cuando sólo se preocupan por ES6, puede agregar el indicador y orientar
instantáneamente sólo ES6.
En primer lugar, el rendimiento de try / catch es más lento, pero no hay supuesto
razonable de que tiene que ser de esa manera, o incluso que siempre será de esa manera.
Dado que el transportista oficial de ES6 aprobado por TC39 utiliza try / catch , el
equipo de Traceur ha pedido a Chrome que mejore el rendimiento de try / catch , y
obviamente están motivados para hacerlo.
En segundo lugar, IIFE no es una comparación de manzanas a manzanas justa con try /
catch , porque una función envuelta alrededor de cualquier código arbitrario cambia el
La pregunta realmente se convierte en: ¿quieres bloquear el ámbito, o no. Si lo hace, estas
herramientas le proporcionan esa opción. Si no es así, sigue usando var y continúa tu
codificación!
[^note-traceur]:Google Traceur
[^note-let_er]:let-er
8- Lexical-this
Aunque este título no aborda este mecanismo this en detalle, hay un tema ES6 que
relaciona esto con el alcance léxico de una manera importante, que examinaremos
rápidamente.
ES6 agrega una forma sintáctica especial de declaración de función llamada "función de
flecha (arrow function)". Se parece a esto:
La llamada "flecha gorda (fat arrow)" se menciona a menudo como una palabra corta para
la palabra clave de function tediosamente verbosa (sarcasmo).
Pero hay algo mucho más importante en las funciones de flecha que no tiene nada que ver
con el ahorro de pulsaciones de teclado en su declaración.
1 var obj = {
2 id: "awesome",
3 cool: function coolFn() {
4 console.log( this.id );
5 }
6 };
7
8 var id = "not awesome";
9
10 obj.cool(); // awesome
11
12 setTimeout( obj.cool, 100 ); // not awesome
El problema es la pérdida de this binding en la función cool() . Hay varias maneras de
abordar ese problema, pero una solución frecuentemente repetida es var self = this ; .
1 var obj = {
2 count: 0,
3 cool: function coolFn() {
4 var self = this;
5
6 if (self.count < 1) {
7 setTimeout( function timer(){
8 self.count++;
9 console.log( "awesome?" );
10 }, 100 );
11 }
12 }
13 };
14
15 obj.cool(); // awesome?
Sin llegar demasiado a las malas hierbas aquí, la "solución" var self = this
simplemente dispensa todo el problema de la comprensión y el uso adecuado de this
binding, y en su lugar vuelve a algo con lo que estamos quizás más cómodos: ámbito
léxico. El self se convierte en un simple identificador que puede ser resuelto a través del
alcance léxico y el closure, y no se preocupa de lo que le pasó a el binding de this a lo
largo del camino.
A la gente no le gusta escribir cosas verbosas, especialmente cuando lo hacen una y otra
vez. Por lo tanto, una motivación de ES6 es ayudar a aliviar estos escenarios, y, de hecho,
corregir problemas del lenguaje común, como este.
1 var obj = {
2 count: 0,
3 cool: function coolFn() {
4 if (this.count < 1) {
5 setTimeout( () => { // arrow-function ftw?
6 this.count++;
7 console.log( "awesome?" );
8 }, 100 );
9 }
10 }
11 };
12
13 obj.cool(); // awesome?
La breve explicación es que las funciones de flecha no se comportan en absoluto como las
funciones normales cuando se trata de la vinculación de this . Descartan todas las reglas
normales para la vinculación de this , y en cambio asumen el valor de this y el alcance
inmediato de su inclusión léxica, cualquiera que sea.
Por lo tanto, en ese fragmento, la función de flecha no obtiene su this sin consolidarlo de
alguna manera impredecible, simplemente "hereda" el this de la función cool() (que es
correcto si lo invocamos como se muestra!).
Mientras que esto hace para el código más corto, mi perspectiva es que las funciones de la
flecha son realmente apenas las que codifican en la sintaxis del lenguaje un error común
de los reveladores, que es confundir y combinar reglas "que vinculan" con las reglas de
"alcance lexical".
Dicho de otra manera: ¿por qué ir al problema y la verbosidad de usar el paradigma del
estilo this de codificación, sólo para cortar en las rodillas, mezclándolo con referencias
léxicas. Parece natural abrazar un enfoque o el otro para cualquier pieza de código, y no
mezclarlos en el mismo código.
Nota: otra detracción de las funciones de flecha es que son anónimas, no nombradas.
Consulte el Capítulo 3 para ver las razones por las cuales las funciones anónimas son
menos deseables que las funciones con nombre.
1 var obj = {
2 count: 0,
3 cool: function coolFn() {
4 if (this.count < 1) {
5 setTimeout( function timer(){
6 this.count++; // `this` is safe because of `bind(..)`
7 console.log( "more awesome" );
8 }.bind( this ), 100 ); // look, `bind()`!
9 }
10 }
11 };
12
13 obj.cool(); // more awesome
Al leer este libro en preparación para escribir este prólogo, me vi obligado a reflexionar
sobre cómo aprendí JavaScript y cuánto ha cambiado en los últimos 15 años que he
estado programando y desarrollando con él.
Cuando empecé a usar JavaScript hace 15 años, la práctica de usar tecnologías no HTML
como CSS y JS en sus páginas web se llamaba DHTML o HTML dinámico. En aquel
entonces, la utilidad de JavaScript variaba enormemente y parecía estar inclinada hacia la
adición de copos de nieve animados a sus páginas web o relojes dinámicos que decía la
hora en la barra de estado. Basta con decir que realmente no presté mucha atención a
JavaScript en la primera parte de mi carrera debido a la novedad de las implementaciones
que he encontrado a menudo en Internet.
No fue hasta 2005 que redescubrí por primera vez JavaScript como un lenguaje de
programación real al que necesitaba prestar más atención. Después de excavar en la
primera versión beta de Google Maps, me enganchó el potencial que tenía. En ese
momento, Google Maps era una aplicación de primera clase que le permitía mover un
mapa con el ratón, acercar y alejar, y hacer solicitudes de servidor sin recargar la página,
todo ello con JavaScript. ¡Parecía mágico!
Cuando algo parece mágico, suele ser una buena indicación de que estás en el amanecer
de una nueva forma de hacer las cosas. Y muchacho, no me equivoqué - el rápido
crecimiento a hoy, yo diría que Javascript es uno de los lenguajes primarios que utilizo para
la programación del lado del cliente y del servidor, y no lo haría de cualquier otra manera.
Si tuviera esta serie de libros de You Do not Know JS al comienzo de mi carrera, mi historia
se vería muy diferente de lo que es hoy. Y esa es una de las cosas que me encanta acerca
de esta serie: explica JS a un nivel que construye su comprensión a medida que avanza la
serie, pero de una manera divertida e informativa.
this & Object Prototypes es una continuación maravillosa a la serie. Hace un trabajo grande
y natural de construir en el libro anterior, scope y closures, y extender ese conocimiento a
una parte muy importante del lenguaje de JS, la palabra clave this y prototipos. Estas
dos cosas simples son fundamentales para lo que aprenderás en los libros futuros, porque
son fundamentales para hacer una programación real con JavaScript. El concepto de
cómo crear objetos, relacionarlos y ampliarlos para representar cosas en su aplicación es
necesario para crear aplicaciones grandes y complejas en JavaScript. Y sin ellos, la
creación de aplicaciones complejas (como Google Maps) no sería posible en JavaScript.
Yo diría que la gran mayoría de los desarrolladores web probablemente nunca han
construido un objeto JavaScript y sólo tratan el lenguaje como pegamento vinculante de
eventos entre los botones y las solicitudes AJAX. Yo estaba en ese campo en un momento
de mi carrera, pero después de aprender a dominar prototipos y crear objetos en JavaScript,
un mundo de posibilidades se abría para mí. Si usted cae en la categoría de sólo la
creación de eventos vinculante y código de pegamento, este libro es una lectura obligada;
Si sólo necesita un refresco, este libro será un recurso para usted. De cualquier manera, no
te decepcionará. ¡Créeme!
Nick Berardi
Nickberardi.com, @nberardi
0- Prefacio
Estoy seguro de que se dio cuenta, pero "JS" en el título de la serie de libros no es una
abreviatura de palabras utilizadas para maldecir sobre JavaScript, aunque maldecir a las
peculiaridades del lenguaje es algo con lo que probablemente todos se pueden identificar.
Desde los primeros días de la web, JavaScript ha sido una tecnología fundamental que ha
impulsado la experiencia interactiva en torno al contenido que consumimos. Mientras que
el apuntador parpadeante y los molestos avisos emergentes podrían ser donde comenzó
JavaScript, casi dos décadas después, la tecnología y la capacidad de JavaScript ha
crecido en muchos ámbitos de magnitud, y pocos dudan de su importancia en el corazón
de la plataforma de software más ampliamente disponible: La web.
Pero como lenguaje, ha sido perpetuamente un blanco para mucha crítica, debido en parte
a su "herencia", pero más aún debido a su filosofía de diseño. Incluso el nombre evoca,
como Brendan Eich una vez lo dijo, "el hermano bebe estúpido" comparado junto a su
hermano mayor más maduro "Java". Pero el nombre es simplemente un accidente de la
política y el marketing. Los dos idiomas son muy diferentes en muchos aspectos
importantes. "JavaScript" está relacionado con "Java" como "Carnival" es a "Car".
Mientras que JavaScript es quizás uno de los idiomas más fáciles de poner en
funcionamiento, sus excentricidades hacen que el dominio sólido del lenguaje sea una
ocurrencia mucho menos común que en muchos otros idiomas. Cuando se necesita un
conocimiento bastante profundo de un lenguaje como C o C++ para escribir un programa a
gran escala, la producción a gran escala de JavaScript puede, y con frecuencia lo es,
apenas se raya con la superficie de lo que el lenguaje puede realmente hacer.
Es simultáneamente un lenguaje sencillo y fácil de usar que tiene un amplio atractivo y una
colección compleja y matizada de mecánica del lenguaje que sin un estudio cuidadoso
escapará a la verdadera comprensión, incluso de los más experimentados desarrolladores
de JavaScript.
Misión
Si bien este subconjunto ha sido conocido como "The Good Parts", le pediría a usted,
querido lector, que lo considere "The Easy Parts", "The Safe Parts" o incluso "The
Incomplete Parts".
No estoy contento, ni debes estar, en parar una vez que algo funciona correctamente, y no
saber realmente por qué. Le desafío a viajar por ese "camino costoso" y abrazar todo lo que
JavaScript es y puede hacer. Con ese conocimiento, ninguna técnica, ningún framework,
ningún acrónimo popular de moda, estará más allá de su comprensión.
Estos libros toman partes específicas del lenguaje que son las comúnmente mal
entendidas o no comprendidas, y se sumerge muy profundamente y exhaustivamente en
ellas. Usted debe termina la lectura con una firme confianza en su comprensión, no sólo de
lo teórico, sino lo práctico "lo que necesita saber".
El JavaScript que usted conoce ahora mismo es probablemente la partes dada a usted por
otros que han sido "quemados" por una comprensión incompleta. Ese JavaScript es sólo
una sombra del verdadero lenguaje. Realmente no sabes JavaScript, pero si crees en esta
serie, lo harás. Sigue leyendo, amigos. JavaScript te espera.
Resumen
Nota: Muchos de los ejemplos en este libro asumen los entornos de motor de JavaScript
modernos (y futuros), como ES6. Es posible que algunos códigos no funcionen como se
describe si se ejecutan en motores anteriores (pre-ES6).
1- this o That?
Uno de los mecanismos más confusos en JavaScript es la palabra clave this . Es una
palabra clave de identificador especial que se define automáticamente en el ámbito de
cada función, pero a lo que se refiere exactamente podría molestar incluso a los
desarrolladores de JavaScript más experimentados.
Nota: La palabra " this " es un pronombre terriblemente común en el discurso general. Por
lo tanto, puede ser muy difícil, especialmente verbalmente, determinar si estamos
utilizando "este" como un pronombre o usarlo para referirnos al identificador de palabra
clave real. Para mayor claridad, utilizaré siempre this para referirme a la palabra clave
especial, y "this" o this o de otra manera.
1.1 ¿Porque this?
Si el mecanismo this es tan confuso, incluso para los desarrolladores de JavaScript
experimentados, uno puede preguntarse por qué es incluso útil? ¿Es más problemático de
lo que vale? Antes de saltar en el cómo, debemos examinar el por qué.
1 function identify() {
2 return this.name.toUpperCase();
3 }
4
5 function speak() {
6 var greeting = "Hello, I'm " + identify.call( this );
7 console.log( greeting );
8 }
9
10 var me = {
11 name: "Kyle"
12 };
13
14 var you = {
15 name: "Reader"
16 };
17
18 identify.call( me ); // KYLE
19 identify.call( you ); // READER
20
21 speak.call( me ); // Hello, I'm KYLE
22 speak.call( you ); // Hello, I'm READER
Este fragmento de código permite que las funciones identifier() y speak() sean
reutilizadas en contextos múltiples ( me y you ), en lugar de necesitar una versión
separada de la función para cada objeto.
En lugar de confiar en this , usted podría haberlo pasado explícitamente en un objeto de
contexto tanto para identifier() como para speak() .
1 function identify(context) {
2 return context.name.toUpperCase();
3 }
4
5 function speak(context) {
6 var greeting = "Hello, I'm " + identify( context );
7 console.log( greeting );
8 }
9
10 var me = {
11 name: "Kyle"
12 };
13
14 var you = {
15 name: "Reader"
16 };
17
18 identify( you ); // READER
19 speak( me ); // Hello, I'm KYLE
Sin embargo, el mecanismo this proporciona una forma más elegante de "pasar"
implícitamente una referencia de objeto, lo que conduce a un diseño de API más limpio y
una reutilización más fácil.
Cuanto más complejo es su patrón de uso, más claramente verá que pasar el contexto
alrededor como un parámetro explícito es a menudo más desordenado que pasar alrededor
del contexto de this . Cuando exploramos objetos y prototipos, veremos la utilidad de una
colección de funciones que pueden referenciar automáticamente al objeto de contexto
apropiado.
1.2 Confusiones
Pronto comenzaremos a explicar cómo this funciona realmente, pero primero debemos
disipar algunos conceptos erróneos sobre cómo realmente NO funciona.
El nombre "this" crea confusión cuando los desarrolladores tratan de pensarlo demasiado
literalmente. Hay dos significados a menudo asumidos, pero ambos son incorrectos.
Itself
La primera tentación común es asumir que this se refiere a la función misma. Esa es
una inferencia gramatical razonable, por lo menos.
¿Por qué quieres referirte a una función desde dentro de sí mismo? Las razones más
comunes serían cosas como la recursión (llamando a una función desde dentro de sí
misma) o tener un manejador de eventos que pueda desvincularse cuando se llama por
primera vez.
Pero por un momento exploraremos ese patrón, para ilustrar cómo this no permite que
una función obtenga una referencia a sí misma tal como podríamos haber asumido.
Considere el siguiente código, donde intentamos rastrear cuántas veces se llamó una
función ( foo ):
1 function foo(num) {
2 console.log( "foo: " + num );
3
4 // keep track of how many times `foo` is called
5 this.count++;
6 }
7
8 foo.count = 0;
9
10 var i;
11
12 for (i=0; i<10; i++) {
13 if (i > 5) {
14 foo( i );
15 }
16 }
17 // foo: 6
18 // foo: 7
19 // foo: 8
20 // foo: 9
21
22 // how many times was `foo` called?
23 console.log( foo.count ); // 0 -- WTF?
foo.count sigue siendo 0 , aunque las cuatro sentencias console.log indican
Cuando el código ejecuta foo.count = 0 , de hecho agrega una propiedad count al
objeto de función foo . Sin embargo, para la referencia this.count dentro de la función,
this no apunta en absoluto a ese objeto de función, y así, aunque los nombres de
propiedad son los mismos, los objetos raíz son diferentes y se produce confusión.
En lugar de detenerse en este punto y cavar en por qué la referencia this no parece estar
comportándose como se esperaba, y responder a esas preguntas difíciles pero importantes,
muchos desarrolladores simplemente evitan el problema en conjunto, y hackean hacia otra
solución, como la creación de otro Objeto para contener la propiedad count :
1 function foo(num) {
2 console.log( "foo: " + num );
3
4 // keep track of how many times `foo` is called
5 data.count++;
6 }
7
8 var data = {
9 count: 0
10 };
11
12 var i;
13
14 for (i=0; i<10; i++) {
15 if (i > 5) {
16 foo( i );
17 }
18 }
19 // foo: 6
20 // foo: 7
21 // foo: 8
22 // foo: 9
23
24 // how many times was `foo` called?
25 console.log( data.count ); // 4
1 function foo() {
2 foo.count = 4; // `foo` refers to itself
3 }
4
5 setTimeout( function(){
6 // anonymous function (no name), cannot
7 // refer to itself
8 }, 10 );
En la primera función, llamada "función nombrada", foo es una referencia que se puede
utilizar para referirse a la función desde dentro de sí misma.
Nota: La referencia a la vieja escuela, pero ahora desaprobada y con el ceño fruncido
arguments.callee . La referencia dentro de una función también apunta al objeto de
Por lo tanto, otra solución a nuestro ejemplo de ejecución habría sido usar el identificador
foo como una referencia de objeto de función en cada lugar, y no usar this en
1 function foo(num) {
2 console.log( "foo: " + num );
3
4 // keep track of how many times `foo` is called
5 foo.count++;
6 }
7
8 foo.count = 0;
9
10 var i;
11
12 for (i=0; i<10; i++) {
13 if (i > 5) {
14 foo( i );
15 }
16 }
17 // foo: 6
18 // foo: 7
19 // foo: 8
20 // foo: 9
21
22 // how many times was `foo` called?
23 console.log( foo.count ); // 4
Sin embargo, ese enfoque de manera similar a los pasos laterales de la comprensión real
de this y se basa enteramente en el ámbito léxico de la variable foo .
Sin embargo, otra forma de abordar el problema es forzar this a apuntar realmente al
objeto de función foo :
1 function foo(num) {
2 console.log( "foo: " + num );
3
4 // keep track of how many times `foo` is called
5 // Note: `this` IS actually `foo` now, based on
6 // how `foo` is called (see below)
7 this.count++;
8 }
9
10 foo.count = 0;
11
12 var i;
13
14 for (i=0; i<10; i++) {
15 if (i > 5) {
16 // using `call(..)`, we ensure the `this`
17 // points at the function object (`foo`) itself
18 foo.call( foo, i );
19 }
20 }
21 // foo: 6
22 // foo: 7
23 // foo: 8
24 // foo: 9
25
26 // how many times was `foo` called?
27 console.log( foo.count ); // 4
Su alcance
El siguiente error más común sobre el significado de this es que de alguna manera se
refiere al alcance de la función. Es una pregunta difícil, porque en cierto sentido hay algo de
verdad, pero en el otro sentido, es bastante equivocado.
Para ser claro, this no se refiere, en modo alguno, al ámbito léxico de una función. Es
cierto que internamente, el alcance es como un objeto con propiedades para cada uno de
los identificadores disponibles. Pero el "objeto" de alcance no es accesible al código
JavaScript. Es una parte interna de la implementación del Motor.
Considere el código que intenta (y falla!) cruzar el límite y usa this para referirse
implícitamente al ámbito léxico de una función:
1 function foo() {
2 var a = 2;
3 this.bar();
4 }
5
6 function bar() {
7 console.log( this.a );
8 }
9
10 foo(); //undefined
Hay más de un error en este fragmento. Aunque puede parecer artificial, el código que ves
es una destilación del código real del mundo real que se ha intercambiado en los foros
públicos de ayuda de la comunidad. Es una ilustración maravillosa (si no triste) de lo
equivocados que pueden ser estos supuestos sobre this .
Sin embargo, el desarrollador que escribe este código intenta usar this para crear un
puente entre los ámbitos léxicos de foo() y bar() , de modo que bar() tenga acceso a
la variable a en el ámbito interno de foo() . Ningún puente es posible. No puedes usar
esta referencia para buscar algo en un ámbito léxico. No es posible.
Cada vez que se sientan tratando de mezclar perspectivas de alcance léxico con this ,
recuerde: no hay puente.
1.3 ¿Que es this?
Después de apartar varias suposiciones incorrectas, volvamos ahora nuestra atención a
cómo funciona realmente el mecanismo this .
Hemos dicho anteriormente que this no es una vinculación autor-tiempo sino un enlace
de tiempo de ejecución. Es contextual basado en las condiciones de la invocación de la
función. El alcance this no tiene nada que ver con el lugar donde se declara una función,
sino que tiene en cambio que ver con la forma en que se llama a la función.
Cuando se invoca una función, se crea un registro de activación, también conocido como
contexto de ejecución. Este registro contiene información sobre dónde se llamó la función
(la pila de llamadas), cómo se invocó la función, qué parámetros se pasaron, etc. Una de
las propiedades de este registro es la referencia de this que se utilizará durante la
duración de la ejecución de esa función.
Para aprender this , primero tiene que aprender lo que no es this , a pesar de cualquier
suposición o concepto erróneo que pueden llevarlo por esos caminos. this no es ni una
referencia a la función en sí, ni una referencia al ámbito léxico de la función.
this es en realidad un enlace que se realiza cuando se invoca una función y lo que hace
Encontrar el sitio de llamada se trata generalmente: "de ir a localizar desde donde se llama
una función", pero no siempre es tan fácil, ya que ciertos patrones de codificación pueden
oscurecer el verdadero sitio de llamada.
Lo importante es pensar en la pila de llamadas (la pila de funciones que se han llamado
para llegar al momento actual en ejecución). El sitio de llamada que nos interesa es en la
invocación antes de la función en ejecución.
1 function baz() {
2 // call-stack es: `baz`
3 // así, nuestro sitio de llamada está en el ámbito global
4
5 console.log( "baz" );
6 bar(); // <-- call-site para `bar`
7 }
8
9 function bar() {
10 // call-stack es: `baz` -> `bar`
11 // así, nuestro sitio de llamada está en `baz`
12
13 console.log( "bar" );
14 foo(); // <-- call-site para `foo`
15 }
16
17 function foo() {
18 // call-stack es: `baz` -> `bar` -> `foo`
19 // asi, nuestro sitio de llamada esta en `bar`
20
21 console.log( "foo" );
22 }
23
24 baz(); // <-- call-site para `baz`
Tenga cuidado al analizar el código para encontrar el sitio de llamada real (desde la pila de
llamadas), porque es lo único que importa para el enlace de this .
Nota: Puede visualizar una pila de llamadas en su mente mirando la cadena de llamadas
de función en orden, como hicimos con los comentarios en el fragmento anterior. Pero esto
es laborioso y propenso a errores. Otra forma de ver la pila de llamadas es usar una
herramienta de depuración en su navegador. La mayoría de los navegadores de escritorio
modernos incorporan herramientas de desarrollo, que incluyen un depurador JS. En el
fragmento anterior, podría haber establecido un punto de interrupción en las herramientas
para la primera línea de la función foo() o simplemente insertado el debugger ; en la
declaración de esa primera línea. Al ejecutar la página, el depurador se detendrá en esta
ubicación y le mostrará una lista de las funciones que se han llamado para llegar a esa
línea, que será la pila de llamadas. Por lo tanto, si está intentando diagnosticar la
vinculación de this , utilice las herramientas de desarrollador para obtener la pila de
llamadas y, a continuación, busque el segundo elemento desde la parte superior y se
mostrará el sitio de llamada real.
2.2 Nada más que reglas
Volvamos nuestra atención ahora a cómo el sitio de llamada determina dónde apuntará a
this durante la ejecución de una función.
Debe inspeccionar el sitio de llamada y determinar cuál de las 4 reglas se aplica. Primero
explicaremos cada una de estas 4 reglas de forma independiente, y luego ilustraremos su
orden de precedencia, si se pudieran aplicar múltiples reglas al sitio de llamada.
La primera regla que examinaremos viene del caso más común de las llamadas de
función: invocación de función autónoma. Piense en esta regla this como la regla de
captura por defecto cuando no se aplica ninguna otra regla.
1 function foo() {
2 console.log( this.a );
3 }
4
5 var a = 2;
6
7 foo(); // 2
Lo primero que hay que tener en cuenta es que si las variables declaradas en el ámbito
global, como var a = 2 , son sinónimas con propiedades de objeto global del mismo
nombre. No son copias el uno del otro, son el uno al otro. Piense en ello como dos caras de
la misma moneda.
En segundo lugar, vemos que cuando foo() se llama, this.a se resuelve a nuestra
variable global a . ¿Por qué? Porque en este caso, el enlace por defecto para this se
aplica a la llamada de la función, y así apunta this al objeto global.
¿Cómo sabemos que la regla de vinculación predeterminada se aplica aquí? Examinamos
el sitio de llamada para ver cómo se llama foo() . En nuestro fragmento, foo() se llama
con una referencia de función simple, no decorada. Ninguna de las otras reglas que
demostraremos se aplicará aquí, por lo que se aplica la vinculación por defecto.
1 function foo() {
2 "use strict";
3
4 console.log( this.a );
5 }
6
7 var a = 2;
8
9 foo(); // TypeError: `this` is `undefined`
Un detalle sutil pero importante es: aunque el conjunto de reglas de vinculación de this
se basan totalmente en el call-site, el objeto global sólo es elegible para el enlace por
defecto si el contenido de foo() no se ejecuta en modo estricto; El estado de modo
estricto del sitio de llamada de foo() es irrelevante.
1 function foo() {
2 console.log( this.a );
3 }
4
5 var a = 2;
6
7 (function(){
8 "use strict";
9
10 foo(); // 2
11 })();
Vinculación implícita
Otra regla a considerar es: el sitio de llamada tiene un objeto de contexto, también
conocido como un objeto propietario o que contiene, aunque estos términos alternativos
podrían ser un poco engañosos.
Considere:
1 function foo() {
2 console.log( this.a );
3 }
4
5 var obj = {
6 a: 2,
7 foo: foo
8 };
9
10 obj.foo(); // 2
En primer lugar, observe la manera en que foo() se declara y luego se agrega como una
propiedad de referencia en obj . Independientemente de si foo() se declara inicialmente
en obj , o se agrega como una referencia posterior (como muestra este fragmento), en
ninguno de los casos la función es realmente una "propiedad" o está "contenida" por el
objeto obj .
Sin embargo, el sitio de llamada utiliza el contexto obj para referenciar la función, por lo
que podría decirse que el objeto obj "posee" o "contiene" la referencia de función en el
momento en que se llama a la función.
Lo que usted decida para llamar a este patrón, en el punto que foo() se llama, es
precedido por una referencia de objeto a obj . Cuando hay un objeto de contexto para una
referencia de función, la regla de vinculación implícita dice que es el objeto que se debe
utilizar para la llamada de función de la vinculación de this .
Debido a que obj es lo mismo que this para la llamada foo() , this.a es sinónimo
de obj.a .
1 function foo() {
2 console.log( this.a );
3 }
4
5 var obj2 = {
6 a: 42,
7 foo: foo
8 };
9
10 var obj1 = {
11 a: 2,
12 obj2: obj2
13 };
14
15 obj1.obj2.foo(); // 42
Implícitamente perdido
Una de las frustraciones más comunes que crea la vinculación de this es cuando una
función implícitamente vinculada pierde esa vinculación, lo que generalmente significa que
se vuelve a la vinculación por defecto, ya sea del objeto global o undefined , dependiendo
del strict mode .
Considere:
1 function foo() {
2 console.log( this.a );
3 }
4
5 var obj = {
6 a: 2,
7 foo: foo
8 };
9
10 var bar = obj.foo; // function reference/alias!
11
12 var a = "oops, global"; // `a` also property on global object
13
14 bar(); // "oops, global"
A pesar de que bar parece ser una referencia a obj.foo , de hecho, es realmente sólo
otra referencia a la propia foo . Además, el call-site es lo que importa, y el call-site es
bar() , que es una llamada simple, no decorada y por lo tanto se aplica el enlace por
defecto.
La forma más sutil, más común y más inesperada en que esto ocurre es cuando
consideramos pasar una función de devolución de llamada:
1 function foo() {
2 console.log( this.a );
3 }
4
5 function doFoo(fn) {
6 // `fn` is just another reference to `foo`
7
8 fn(); // <-- call-site!
9 }
10
11 var obj = {
12 a: 2,
13 foo: foo
14 };
15
16 var a = "oops, global"; // `a` also property on global object
17
18 doFoo( obj.foo ); // "oops, global"
1 function foo() {
2 console.log( this.a );
3 }
4
5 var obj = {
6 a: 2,
7 foo: foo
8 };
9
10 var a = "oops, global"; // `a` also property on global object
11
12 setTimeout( obj.foo, 100 ); // "oops, global"
1 function setTimeout(fn,delay) {
2 // wait (somehow) for `delay` milliseconds
3 fn(); // <-- call-site!
4 }
populares son muy aficionados a forzar su devolución de llamada a tener un this al que
apunta, por ejemplo, al elemento DOM que activó el evento. Mientras que a veces puede ser
útil, otras veces puede ser francamente exasperante. Lamentablemente, estas herramientas
rara vez le permiten elegir.
Vinculación explícita
Con la vinculación implícita como acabamos de ver, tuvimos que mutar el objeto en
cuestión para incluir una referencia sobre sí misma a la función, y usar esta referencia de
función de propiedad para indirectamente (implícitamente) vincular this con el objeto.
¿Pero, qué pasa si usted quiere forzar una llamada de la función para utilizar un objeto
particular para este enlace, sin poner una referencia de la función de la característica en el
objeto?
Todas las funciones del lenguaje tienen algunas utilidades a su disposición (a través de su
[[Prototype]]) - más sobre esto más adelante) que pueden ser útiles para esta tarea.
Específicamente, las funciones tienen métodos de call(..) y de apply(..) .
Técnicamente, los ambientes de host JavaScript a veces proporcionan funciones que son
lo suficientemente especiales (una forma amable de decirlo!) Que no tienen dicha
funcionalidad. Pero son pocos. La gran mayoría de las funciones proporcionadas, y
ciertamente todas las funciones que va a crear, tienen acceso a call(..) y apply(..) .
¿Cómo funcionan estas utilidades? Ambos toman, como su primer parámetro, un objeto
para usar para this , y luego invocan la función con el this especificado. Puesto que
usted está indicando directamente lo que usted quiere que this sea, lo llamamos enlace
explícito.
Considere:
1 function foo() {
2 console.log( this.a );
3 }
4
5 var obj = {
6 a: 2
7 };
8
9 foo.call( obj ); // 2
Invocando foo con una vinculación explícita por foo.call(..) nos permite forzar a su
this a ser obj .
Si pasa un valor primitivo simple (de tipo string , boolean o number ) como este
enlace, el valor primitivo se envuelve en su objeto-forma ( new String(..) ,
new Boolean(..) o new Number(..) , respectivamente). Esto se refiere a menudo como
"boxing".
1 function foo() {
2 console.log( this.a );
3 }
4
5 var obj = {
6 a: 2
7 };
8
9 var bar = function() {
10 foo.call( obj );
11 };
12
13 bar(); // 2
14 setTimeout( bar, 100 ); // 2
15
16 // `bar` hard binds `foo`'s `this` to `obj`
17 // so that it cannot be overriden
18 bar.call( window ); // 2
Veamos cómo funciona esta variación. Creamos una función bar() que, internamente,
llama manualmente a foo.call(obj) , invocando forzosamente foo con la vinculación
obj para this . No importa cómo invoque más tarde la función bar() , siempre se
invocará manualmente foo con obj . Esta vinculación es tanto explícita como fuerte
(hard), por lo que lo llamamos vinculación dura (hard binding).
La forma más típica de envolver una función con una vinculación dura crea un paso a
través de cualquier argumento pasado y cualquier valor de retorno recibido:
1 function foo(something) {
2 console.log( this.a, something );
3 return this.a + something;
4 }
5
6 var obj = {
7 a: 2
8 };
9
10 var bar = function() {
11 return foo.apply( obj, arguments );
12 };
13
14 var b = bar( 3 ); // 2 3
15 console.log( b ); // 5
1 function foo(something) {
2 console.log( this.a, something );
3 return this.a + something;
4 }
5
6 // simple `bind` helper
7 function bind(fn, obj) {
8 return function() {
9 return fn.apply( obj, arguments );
10 };
11 }
12
13 var obj = {
14 a: 2
15 };
16
17 var bar = bind( foo, obj );
18
19 var b = bar( 3 ); // 2 3
20 console.log( b ); // 5
Dado que la vinculación dura es un patrón tan común, se proporciona con una utilidad
integrada como ES5: Function.prototype.bind , y se utiliza de esta manera:
1 unction foo(something) {
2 console.log( this.a, something );
3 return this.a + something;
4 }
5
6 var obj = {
7 a: 2
8 };
9
10 var bar = foo.bind( obj );
11
12 var b = bar( 3 ); // 2 3
13 console.log( b ); // 5
bind(..) devuelve una nueva función que está codificada para llamar a la función
Nota: A partir de ES6, la función hard-bound producida por bind(..) tiene una propiedad
.name que deriva de la función de destino original. Por ejemplo: bar = foo.bind(..)
debe tener un valor bar.name de " bound foo ", que es el nombre de la llamada a la
función que debe aparecer en un stack trace.
Por ejemplo:
1 function foo(el) {
2 console.log( el, this.id );
3 }
4
5 var obj = {
6 id: "awesome"
7 };
8
9 // use `obj` como `this` para la llamada `foo(..)`
10 [1, 2, 3].forEach( foo, obj ); // 1 awesome 2 awesome 3 awesome
Internamente, estas diversas funciones casi seguramente usan una vinculación explícito
vía call(..) o apply(..) , ahorrándole el problema.
new Binding
La cuarta y última regla para la vinculación this nos obliga a repensar un concepto
erróneo muy común sobre las funciones y los objetos en JavaScript.
En primer lugar, vamos a volver a definir lo que es un "constructor" en JavaScript. En JS, los
constructores son sólo funciones que pasan a ser llamadas con el operador new delante
de ellas. No están unidos a las clases, ni están instanciando una clase. Ni siquiera son
tipos especiales de funciones. Son sólo funciones regulares que, en esencia, son
secuestradas por el uso de new en su invocación.
Por ejemplo, la función Number(..) que actúa como constructor, citando desde la
especificación ES5.1:
Por lo tanto, casi cualquier función, incluyendo las funciones de objetos integradas como
Number(..) (véase el capítulo 3) se puede llamar con new delante de ella, y hace que la
función llamada sea una llamada al constructor. Esta es una distinción importante pero
sutil: realmente no hay tal cosa como "funciones constructoras", sino más bien llamadas
de construcción de funciones.
Cuando se invoca una función con new delante de ella, también conocida como llamada
del constructor, se ejecutan automáticamente las siguientes acciones:
1 function foo(a) {
2 this.a = a;
3 }
4
5 var bar = new foo( 2 );
6 console.log( bar.a ); // 2
Al llamar a foo(...) con un new delante de él, hemos construido un nuevo objeto y
establecemos ese nuevo objeto como el de la llamada de foo(..) . Por tanto new es la
manera final en que una llamada a la función this puede ser vinculada. Llamaremos a
esto nueva vinculación (new banding).
2.3 Todo en orden
Por lo tanto, ahora hemos descubierto las 4 reglas para vincular this en las llamadas de
función. Todo lo que necesita hacer es encontrar el sitio de llamada e inspeccionarlo para
ver qué regla se aplica. Pero, ¿qué pasa si el sitio de llamada tiene múltiples reglas
elegibles? Debe haber un orden de precedencia a estas reglas, por lo que demostraremos a
continuación cuál es el orden para aplicar las reglas.
Debe quedar claro que la vinculación por defecto es la regla de prioridad más baja de las 4.
Por lo tanto, vamos a dejarla de lado.
1 function foo() {
2 console.log( this.a );
3 }
4
5 var obj1 = {
6 a: 2,
7 foo: foo
8 };
9
10 var obj2 = {
11 a: 3,
12 foo: foo
13 };
14
15 obj1.foo(); // 2
16 obj2.foo(); // 3
17
18 obj1.foo.call( obj2 ); // 3
19 obj2.foo.call( obj1 ); // 2
Por lo tanto, la vinculación explicita tiene prioridad sobre la vinculacion implicita, lo que
significa que debe preguntar primero si se aplica la vinculación explicita antes de
comprobar la vinculación implícita.
OK, la nueva vinculación es más precedente que la vinculación implícita. Pero ¿crees que la
nueva vinculación es más o menos precedente que la vinculación explícita?
Nota: new y call / apply no se pueden usar juntos, por lo que no se permite
new foo.call(obj1) , para probar el nuevo vinculo directamente contra el vinculo
explícito. Pero todavía podemos usar una vinculación dura (hard binding) para probar la
precedencia de las dos reglas.
Por ese razonamiento, parecería obvio asumir que la vinculación dura (que es una forma de
vinculación explícita) es más precedente que una nueva vinculación, y por lo tanto no
puede ser reemplazada por una nueva.
Vamos a revisar:
1 function foo(something) {
2 this.a = something;
3 }
4
5 var obj1 = {};
6
7 var bar = foo.bind( obj1 );
8 bar( 2 );
9 console.log( obj1.a ); // 2
10
11 var baz = new bar( 3 );
12 console.log( obj1.a ); // 2
13 console.log( baz.a ); // 3
Whoa! bar está limitado contra obj1 , pero new bar(3) no cambió obj1.a para ser
3 como habríamos esperado. En su lugar, la llamada a bar(..) con límite rígido (a
obj1 ) puede ser reemplazada por new . Desde que se aplicó new , obtuvimos el objeto
recién creado, que denominamos baz , y vemos de hecho que baz.a tiene el valor 3 .
Si razonas sobre cómo funciona el código del ayudante, no tiene una forma para que una
llamada new de operador anule la vinculación de hard-obj como acabamos de observar.
1 if (!Function.prototype.bind) {
2 Function.prototype.bind = function(oThis) {
3 if (typeof this !== "function") {
4 // closest thing possible to the ECMAScript 5
5 // internal IsCallable function
6 throw new TypeError( "Function.prototype.bind - what " +
7 "is trying to be bound is not callable"
8 );
9 }
10
11 var aArgs = Array.prototype.slice.call( arguments, 1 ),
12 fToBind = this,
13 fNOP = function(){},
14 fBound = function(){
15 return fToBind.apply(
16 (
17 this instanceof fNOP &&
18 oThis ? this : oThis
19 ),
20 aArgs.concat( Array.prototype.slice.call( arguments )
21 );
22 }
23 ;
24
25 fNOP.prototype = this.prototype;
26 fBound.prototype = new fNOP();
27
28 return fBound;
29 };
30 }
Nota: El bind() polyfill mostrado anteriormente difiere del built-in bind(..) en ES5 con
respecto a las funciones enlazadas que se usarán con new (vea más adelante por qué es
útil). Debido a que el polyfill no puede crear una función sin un .prototype como lo hace
la utilidad incorporada, hay alguna indirección matizada para aproximar el mismo
comportamiento. Pise con cuidado si va a utilizar el new con una función de hard-bound y
confía en este polyfill.