Literales de plantilla para texto multilínea

Figura
Figura. EcmaScript 6 (ES6) viene con los literales de plantilla para los Strings (template literal).

Hasta ahora podíamos presentar una declaración literal de un string usando comillas simples o dobles, indistintamente. Comentamos algo de eso en el tema anterior Strings en JavaScript ES6. Ahora con EcmaScript 6 (ES6) podemos también usar el carácter acento grave para encerrar una declaración literal denominada literal de plantilla (template literal), como se observa en la Figura.

Su principal finalidad es incluir código embebido entre los delimitadores ${} que será resuelto cuando se ejecute la cadena. Pero también hay otras cosas que podemos hacer con literales de plantilla, como incluir texto multilínea.

Empecemos por ver el problema que se plantea en JavaScript cuando escribimos en el código textos muy largos, donde concatenamos varias cadenas para asignar un único texto a una variable:

let a = "Lorem ipsum ad his scripta blandit " +
        "partiendo, eum fastidii accumsan " +
        "euripidis in, eum liber hendrerit an.";
    

Esto es usual hacerlo para evitar tener líneas de código muy largas en nuestro editor de trabajo, utilizando el operador de concatenación (+). También es posible usar barras invertidas a final de cada línea para forzar un salto de línea, dígamos que sería un escape de salto de código pues sólo es a efectos de la edición de código:

let a = "Lorem ipsum ad his scripta blandit \
partiendo, eum fastidii accumsan \
euripidis in, eum liber hendrerit an.";
    

Sin embargo esta solución plantea el problema del identado, pues tras cada escape de salto de código en la siguiente línea no podemos dejar espacios algunos, pues formarían parte del texto final. Otra forma de evitar la concatenación y el escape de salto de código es usar el método join() de los arrays:

let a = ["Lorem ipsum ad his scripta blandit ",
        "partiendo, eum fastidii accumsan ",
        "euripidis in, eum liber hendrerit an."].join("");
    

Expresamos cada línea de código como una entrada del array y al final concatenamos todas las entradas con una cadena separadora vacía. Esta forma es especialmente útil cuando el texto también contiene saltos de línea.

Un salto de línea en un string se expresa usualmente con "\n". Con concatenación pondríamos un salto "\n" al final de cada línea de código, de tal forma que por comodidad de edición haríamos corresponder cada línea de código con una línea real del texto.

let a = "Lorem ipsum ad his scripta blandit \n" +
        "partiendo, eum fastidii accumsan \n" +
        "euripidis in, eum liber hendrerit an.";
    

Podríamos usar escapes de saltos de código, para lo cual agregaríamos un "\n" antes de cada barra de escape final:

let a = "Lorem ipsum ad his scripta blandit \n\
partiendo, eum fastidii accumsan \n\
euripidis in, eum liber hendrerit an.";
    

Y también un array uniéndolo por "\n":

let a = ["Lorem ipsum ad his scripta blandit ",
        "partiendo, eum fastidii accumsan ",
        "euripidis in, eum liber hendrerit an."].join("\n");
    

Esta es quizás la forma preferible pues cuando hay muchas líneas evitamos los "\n" en cada línea y sólo tenemos que añadir los corchetes de cierre del array así como join("\n") al final. Además de poder conservar el identado sin afectar al resultado, otra utilidad es que podríamos adaptar con facilidad el tipo de salto según plataforma. En sistemas Unix el salto es "\n", en Windows es "\r\n" y en versiones anteriores de Mac OS usaban "\r" (las versiones recientes están basadas también en Unix). Si el texto fuese a ser insertado como HTML podríamos adaptarlo con el salto <br>. En definitiva, tener un array de líneas de texto nos permite cualquier adaptación posterior con mayor facilidad.

Ahora con las plantillas literales de ES6 podemos mantener todos los saltos de línea tal como se escriben en el código:

let a = `Lorem ipsum ad his scripta blandit
partiendo, eum fastidii accumsan
euripidis in, eum liber hendrerit an.`;
    

De esta forma el texto será almacenado tal como aparece en el código entre los delimitadores de cadena. El problema que sigue existiendo es el del indentado, pero al menos ya nos evitamos barras de escapes y otros artilugios.

