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 de array
  • last: elemento insertado al final de array.

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 con undefined 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 objeto options. El nuevo objeto recibe todas las propiedades de unsafeOptions, pero las que faltan se toman de defaults.

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 devuelve undefined.

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 que null 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, utilice let
  • 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?

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *