Expresión vs sentencia en JavaScript, a fondo

En esta página
Enterate del próximo post
Un mail cuando publico algo nuevo. Sin spam y te podés dar de baja cuando quieras.

Un mail cuando publico algo nuevo. Sin spam y te podés dar de baja cuando quieras.
El post anterior sobre variables y funciones ya mostró una expresión y una sentencia una al lado de la otra: 3 + 4 como expresión que produce un valor y se pierde, let total = 3 + 4 como sentencia que guarda ese valor. Esa distinción tiene una consecuencia más profunda de lo que parece a primera vista: define qué se puede escribir en cada lugar del código, y por qué algunas construcciones que se ven parecidas se comportan de forma completamente distinta.
console.log(3 + 4);
// 73 + 4 es una expresión: se evalúa y produce el valor 7. console.log(3 + 4) completo es una sentencia: una instrucción que el motor ejecuta de punta a punta, en este caso una llamada a función. Adentro de esa sentencia hay una expresión, 3 + 4, ocupando el lugar del argumento. La sentencia entera no produce ningún valor utilizable por otra parte del código: hace su trabajo (imprimir algo en la consola) y ahí termina.
Esto es parejo en cualquier sentencia: let total = 3 + 4; contiene la expresión 3 + 4; saludar("Fernando"); contiene la expresión "Fernando" como argumento y la llamada completa es a la vez una expresión (produce el string que devuelve saludar) envuelta en una sentencia (la línea entera, terminada en ;). Una expresión puede vivir sola, dentro de otra expresión, o dentro de una sentencia. Una sentencia, en cambio, no puede aparecer dentro de una expresión: ese límite es el que se explora en el resto de este post.
let nombre = "Fernando";
console.log(nombre = "Ana");
// Ana
console.log(nombre);
// Ananombre = "Ana" (sin let, reasignando una variable que ya existe) no es solo una acción: es una expresión, y como toda expresión, produce un valor. Ese valor es el mismo que queda asignado, "Ana". Por eso console.log(nombre = "Ana") funciona: la asignación ocurre primero, y el valor que produce ("Ana") es lo que efectivamente recibe console.log como argumento. La segunda línea confirma que la asignación además dejó su efecto de siempre: nombre sigue valiendo "Ana" después.
Esto explica algo que la declaración con let no permite: let nombre = "Fernando" no se puede meter dentro de otra expresión de la misma forma, porque let no es un operador que produzca un valor, es una palabra clave que arranca una sentencia completa. La reasignación con = sola sí es una expresión, y por eso se la puede anidar donde una declaración no entra. La próxima sección muestra qué pasa cuando se intenta de todos modos.
function: sentencia en un lugar, expresión en otrofunction saludar(nombre) {
return `Hola, ${nombre}`;
}
const duplicar = function (numero) {
return `${numero} + ${numero} = ${numero + numero}`;
};
console.log(saludar("Fernando"));
// Hola, Fernando
console.log(duplicar(4));
// 4 + 4 = 8function saludar(nombre) { ... }, escrita así, arrancando una línea con la palabra function seguida de un nombre, es una sentencia: una declaración de función. function (numero) { ... }, en cambio, aparece después de un =, en un lugar donde el motor espera una expresión (algo que produzca un valor para guardar en duplicar). Ahí la misma palabra function deja de ser una declaración y pasa a ser una expresión de función: produce un valor (la función en sí) que la sentencia const duplicar = ... guarda, igual que 3 + 4 produce un valor que let total = ... guarda.
Esto ya había aparecido en el post anterior, sin nombrarlo: const saludar = (nombre) => ... con arrow function es exactamente el mismo caso, una expresión de función guardada en una constante. La arrow function y function (numero) { ... } son dos formas distintas de escribir una expresión de función; function saludar(nombre) { ... } sin asignar a nada es la única de las tres que es una sentencia. Lo que decide si function es sentencia o expresión no es la palabra en sí, es el lugar exacto donde aparece en el código.
console.log(let);
// ReferenceError: let is not definedEste código llama la atención: let es una palabra reservada para declarar variables, y sin embargo acá se comporta como un nombre cualquiera. El archivo sí llega a ejecutarse (el error aparece al correrlo, no al analizarlo), y lo que tira es un error de referencia, el mismo tipo de error que tiraría cualquier nombre usado sin haber sido declarado antes. Esto pasa porque, en esta posición (adentro de los paréntesis de una llamada, un lugar donde el motor espera una expresión), let no puede leerse como el inicio de una declaración: ahí no hay espacio para una sentencia. La única lectura que le queda disponible al motor es tratarlo como si fuera un identificador común, una expresión válida aunque no exista ninguna variable con ese nombre.
const resultado = let y = 5;const resultado = let y = 5;
^
SyntaxError: Unexpected identifier 'y'Este segundo archivo ni siquiera llega a ejecutarse: falla al analizarlo, antes de correr una sola línea. La razón no es evidente a simple vista, porque el error señala y, no let. Es la misma lectura del ejemplo anterior, llevada un paso más allá: a la derecha de ese =, el motor lee let como un identificador suelto, exactamente igual que en console.log(let). Con esa lectura, la línea queda como const resultado = let seguido de y, dos nombres pegados uno al lado del otro sin ningún operador entre medio, y ahí el análisis no tiene forma de continuar: de ahí el error apuntando a y, no a let.
El error puntual cambia según el contexto exacto (a veces señala el identificador siguiente, otras el propio let o const), pero la causa de fondo es siempre la misma: una declaración es una sentencia, y una sentencia no tiene ningún lugar disponible adentro de una expresión. Lo mismo vale para function saludar() { ... } escrita como declaración: intentar usarla directamente donde se espera un valor, sin pasar por una asignación que la convierta en expresión, choca contra el mismo límite. Confundir esos dos lugares no es un detalle de estilo, es lo que separa un programa que compila de uno que no.
Una expresión produce un valor; una sentencia ejecuta una instrucción completa, y puede tener expresiones adentro pero nunca al revés. La asignación (
nombre = "Ana") es una expresión con valor propio, distinta de la declaración (let nombre = ...) que la contiene. La misma palabra, comofunction, puede ser sentencia o expresión según el lugar donde aparece: no hay nada en la palabra en sí que lo determine. Intentar poner una sentencia donde el motor espera una expresión termina siempre en unSyntaxError, antes de que el programa llegue a correr.
El próximo post de la serie trata el scope léxico: qué determina, en cada punto del código, si un nombre está visible o no, y por qué esa regla ya estuvo actuando en silencio en varios de los ejemplos de esta serie.