Por otro lado habrá que tener en cuenta si el salto que pretendemos incluir en el string es el que está incoporando nuestro editor de código, "\n" o "\r\n". En algún caso la diferencia podría ser significativa. Por ejemplo, en el tema que introduce el protocolo SMTP, el final del envío del mensaje se detecta con un punto entre dos saltos de tipo Windows, es decir CRLF.CRLF, que con los escapes JavaScript sería "\r\n.\r\n". Si estamos manejando texto para incluir en el cuerpo de un mensaje de correo en un sistema que no controle este aspecto podríamos tener resultados inesperados. Puedes ver más sobre esto de los saltos de línea en Wikipedia: Nueva Línea.

Expresiones embebidas en los literales de plantillas strings de ES6

Además de acoger textos multilínea, el principal objetivo de los literales de plantilla son las expresiones embebidas o interpoladas. Son expresiones insertadas dentro de la cadena entrecomillada que referencia o resuelven valores en tiempo de ejecución. Veámos un ejemplo:

Ejemplo: Expresiones embebidas en literales de plantilla de ES6

let lado = 5.13;
let area = Math.pow(lado, 2);
let mensaje = "Si lado es " + lado + " su área es " + area;
        
let lado = 5.13;
let mensaje = `SI LADO ES ${lado} SU ÁREA ES ${Math.pow(lado, 2)}`;
        
A fecha de la publicación de este tema Firefox 44, Chrome 48 e IE12 soportan los literales de plantilla. Si no hay soporte de esa u otra característica de ES6 que se use en el script de los ejemplos le aparecerá un aviso de error. Ver soporte actual en Kangx compatibility table ES. Puede ver el código JS de todos los ejemplos de esta página.

Con el primer código del ejemplo concatenamos las variables con las cadenas del mensaje. Es como hasta ahora veníamos haciendo. Con ES6 y las expresiones embebidas podemos ahorrarnos el paso de guardar el cálculo del área en una variable para luego concatenarlo. Esto por supuesto no será eficiente si fuéramos a necesitar ese cálculo en un momento posterior del código.

Una expresión es cualquier cosa que devuelva un valor. Así también un literal de string o, incluso, de plantilla, también son expresiones. Así que podríamos anidar plantillas, como en este ejemplo donde obtendríamos abc 123 def:


console.log(`abc ${`${123}`} def`);
    

Las plantillas se detectan con la secuencia de caracteres "${", por lo que para evitarlas y expresar literalmente esa secuencia podemos escapar alguno de sus caracteres:


console.log(`abc \${miVar} def $\{123} `);
 
    

Literales de plantilla etiquetadas

Los literales de plantilla etiquetadas nos permiten realizar una transformación posterior a la ejecución de un literal de plantilla. Son realmente funciones que toman como argumentos los valores que entran en juego en la plantilla, para que podamos actuar con ellos de alguna forma. Lo mejor es verlo en acción en este ejemplo:

Ejemplo: Literales de plantilla etiquetadas

let x = 1, y = 2;
//Un literal de plantilla con esas variables
console.log(`A${x}B${y}C`); //Obtenemos "A1B2C"
//Definimos una función de etiqueta de plantilla
function tag(arr, val1, val2){
    //Modificamos la plantilla para que aparezcan
    //los trozos y después los valores
    return arr.join("") + val1 + val2;
}
//Ahora probamos esa función
let mens = tag`A${x}B${y}C`;
console.log(mens); //Obtenemos "ABC12""
        

Este es el resultado de console.log(`A${x}B${y}C`):

Este es el resultado de console.log(tag`A${x}B${y}C`):

A fecha de la publicación de este tema Firefox 44, Chrome 48 e IE12 soportan los literales de plantilla. Si no hay soporte de esa u otra característica de ES6 que se use en el script de los ejemplos le aparecerá un aviso de error. Ver soporte actual en Kangax compatibility table ES. Puede ver el código JS de todos los ejemplos de esta página.

Tenemos en ese ejemplo dos variables que manipularemos con una plantilla muy simple (`A${x}B${y}C`). Esto nos producirá el resultado "A1B2C". Podemos realizar una segunda transformación definiendo una función de etiqueta de plantilla que denominamos en el ejemplo como tag(). El nombre de la función puede ser cualquiera. Los argumentos vienen de la propia plantilla que estamos usando:

  • El primer argumento es el array ["A", "B", "C"] que contiene los trozos de cadenas de texto entre plantillas, es decir, entre las dos plantillas del ejemplo ${x} y ${y}.
  • El segundo argumento es el primer valor de sustitución, en este caso el valor de la variable x obtenido en la ejecución de la primera plantilla ${x}.
  • El tercer argumento es el segundo valor de sustitución, en este caso el valor de la variable y obtenido en la ejecución de la segunda plantilla ${y}.

Cuando invoquemos la función con la sintaxis tag`A${x}B${y}C`; JavaScript generará los argumentos a partir de la ejecución de ese literal de plantilla, teniéndolos disponibles para hacer lo que sea con ellos. En el ejemplo los tomamos para modificar el resultado de la plantilla. Unimos los trozos del array con una cadena vacía y luego concatenamos los dos valores. Observe la sintaxis de la llamada a la función, donde el nombre de la función ha de ir inmediatamente seguido de la comilla de apertura del literal de plantilla.

Al ejecutarse la función de etiqueta de plantilla siempre tendremos disponibles los argumentos con ese esquema: el primer argumento será un array con los trozos y el resto de argumentos son los valores de sustitución. Así que el nombre de los argumentos puede ser cualesquiera. O incluso no especificar ningún argumento pues ya están implícitos. Esto sería útil cuando definamos una función que pueda aplicarse a cualquier plantilla, sin tener que conocer previamente cuántos valores de sustitución tendría.

Sabiendo que podemos acceder a la colección de argumentos de una función usando la palabra clave arguments, podemos definir una función que itere por esa colección. Si hay n argumentos, el primero será el array de trozos que tendrá también esa longitud, mientras que habrá n-1 valores, pues los trozos se intercalan entre los valores incluso al principio y final de plantilla: `A${x}B${y}C`;. Si entre dos plantillas o al inicio y final no hay trozo de texto se supone una cadena vacía. Así las condiciones de longitudes de trozos y valores siempre se mantiene con coherencia.

Podemos iterar por los valores de sustitución, teniendo el siguiente esquema de funcionamiento del bucle.

function tag(){
    //En arguments[0] tenemos el array de trozos. El resto de 
    //argumentos son los valores de sustitución
    let n = arguments.length;
    //Iteramos por los valores, desde i=1 hasta i=n-1
    for (let i=1; i<n; i++){
        //El array del primer argumento va desde i=0 hasta i=n-1
        //En arguments[0][i-1] tendremos el trozo (i-1)
        //En arguments[i] tendremos el valor i
    }
    //En arguments[0][n-1] tendremos el último trozo
}
    

Lo anterior nos permite ir extrayendo trozos y valores de forma alternada. Cuando el bucle finalice aún nos faltará el último trozo de la derecha de la plantilla. Podríamos acceder a el sabiendo que el array tiene la misma longitud que los argumentos.

También podríamos iterar por el array de trozos. En este caso habría que prever con un condicional que hay menos valores que trozos. Pero al menos no necesitamos acceder al último trozo por fuera del bucle.

