Coalescencia nula
La propuesta de coalescencia nula (??
) agrega un nuevo operador de cortocircuito destinado a manejar valores predeterminados.
Probablemente ya estés familiarizado con los otros operadores de cortocircuito &&
y ||
. Ambos operadores manejan valores “truthy” y “falsy”. Imagina el ejemplo de código lhs && rhs
. Si lhs
(lado izquierdo) es falsy, la expresión se evalúa como lhs
. De lo contrario, se evalúa como rhs
(lado derecho). Lo opuesto ocurre con el ejemplo de código lhs || rhs
. Si lhs
es truthy, la expresión se evalúa como lhs
. De lo contrario, se evalúa como rhs
.
Pero, ¿qué significan exactamente “truthy” y “falsy”? En términos de la especificación, equivale a la operación abstracta ToBoolean
. Para los desarrolladores regulares de JavaScript, todo es truthy excepto los valores falsy undefined
, null
, false
, 0
, NaN
, y la cadena vacía ''
. (Técnicamente, el valor asociado con document.all
también es falsy, pero llegaremos a eso más tarde).
Entonces, ¿cuál es el problema con &&
y ||
? ¿Y por qué necesitamos un nuevo operador de coalescencia nula? Es porque esta definición de truthy y falsy no se ajusta a todos los escenarios y esto conduce a errores. Imagina lo siguiente:
function Component(props) {
const enable = props.enabled || true;
// …
}
En este ejemplo, tratemos la propiedad enabled
como una propiedad booleana opcional que controla si alguna funcionalidad en el componente está habilitada. Es decir, podemos establecer explícitamente enabled
como true
o false
. Pero, como es una propiedad opcional, podemos implícitamente establecerla como undefined
al no asignarla en absoluto. Si es undefined
, queremos tratarlo como si el componente tuviera enabled = true
(su valor predeterminado).
Probablemente ya puedes identificar el error en el ejemplo de código. Si establecemos explícitamente enabled = true
, entonces la variable enable
es true
. Si establecemos implícitamente enabled = undefined
, entonces la variable enable
es true
. ¡Y si establecemos explícitamente enabled = false
, entonces la variable enable
sigue siendo true
! Nuestra intención era predeterminar el valor a true
, pero en realidad forzamos el valor en su lugar. La solución en este caso es ser muy explícito con los valores que esperamos:
function Component(props) {
const enable = props.enabled !== false;
// …
}
Vemos este tipo de errores surgir con todos los valores falsy. Esto podría haberse tratado fácilmente de una cadena opcional (donde la cadena vacía ''
es considerada una entrada válida), o un número opcional (donde 0
es considerado una entrada válida). Este es un problema tan común que estamos introduciendo el operador de coalescencia nula para manejar este tipo de asignación de valores predeterminados:
function Component(props) {
const enable = props.enabled ?? true;
// …
}
El operador de coalescencia nula (??
) actúa de manera muy similar al operador ||
, excepto que no usamos “truthy” al evaluar el operador. En su lugar, usamos la definición de “nulo”, que significa “es el valor estrictamente igual a null
o undefined
”. Así que imagina la expresión lhs ?? rhs
: si lhs
no es nulo, se evalúa como lhs
. De lo contrario, se evalúa como rhs
.
Explícitamente, eso significa que los valores false
, 0
, NaN
, y la cadena vacía ''
son todos valores falsy que no son nulos. Cuando estos valores falsy-pero-no-nulos están en el lado izquierdo de un lhs ?? rhs
, la expresión se evalúa como ellos en lugar del lado derecho. ¡Adiós a los errores!
false ?? true; // => false
0 ?? 1; // => 0
'' ?? 'default'; // => ''
null ?? []; // => []
undefined ?? []; // => []
¿Qué pasa con la asignación predeterminada al destructurar?
Puede que hayas notado que el último ejemplo de código también se podría haber corregido usando una asignación predeterminada dentro de una desestructuración de objeto:
function Component(props) {
const {
enabled: enable = true,
} = props;
// …
}
Es un poco extenso, pero sigue siendo completamente válido en JavaScript. Sin embargo, utiliza una semántica ligeramente diferente. La asignación predeterminada dentro de desestructuraciones de objetos verifica si la propiedad es estrictamente igual a undefined
, y si es así, predetermina la asignación.
Pero estas pruebas de igualdad estricta solo para undefined
no siempre son deseables, y no siempre hay un objeto disponible para realizar la desestructuración. Por ejemplo, tal vez quieras establecer un valor predeterminado en los valores de retorno de una función (no hay objeto para desestructurar). O tal vez la función devuelva null
(lo cual es común en las API del DOM). Estas son las situaciones en las que querrás recurrir a la coalescencia nula:
// Coalescencia nula concisa
const link = document.querySelector('link') ?? document.createElement('link');
// Estructuración predeterminada con plantilla inicial
const {
link = document.createElement('link'),
} = {
link: document.querySelector('link') || undefined
};
Además, ciertas características nuevas como chaining opcional no funcionan perfectamente con la estructuración. Dado que la estructuración requiere un objeto, debes proteger la estructuración en caso de que el chaining opcional devuelva undefined
en lugar de un objeto. Con la coalescencia nulosa, no tenemos ese problema:
// Chaining opcional y coalescencia nulosa combinados
const link = obj.deep?.container.link ?? document.createElement('link');
// Estructuración predeterminada con chaining opcional
const {
link = document.createElement('link'),
} = (obj.deep?.container || {});
Combinar y ajustar operadores
El diseño del lenguaje es complicado, y no siempre podemos crear nuevos operadores sin una cierta cantidad de ambigüedad en la intención del desarrollador. Si alguna vez has combinado los operadores &&
y ||
juntos, probablemente te hayas encontrado con esta ambigüedad tú mismo. Imagina la expresión lhs && middle || rhs
. En JavaScript, esto se analiza realmente de la misma manera que la expresión (lhs && middle) || rhs
. Ahora imagina la expresión lhs || middle && rhs
. Esta se analiza realmente de la misma manera que lhs || (middle && rhs)
.
Probablemente puedes ver que el operador &&
tiene una mayor precedencia para su lado izquierdo y derecho que el operador ||
, lo que significa que los paréntesis implícitos envuelven el &&
en lugar de el ||
. Al diseñar el operador ??
, tuvimos que decidir cuál sería la precedencia. Podría tener:
- menor precedencia que ambos
&&
y||
- menor precedencia que
&&
pero mayor que||
- mayor precedencia que ambos
&&
y||
Para cada una de estas definiciones de precedencia, luego tuvimos que probarla con los cuatro casos posibles:
lhs && middle ?? rhs
lhs ?? middle && rhs
lhs || middle ?? rhs
lhs ?? middle || rhs
En cada expresión de prueba, tuvimos que decidir dónde pertenecían los paréntesis implícitos. Y si no envolvían la expresión exactamente de la manera que el desarrollador pretendía, entonces tendríamos un código mal escrito. Desafortunadamente, sin importar el nivel de precedencia que eligiéramos, una de las expresiones de prueba podría violar las intenciones del desarrollador.
Al final, decidimos requerir paréntesis explícitos al combinar ??
y (&&
o ||
) (¡observa que fui explícito con mi agrupación de paréntesis! ¡broma meta!). Si combinas, debes envolver uno de los grupos de operadores en paréntesis, o obtendrás un error de sintaxis.
// Se requiere grupos explícitos de paréntesis para combinar
(lhs && middle) ?? rhs;
lhs && (middle ?? rhs);
(lhs ?? middle) && rhs;
lhs ?? (middle && rhs);
(lhs || middle) ?? rhs;
lhs || (middle ?? rhs);
(lhs ?? middle) || rhs;
lhs ?? (middle || rhs);
De esta manera, el analizador del lenguaje siempre coincide con lo que el desarrollador pretendía. Y cualquier persona que lea el código más tarde también puede comprenderlo de inmediato. ¡Genial!
Háblame sobre document.all
document.all
es un valor especial que nunca jamás deberías usar. Pero si decides usarlo, es mejor que sepas cómo interactúa con lo “verdadero” y lo “nulo”.
document.all
es un objeto similar a un array, lo que significa que tiene propiedades indexadas como un array y una longitud. Los objetos generalmente son verdaderos — pero sorprendentemente, document.all
pretende ser un valor falso. De hecho, es igual en forma laxa a ambos null
y undefined
(lo que normalmente significa que no puede tener propiedades en absoluto).
Cuando se usa document.all
con &&
o ||
, pretende ser falso. Pero no es estrictamente igual a null
ni a undefined
, por lo que no es nulo. Así que cuando se usa document.all
con ??
, se comporta como cualquier otro objeto lo haría.
document.all || true; // => true
document.all ?? true; // => HTMLAllCollection[]