7 consejos para manejar undefined en JavaScript
La mayoría de los lenguajes modernos como Ruby, Python o Java tienen un solo valor nulo (nil
o null
), que parece un enfoque razonable.
Pero JavaScript es diferente.
null
, pero también undefined
, representan en JavaScript valores vacíos. Entonces, ¿cuál es la diferencia exacta entre ellos?
La respuesta corta es que el intérprete de JavaScript devuelve undefined
cuando accede a una variable o propiedad de objeto que aún no está inicializada. Por ejemplo:
En el otro lado, null
representa un objeto faltante referencia. JavaScript no inicializa variables ni propiedades de objeto con null
.
Algunos métodos nativos como String.prototype.match()
pueden devolver null
para indicar un objeto faltante. Eche un vistazo al ejemplo:
Debido a que JavaScript es permisivo, los desarrolladores tienen la tentación de acceder a valores no inicializados. Yo también soy culpable de tan mala práctica.
A menudo, estas acciones riesgosas generan undefined
errores relacionados:
-
TypeError: "undefined" is not a function
-
TypeError: Cannot read property "<prop-name>" of undefined
- y errores de tipo similares.
El desarrollador de JavaScript puede entender la ironía de este chiste :
Para reducir tales errores, debe comprender los casos en los que undefined
se genera. Exploremos undefined
y su efecto en la seguridad del código.
1. Lo que no está definido
JavaScript tiene 6 tipos primitivos:
Y un tipo de objeto separado: {name: "Dmitri"}
, .
De 6 tipos primitivos undefined
hay un valor especial con su propio tipo Indefinido. De acuerdo con la especificación ECMAScript:
El valor primitivo de valor indefinido se usa cuando a una variable no se le ha asignado un valor.
El estándar define claramente que recibirá undefined
cuando acceda a variables no inicializadas, propiedades de objeto no existentes, elementos de matriz no existentes y similares.
Algunos ejemplos:
El ejemplo anterior demuestra que acceder a:
- una variable no inicializada
number
- una propiedad de objeto no existente
movie.year
- o un elemento de matriz no existente
movies
se evalúan como undefined
.
La especificación ECMAScript define el tipo de undefined
valor:
El tipo no definido es un tipo cuyo único valor es el
undefined
valor.
En este sentido, devuelve "undefined"
cadena para un valor undefined
:
Por supuesto, typeof
funciona muy bien para verificar si una variable contiene un valor undefined
:
2. Escenarios que crean indefinidos
2.1 Variable no inicializada
Una variable declarada pero aún no asignada con un valor (sin inicializar) es por defecto
undefined
.
Simple y llanamente:
myVariable
está declarado y aún no se le ha asignado un valor. El acceso a la variable se evalúa como undefined
.
Un enfoque eficaz para resolver los problemas de las variables no inicializadas es, siempre que sea posible, asignar un valor inicial. Cuanto menos exista la variable en un estado no inicializado, mejor.
Idealmente, asignaría un valor inmediatamente después de la declaración const myVariable = "Initial value"
. Pero eso no siempre es posible.
Consejo 1: favorezca const
; de lo contrario, utilice let
, pero despídase de var
En mi opinión, una de las mejores características de ECMAScript 2015 es la nueva forma de declarar variables usando const
y let
. Es un gran paso adelante.
const
y let
tienen alcance de bloque (al contrario que la función anterior con alcance var
) y existen en una zona muerta temporal hasta la línea de declaración.
Recomiendo la variable const
cuando su valor no va a cambiar. Crea una unión inmutable.
Una de las buenas características de const
es que debe asignar un valor inicial a la variable const myVariable = "initial"
. La variable no está expuesta al estado no inicializado y el acceso a undefined
es imposible.
Comprobemos la función que verifica si una palabra es un palíndromo:
length
y half
a las variables se les asigna un valor una vez. Parece razonable declararlos como const
ya que estas variables no van a cambiar.
Utilice la declaración let
para las variables cuyo valor puede cambiar. Siempre que sea posible, asigne un valor inicial inmediatamente, p. Ej. let index = 0
.
¿Qué pasa con la vieja escuela var
? Mi sugerencia es dejar de usarlo.
var
El problema de declaración es la elevación de la variable dentro del alcance de la función. Puede declarar una var
variable en algún lugar al final del alcance de la función, pero aún así, puede acceder a ella antes de la declaración: y obtendrá un undefined
.
myVariable
es accesible y contiene undefined
incluso antes de la línea de declaración: var myVariable = "Initial value"
.
Por el contrario, no se puede acceder a una variable const
o let
antes de la línea de declaración: la variable se encuentra en una zona muerta temporal antes de la declaración. Y eso es bueno porque tiene menos posibilidades de acceder a un undefined
.
El ejemplo anterior actualizado con let
(en su lugar de var
) arroja un ReferenceError
porque la variable en la zona muerta temporal no es accesible.
Fomentar el uso de const
para enlaces inmutables o let
de otra manera asegura una práctica que reduce la apariencia de los no inicializados variable.
Consejo 2: Incrementar la cohesión
La cohesión caracteriza el grado en el que los elementos de un módulo (espacio de nombres, clase, método, bloque de código) pertenecen juntos. La cohesión puede ser alta o baja.
Es preferible un módulo de alta cohesión porque los elementos de dicho módulo se centran únicamente en una única tarea. Hace que el módulo:
- Enfocado y comprensible: más fácil de entender lo que hace el módulo
- Mantenible y más fácil de refactorizar: el cambio en el módulo afecta a menos módulos
- Reutilizable: al estar enfocado en una sola tarea, hace que el módulo sea más fácil de reutilizar
- Comprobable: sería más fácil probar un módulo enfocado en una sola tarea
La alta cohesión acompañada de un acoplamiento flojo es la característica de un sistema bien diseñado.
Un bloque de código puede considerarse un módulo pequeño. Para aprovechar los beneficios de una alta cohesión, mantenga las variables lo más cerca posible del bloque de código que las usa.
Por ejemplo, si una variable existe únicamente para formar la lógica del alcance del bloque, entonces declare y haga que la variable viva solo dentro de ese bloque (usando const
o let
declaraciones). No exponga esta variable al alcance del bloque externo, ya que al bloque externo no debería importarle esta variable.
Un ejemplo clásico de la vida innecesariamente extendida de las variables es el uso de for
ciclo dentro de una función:
index
, item
y se declaran al principio del cuerpo de la función. Sin embargo, solo se usan cerca del final. ¿Cuál es el problema con este enfoque?
Entre la declaración en la parte superior y el uso en la declaración for
las variables index
, item
no están inicializados y expuestos a undefined
. Tienen un ciclo de vida excesivamente largo en todo el ámbito de funciones.
Un mejor enfoque es mover estas variables lo más cerca posible de su lugar de uso:
index
y existen solo en el alcance del bloque de la instrucción for
. No tienen ningún significado fuera de for
.
length
La variable también se declara cerca de la fuente de su uso.
¿Por qué la versión modificada es mejor que la inicial? Veamos:
- Las variables no están expuestas a un estado no inicializado, por lo que no tiene riesgo de acceder a
undefined
- Moviendo el las variables lo más cerca posible de su lugar de uso aumenta la legibilidad del código
- Los fragmentos de código altamente cohesivos son más fáciles de refactorizar y extraer en funciones separadas, si es necesario
2.2 Acceso una propiedad no existente
Cuando se accede a una propiedad de objeto no existente, JavaScript devuelve
undefined
.
Demostremos eso en un ejemplo:
favoriteMovie
es un objeto con una sola propiedad title
. El acceso a una propiedad no existente actors
mediante un descriptor de acceso de propiedad favoriteMovie.actors
se evalúa como undefined
.
Acceder a una propiedad no existente no arroja un error. El problema aparece al intentar obtener datos de la propiedad que no existe, que es la undefined
trampa más común, reflejada en el conocido mensaje de error TypeError: Cannot read property <prop> of undefined
.
Modifiquemos ligeramente el fragmento de código anterior para ilustrar un TypeError
lanzamiento:
favoriteMovie
no tiene la propiedad actors
, por lo que favoriteMovie.actors
se evalúa como undefined
.
Como resultado, acceder al primer elemento de un valor undefined
usando la expresión favoriteMovie.actors
arroja un TypeError
.
La naturaleza permisiva de JavaScript que permite acceder a propiedades no existentes es una fuente de no determinismo: la propiedad puede estar configurada o no. La buena forma de evitar este problema es restringir el objeto para que siempre haya definido las propiedades que contiene.
Desafortunadamente, a menudo no tienes control sobre los objetos. Estos objetos pueden tener un conjunto diferente de propiedades en diversos escenarios. Por lo tanto, debe manejar todos estos escenarios manualmente.
Implementemos una función append(array, toAppend)
que agrega al principio y / o al final de una matriz de elementos nuevos. El parámetro toAppend
acepta un objeto con propiedades:
-
first
: elemento insertado al principio dearray
-
last
: elemento insertado al final dearray
.
La función devuelve una nueva instancia de matriz, sin alterar la matriz original.
La primera versión de append()
, un poco ingenua, puede verse así:
Porque toAppend
objeto puede omitir first
o last
propiedades, es obligatorio verificar si estas propiedades existen en toAppend
.
Un descriptor de acceso a la propiedad se evalúa como undefined
si la propiedad no existe. La primera tentación de comprobar si las propiedades first
o last
están presentes es verificarlas con undefined
. Esto se realiza en condicionales if(toAppend.first){}
y if(toAppend.last){}
…
No tan rápido. Este enfoque tiene un inconveniente. undefined
, así como false
, null
, 0
, NaN
y ""
son valores falsos.
En la implementación actual de append()
, la función no permite insertar elementos falsos:
Los siguientes consejos explican cómo verificar correctamente la existencia de la propiedad.
Consejo 3: Verifique la existencia de la propiedad
Afortunadamente, JavaScript ofrece un montón de formas de determinar si el objeto tiene una propiedad específica:
-
obj.prop !== undefined
: comparar conundefined
directamente -
typeof obj.prop !== "undefined"
: verificar el tipo de valor de propiedad -
obj.hasOwnProperty("prop")
: verificar si el el objeto tiene una propiedad propia -
"prop" in obj
: verifique si el objeto tiene una propiedad propia o heredada
Mi recomendación es utilice el operador in
. Tiene una sintaxis corta y dulce. in
La presencia del operador sugiere una clara intención de verificar si un objeto tiene una propiedad específica, sin acceder al valor real de la propiedad.
obj.hasOwnProperty("prop")
también es una buena solución. Es un poco más largo que el operador in
y solo se verifica en las propiedades del objeto.
Mejoremos la función append(array, toAppend)
usando el operador in
:
"first" in toAppend
(y "last" in toAppend
) es true
si existe la propiedad correspondiente, false
de lo contrario.
El operador in
soluciona el problema de insertar elementos falsos 0
y false
. Ahora, agregar estos elementos al principio y al final de produce el resultado esperado
.
Consejo 4: Desestructuración para acceder a las propiedades del objeto
Al acceder a la propiedad de un objeto, a veces es necesario establecer un valor predeterminado si la propiedad no existe.
Puede usar in
acompañado de un operador ternario para lograr eso:
La sintaxis del operador ternario se vuelve abrumadora cuando aumenta el número de propiedades a comprobar. Para cada propiedad, debe crear una nueva línea de código para manejar los valores predeterminados, aumentando un muro desagradable de operadores ternarios de apariencia similar.
Para usar un enfoque más elegante, familiaricémonos con una excelente función de ES2015 llamada desestructuración de objetos.
La desestructuración de objetos permite la extracción en línea de los valores de las propiedades del objeto directamente en variables y establecer un valor predeterminado si la propiedad no existe. Una sintaxis conveniente para evitar tratar directamente con undefined
.
De hecho, la extracción de propiedades ahora es precisa:
Para ver las cosas en acción, definamos una función útil que envuelva una cadena entre comillas.
quote(subject, config)
acepta el primer argumento como la cadena a envolver. El segundo argumento config
es un objeto con las propiedades:
Aplicando los beneficios de la desestructuración de objetos, implementemos quote()
:
const { char = """, skipIfQuoted = true } = config
la asignación de desestructuración en una línea extrae las propiedades char
y skipIfQuoted
del objeto config
.
Si faltan algunas propiedades en el objeto config
, la asignación de desestructuración establece los valores predeterminados : """
para char
y false
para skipIfQuoted
.
Afortunadamente, la función todavía tiene margen de mejora.
Muevamos la asignación de desestructuración a la sección de parámetros. Y establezca un valor predeterminado (un objeto vacío { }
) para el parámetro config
, para omitir el segundo argumento cuando la configuración predeterminada sea suficiente.
La asignación de desestructuración reemplaza el parámetro config
en la firma de la función. Me gusta eso: quote()
se vuelve una línea más corto.
= {}
en el lado derecho de la asignación de desestructuración asegura que se use un objeto vacío si el segundo argumento no se especifica en absoluto quote("Sunny day")
.
La desestructuración de objetos es una característica poderosa que maneja eficientemente la extracción de propiedades de los objetos. Me gusta la posibilidad de especificar un valor predeterminado que se devolverá cuando la propiedad a la que se accede no exista. Como resultado, evita undefined
y la molestia que lo rodea.
Consejo 5: Llene el objeto con propiedades predeterminadas
Si no hay necesidad de crear variables para cada propiedad, como lo hace la asignación de desestructuración, el objeto que pierde algunas propiedades puede ser llenado con valores predeterminados.
El ES2015 Object.assign(target, source1, source2, ...)
copia los valores de todas las propiedades propias enumerables de uno o más objetos de origen en el objeto de destino. La función devuelve el objeto de destino.
Por ejemplo, debe acceder a las propiedades del objeto unsafeOptions
que no siempre contiene su conjunto completo de propiedades.
Para evitar undefined
al acceder a una propiedad no existente desde unsafeOptions
, hagamos algunos ajustes:
- Defina un objeto
defaults
que contenga los valores de propiedad predeterminados - Llame a
Object.assign({ }, defaults, unsafeOptions)
para construir un nuevo objetooptions
. El nuevo objeto recibe todas las propiedades deunsafeOptions
, pero las que faltan se toman dedefaults
.
unsafeOptions
contiene solo la propiedad fontSize
. El objeto defaults
define los valores predeterminados para las propiedades fontSize
y color
.
Object.assign()
toma el primer argumento como objeto de destino {}
. El objeto de destino recibe el valor de la propiedad fontSize
del objeto de origen unsafeOptions
. Y el valor de la propiedad color
del defaults
objeto de origen, porque unsafeOptions
no contiene color
.
El orden en el que se enumeran los objetos fuente es importante: las propiedades posteriores del objeto fuente sobrescriben a las anteriores.
Ahora puede acceder a cualquier propiedad del objeto options
, incluido options.color
que no estaba disponible en unsafeOptions
inicialmente.
Afortunadamente, existe una alternativa más fácil para llenar el objeto con propiedades predeterminadas.Recomiendo usar las propiedades de propagación en inicializadores de objetos.
En lugar de la invocación de Object.assign()
, utilice la sintaxis de extensión de objeto para copiar en el objeto de destino todas las propiedades propias y enumerables de los objetos de origen:
El El inicializador de objetos extiende las propiedades de los objetos de origen defaults
y unsafeOptions
. El orden en el que se especifican los objetos fuente es importante: las propiedades posteriores del objeto fuente sobrescriben a las anteriores.
Completar un objeto incompleto con valores de propiedad predeterminados es una estrategia eficaz para hacer que su código sea seguro y duradero. Independientemente de la situación, el objeto siempre contiene el conjunto completo de propiedades: y undefined
no se puede generar.
Consejo adicional: anular la fusión
El operador anular la fusión se evalúa a un valor predeterminado cuando su operando es undefined
o null
:
El operador de fusión nulo es conveniente para acceder a una propiedad de objeto mientras tiene un valor predeterminado cuando esta propiedad es undefined
o null
:
styles
el objeto no tiene la propiedad color
, por lo que styles.color
El descriptor de acceso de propiedad es undefined
. styles.color ?? "black"
evalúa el valor predeterminado "black"
.
styles.fontSize
es 18
, por lo que el operador de fusión nula evalúa el valor de propiedad 18
.
2.3 Parámetros de función
Los parámetros de función implícitamente predeterminados son
undefined
.
Normalmente, una función definida con un número específico de parámetros debe invocarse con el mismo número de argumentos. Ahí es cuando los parámetros obtienen los valores esperados:
Cuando multiply(5, 3)
, los parámetros a
y b
reciben 5
y respectivamente 3
valores. La multiplicación se calcula como se esperaba: 5 * 3 = 15
.
¿Qué sucede cuando omites un argumento en la invocación? El parámetro correspondiente dentro de la función se convierte en undefined
.
Modifiquemos ligeramente el ejemplo anterior llamando a la función con un solo argumento:
La invocación multiply(5)
se realiza con un solo argumento: como resultado, el parámetro a
es 5
, pero el b
el parámetro es undefined
.
Consejo 6: Utilice el valor de parámetro predeterminado
A veces, una función no requiere el conjunto completo de argumentos en la invocación. Puede establecer valores predeterminados para los parámetros que no tienen un valor.
Recordando el ejemplo anterior, hagamos una mejora. Si el parámetro b
es undefined
, déjelo por defecto en 2
:
La función se invoca con un solo argumento multiply(5)
. Inicialmente, el parámetro a
es 2
y b
es undefined
.
La declaración condicional verifica si b
es undefined
. Si sucede, la asignación b = 2
establece un valor predeterminado.
Si bien la forma proporcionada para asignar valores predeterminados funciona, no recomiendo comparar directamente con undefined
. Es detallado y parece un truco.
Un mejor enfoque es utilizar la función de parámetros predeterminados de ES2015. Es breve, expresivo y sin comparaciones directas con undefined
.
Agregar un valor predeterminado al parámetro b = 2
se ve mejor:
b = 2
en la firma de la función asegura que si b
es undefined
, el parámetro predeterminado es 2
.
La función de parámetros predeterminados de ES2015 es intuitiva y expresiva. Úselo siempre para establecer valores predeterminados para parámetros opcionales.
2.4 Valor de retorno de la función
Implícitamente, sin la instrucción
return
, una función de JavaScript devuelveundefined
.
Una función que no tiene return
devuelve implícitamente undefined
:
no devuelve ningún resultado de cálculo. El resultado de la invocación de la función es undefined
.
La misma situación ocurre cuando la instrucción return
está presente, pero sin una expresión cerca:
return;
se ejecuta la sentencia, pero no devuelve ninguna expresión. El resultado de la invocación también es undefined
.
Por supuesto, indicando cerca de return
la expresión que se devolverá funciona como se esperaba:
Ahora la invocación de la función se evalúa como 4
, que es 2
al cuadrado.
Consejo 7: No confíe en la inserción automática de punto y coma
La siguiente lista de declaraciones en JavaScript debe terminar con punto y coma (;
) :
- declaración vacía
-
let
,const
,var
,import
,export
declaraciones - declaración de expresión
-
debugger
instrucción -
continue
instrucción,break
instrucción -
throw
declaración -
return
declaración
Si usa una de las declaraciones anteriores, asegúrese de indicar un punto y coma al final:
Al final de ambos let
declaración y return
declaración se escribe un punto y coma obligatorio.
¿Qué sucede cuando no desea indicar estos punto y coma? En tal situación, ECMAScript proporciona un mecanismo de inserción automática de punto y coma (ASI), que inserta el punto y coma que faltan.
Con la ayuda de ASI, puede eliminar el punto y coma del ejemplo anterior:
El texto anterior es un código JavaScript válido. Los puntos y comas que faltan se insertan automáticamente.
A primera vista, parece bastante prometedor. El mecanismo ASI le permite omitir los puntos y comas innecesarios. Puede hacer que el código JavaScript sea más pequeño y más fácil de leer.
Hay una trampa pequeña pero molesta creada por ASI. Cuando un salto de línea se encuentra entre return
y la expresión devuelta return \n expression
, ASI inserta automáticamente un punto y coma antes del salto de línea return; \n expression
.
¿Qué significa dentro de una función tener una instrucción return;
? La función devuelve undefined
. Si no conoce en detalle el mecanismo de ASI, el undefined
devuelto inesperadamente es engañoso.
Por ejemplo, estudiemos el valor devuelto de la invocación getPrimeNumbers()
:
Entre la instrucción return
y la expresión literal de matriz existe una nueva línea. JavaScript inserta automáticamente un punto y coma después de return
, interpretando el código de la siguiente manera:
La instrucción return;
hace que la función getPrimeNumbers()
devuelva undefined
en lugar de la matriz esperada.
El problema se resuelve eliminando la nueva línea entre return
y el literal de matriz:
Mi recomendación es estudiar cómo funciona exactamente la inserción automática de punto y coma para evitar tales situaciones.
Por supuesto, nunca ponga una nueva línea entre return
y la expresión devuelta.
2.5 operador void
void <expression>
evalúa la expresión y devuelve undefined
sin importar el resultado de la evaluación.
Un caso de uso del operador void
es suprimir la evaluación de la expresión en undefined
, basándose en algún efecto secundario de la evaluación.
3. indefinido en matrices
Obtienes undefined
al acceder a un elemento de matriz con un índice fuera de límites.
colors
La matriz tiene 3 elementos, por lo que los índices válidos son 0
, 1
y 2
.
Debido a que no hay elementos de matriz en los índices 5
y -1
, los accesos colors
y colors
son undefined
.
En JavaScript, puede encontrar las llamadas matrices dispersas. Estas son matrices que tienen espacios, es decir, en algunos índices, no se definen elementos.
Cuando se accede a un espacio (también conocido como espacio vacío) dentro de una matriz dispersa, también obtiene un undefined
.
El siguiente ejemplo genera matrices dispersas e intenta acceder a sus espacios vacíos:
sparse1
se crea invocando un Array
constructor con un primer argumento numérico.Tiene 3 ranuras vacías.
sparse2
se crea con un literal de matriz con el segundo elemento que falta.
En cualquiera de estos arreglos dispersos, acceder a un espacio vacío se evalúa como undefined
.
Al trabajar con matrices, para evitar undefined
, asegúrese de utilizar índices de matriz válidos y evitar la creación de matrices dispersas.
4. Diferencia entre indefinido y nulo
¿Cuál es la principal diferencia entre undefined
y null
? Ambos valores especiales implican un estado vacío.
undefined
representa el valor de una variable que aún no se ha inicializado, mientras quenull
representa una ausencia intencional de un objeto.
Exploremos la diferencia en algunos ejemplos.
La variable number
está definida , sin embargo, no se le asigna un valor inicial:
number
variable es undefined
, que indica una variable no inicializada.
El mismo concepto no inicializado ocurre cuando se accede a una propiedad de objeto no existente:
Porque lastName
no existe en obj
, JavaScript evalúa obj.lastName
como undefined
.
Por otro lado, sabe que una variable espera un objeto. Pero por alguna razón, no puede crear una instancia del objeto. En tal caso, null
es un indicador significativo de un objeto perdido.
Por ejemplo, clone()
es una función que clona un objeto JavaScript simple. Se espera que la función devuelva un objeto:
Sin embargo, clone()
podría invocarse con un argumento que no sea de objeto: 15
o null
. En tal caso, la función no puede crear un clon, por lo que devuelve null
, el indicador de un objeto faltante.
El operador typeof
hace la distinción entre undefined
y null
:
Además, el operador de calidad estricto ===
diferencia correctamente undefined
de null
:
5. Conclusión
undefined
la existencia es una consecuencia de la naturaleza permisiva de JavaScript que permite el uso de:
- variables no inicializadas
- propiedades o métodos de objetos no existentes
- índices fuera de límites para acceder a elementos de matriz
- el resultado de la invocación de una función que no devuelve nada
La comparación directa con undefined
no es segura porque se basa en una práctica permitida pero no recomendada mencionada anteriormente.
Una estrategia eficaz es reducir al mínimo la aparición de la palabra clave undefined
en su código aplicando buenos hábitos como:
- reducir el uso de variables no inicializadas
- hacer que el ciclo de vida de las variables sea corto y cercano a la fuente de su uso
- siempre que sea posible asignar valores iniciales a las variables
- favorecer
const
; de lo contrario, utilicelet
- utilice valores predeterminados para los parámetros de función insignificantes
- verifique las propiedades existencia o llenar los objetos inseguros con propiedades predeterminadas
- evitar el uso de matrices dispersas
¿Es bueno que JavaScript tenga ambos undefined
y null
para representar valores vacíos?