function tag(){
    //Note que arguments.length == arguments[0].length;
    let n = arguments.length; 
    //Iteramos por todos los trozos del array
    for (let i=0; i<n; i++){
        //En arguments[0][i] tendremos el trozo i
        if (i<n-1){
            //En arguments[i+1] tendremos el valor i+1
        }
    }
    

Con ES6 hay una forma de usar argumentos definidos aún cuando no conozcamos su número. Se trata del operador de propagación cuya sintaxis son tres puntos suspensivos antes de una expresión. Si a una función pasamos por ejemplo los argumentos (arg1, arg2, ...restoArgs) habremos definido los dos primeros argumentos de forma explícita, mientras que el resto de argumentos se pasan dentro de un array en ...restoArgs. Usando esto también podemos manejar la función de etiqueta de plantilla:

function tag(trozos, ...valores){
    //trozos es un array con n cadenas de texto
    //valores es un array con n-1 valores
    ...
}
    

Para ver lo anterior en acción presentamos el siguiente ejemplo. Con la opción Sin argumentos iteramos por la colección de valores a partir de arguments. Con la opción Con argumentos usamos un primer argumento para los trozos de cadena y el operador de propagación para el array de valores.

Ejemplo: Manejo de argumentos de la función etiqueta de literales de plantilla

Seleccione una opción
function resaltar(){
    let resultado = "";
    //iteramos por valores
    let n = arguments.length;
    for (let i=1; i<n; i++){
        resultado += `\t<span class="rojo">${arguments[0][i-1].toUpperCase()}</span>\n`;
        resultado += `\t<span class="azul">${arguments[i]}</span>\n`;
    }
    //Extraemos el último trozo de la derecha de la plantilla
    //Note que arguments.length == arguments[0].length
    resultado += `\t<span class="rojo">${arguments[0][n-1]}</span>\n`;
    return `<span class="resalte-aqua bold">\n${resultado}</span>`;
}
function resaltar(trozos, ...valores){
    let resultado = "";
    //Iteramos por trozos
    let n = trozos.length;
    for (let i=0; i<n; i++){
        resultado += `\t<span class="azul">${trozos[i]}</span>\n`;
        if (i<n-1){
            //Hay n trozos y n-1 valores
            resultado += `\t<span class="rojo">${valores[i]}</span>\n`;
        }
    }
    return `<span class="resalte-amarillo bold">\n${resultado}</span>`;
}
//Ahora probamos esa función Tag
let lado = 5;
let mens = resaltar`Si lado es ${lado} su área es ${Math.pow(lado, 2)} m²`;
document.getElementById("resultado-tag").innerHTML = mens;
        

Este es el resultado:

El código HTML generado es:

En arguments, que es como un array, encontramos esto:

El argumento trozos es un array con este contenido:

El argumento valores es un array con este contenido:

A fecha de la publicación de este tema Firefox 44, Chrome 48 e IE12 soportan los literales de plantilla. Si no hay soporte de esa u otra característica de ES6 que se use en el script de los ejemplos le aparecerá un aviso de error. Ver soporte actual en Kangx compatibility table ES. Puede ver el código JS de todos los ejemplos de esta página.

El método genérico raw() del objeto intrínseco String

El objeto built-in String dispone de un método genérico raw() que nos devuelve una cadena ignorando los escapes de JavaScript:

  • Espacios blancos: \n\r\v\t\b\f para nueva línea, retorno de carro, tabulación vertical, tabulación horizontal, retroceso y avance de línea.
  • Escapes de comillas simples \' y dobles \".
  • Escape de barra invertida \\.
  • Carácter NULL \0.
  • Escapes Unicode \uNNNN hasta FFFF, extendido \u{NNNNNN} hasta 10FFFF y ASCII extendido \xNN hasta FF.

Esto podría ser útil para extraer el texto tal como se escribió, con todos sus escapes. Así por ejemplo String.raw`123\n456`; nos devolverá un string con el literal del escape, es decir, escapando a su vez las barras invertidas: "123\\n456";. En el siguiente ejemplo se muestran más detalles.

Ejemplo: Método raw()

Esta es la vista de salida del string "\xA9\"\t\u260E\n\u{1F512}"; que se compone del carácter "©", una comilla doble escapada "\"", una tabulación "\t", el Unicode "\u260E" icono de un teléfono, un salto de línea "\n" y el Unicode "\u{1F512}" que se verá como un icono de un candado.

 

Con String.raw`\xA9\"\t\u260E\n\u{1F512}`; obtenemos el string tal como se escribió en el código ignorando los escapes:

 

También podemos usar una función de etiqueta de plantilla, puesto que el primer argumento, que es un array con los trozos de cadenas entre plantillas, dispone de la propiedad raw, que resulta ser a su vez un array con los trozos de texto sin los escapes:

function tag(trozos, ...valores){
    return trozos.raw[0];
}
        

De tal forma que con tag`\xA9\"\t\u260E\n\u{1F512}`; obtenemos lo mismo:

 
A fecha de la publicación de este tema Firefox 44, Chrome 48 e IE12 soportan los literales de plantilla. Si no hay soporte de esa u otra característica de ES6 que se use en el script de los ejemplos le aparecerá un aviso de error. Ver soporte actual en Kangx compatibility table ES. Puede ver el código JS de todos los ejemplos de esta página.

En el ejemplo anterior ejecutamos con JavaScript el método String.raw() y también obtenemos el valor de la propiedad raw del primer argumento de la función de etiqueta, volcando los strings en los elementos de la página. Se observan como los caracteres aparecen escapados tal como se escribieron en el código.