Los símbolos bien conocidos

Symbol.iterator, un símbolo bien conocido en JavaScript
Figura. Symbol.iterator, un símbolo bien conocido en JavaScript.

Los símbolos bien conocidos nos permiten cambiar el comportamiento de ciertas características internas a JavaScript. Se trata de que el programador pueda acceder a ellas para llevar a cabo modificaciones en la forma predeterminada de actuar. Hay una tabla en la especificación EcmaScript 6 Well-Known Symbols sobre los símbolos bien conocidos, donde se da una breve descripción.

Los siguientes enlaces llevan a apartados de este tema que exponen todos los símbolos bien conocidos de ES6:

  1. Symbol.toStringTag
  2. Symbol.match, search, split y replace
  3. Symbol.hasInstance
  4. Symbol.toPrimitive
  5. Symbol.isConcatSpreadable
  6. Symbol.unscopables
  7. Symbol.species
  8. Symbol.iterator

Símbolo bien conocido toStringTag

El símbolo bien conocido toStringTag modifica el comportamiento del método toString(). Éste es un método de Object que devuelve la cadena [object TIPO] siendo "TIPO" el correspondiente String, Number, Boolean, etc. Pero toString() es sobrescrito en las instancias. Así por ejemplo Object.prototype.toString.call("abc") devuelve [object String] mientras que "abc".toString() devuelve "abc".

Consultar toString() del prototipo ha venido sirviendo para conocer el tipo de datos de cualquier valor. Pues para los tipos primitivos string, number, boolean, null, undefined y las funciones podemos consultar el tipo con typeof. Para el resto de valores esa consulta siempre nos devolverá object. Así si quisiéramos saber si un valor es un Array tendríamos que usar el toString() del prototipo y comprobar que resulta [object Array]. En el siguiente ejemplo podrá ver todo esto en ejecución:

Ejemplo: Tipos de datos en JavaScript

Este ejemplo usa ES6 en modo estricto. Puedes consultar el código JS original de este ejemplo.

Un constructor es una función. Y la propiedad name para las funciones es algo nuevo de ES6. Podrá comprobar que consultando el nombre del constructor (val.constructor.name) obtenemos también ese "TIPO" que aparece en la expresión [object TIPO] que resulta al ejecutar toString() del prototipo. Aunque no para null y undefined. Además para los objetos generados a partir de un propio constructor es la única forma de obtener el tipo correspondiente, pues incluso el toString() del prototipo resulta [object Object].

Podemos utilizar el símbolo bien conocido toStringTag para modificar el resultado del toString() para un objeto de un constructor propio. En el siguiente código tenemos un constructor y cambiamos en su prototipo el símbolo [Symbol.toStringTag] a una cadena de texto. En este caso usamos el nombre del constructor, pero podría ser cualquier String. Tanto toString() del valor (la instancia) como del prototipo ahora nos devuelven [object MiConstructor].

//Modificando el toString() de un constructor
function MiConstructor(){}
MiConstructor.prototype[Symbol.toStringTag] = MiConstructor.name;
let valor = new MiConstructor();
//Estas dos consultas devuelven [object MiConstructor]
console.log(valor.toString());
console.log(Object.prototype.toString.call(valor));
    
Todos los códigos expuestos en este tema han sido probados en Chrome 50, de donde hemos tomado las salidas console.log().

Símbolos bien conocidos Symbol.match, search, split y replace

Los métodos de expresiones regulares que aplican a String son match(), search(), split() y replace(). Podemos modificar el comportamiento de estos métodos usando los símbolos bien conocidos Symbol.match, Symbol.search, Symbol.split y Symbol.replace. En este apartado sólo probaremos el símbolo para match.

El método match() con búsqueda global (modificador "g") busca una o todas las coincidencias de una expresión regular sobre un String, devolviéndolas en un Array. Sin búsqueda global devuelve también un Array con la primera coincidencia en la posición cero del Array y en el resto de posiciones estarán las coincidencias con los grupos de captura. En el siguiente código hay búsqueda global por lo que tenemos un Array con todos los números existentes en el siguiente texto:

let texto = `Cervantes nació el 29 de septiembre de 1547. 
En el año 1605 publicó El Quijote con 664 páginas.`;
let arr = texto.match(/\d+/g);
console.log(arr); // ["29", "1547", "1605", "664"]
    

Supongamos que en lugar de usar una expresión regular nos creamos nuestro propio método para buscar esos números, entendiendo un número como una secuencia de dígitos. La siguiente función usa el método reduce() de Array aplicado a un String. Va tomando los caracteres uno a uno y los va reduciendo hasta un único resultado. Para ver si un carácter es un dígito consultamos su código ASCII con el método charCodeAt(0). Si lo es lo concatenamos al anterior. Si no lo es y el último carácter reducido no es un espacio le agregamos uno. Al finalizar tendremos un String como "29 1547 1605 664 " para el texto del ejemplo anterior. Con trim() quitamos el espacio final y acabamos devolviendo un Array sólo con los números:

function buscarNumeros(texto){    
    return Array.prototype.reduce.call(texto, (anterior, actual) => {
        let key = actual.charCodeAt(0);
        if ((key>47) && (key<58)){
            anterior += actual;
        } else if (anterior && (anterior[anterior.length-1]!==" ")) {
            anterior += " ";
        }
        return anterior;
    }, "").trim().split(" ");
}
    

Aplicado al mismo texto obtenemos igual resultado que con la expresión regular /\d+/g usada antes:

let texto = `Cervantes nació el 29 de septiembre de 1547. 
En el año 1605 publicó El Quijote con 664 páginas.`;
console.log(
    buscarNumeros(texto)
); // ["29", "1547", "1605", "664"]
    

Podemos usar esa función buscarNumeros() para cambiar el comportamiento del método match(). Sólo tenemos que declarar una expresión regular vacía y cambiar su [Symbol.match] a nuestra función:

let reg = new RegExp();
reg[Symbol.match] = buscarNumeros;
console.log(
    texto.match(reg)
); // ["29", "1547", "1605", "664"]
    

A partir de ahí vemos que texto.match(reg) buscará con nuestra función y no con el método match. Es posible simplificar el código anterior sabiendo que podemos enviar un argumento con un objeto con los símbolos necesarios, en este caso sólo [Symbol.match]:

console.log(
    texto.match({[Symbol.match]: buscarNumeros})
); // ["29", "1547", "1605", "664"]
    

En el siguiente ejemplo interactivo podrá probar este código así como con expresión regular y directamente con nuestra función buscarNumeros():

Ejemplo: Symbol.match

texto.match(/\d+/g) devuelve

buscarNumeros(texto) devuelve

Primero con let reg = new RegExp(); reg[Symbol.match] = buscarNumeros; Entonces texto.match(reg) devuelve

texto.match({[Symbol.match]: buscarNumeros}) devuelve

Este ejemplo usa ES6 en modo estricto. Puedes consultar el código JS original de este ejemplo. Este ejemplo se ejecuta correctamente en Chrome 50, pero no en Firefox 46 para Symbol.match (disponible en la versión 49). Compruebe el soporte de su navegador de los símbolos bien conocidos en la web kangax.github.io.

Símbolo bien conocido Symbol.hasInstance

En ES6 se introduce el método Symbol.hasInstance() para las funciones, actuando como constructores, con objeto de obtener lo mismo que con el operador instanceof para saber si un objeto es una instancia de un constructor:

function MiConstructor(){}
let instancia = new MiConstructor();
console.log(instancia instanceof MiConstructor); // true
console.log(MiConstructor[Symbol.hasInstance](instancia)); // true
    

Podemos utilizar ese símbolo para cambiar la forma de actuar de hasInstance() así como del operador instanceof. Hemos de usar ObjectdefineProperty pues la propiedad es nonwritable:

function MiConstructor(){}
Object.defineProperty(MiConstructor, Symbol.hasInstance, {
   value: function(valor){
       //Esto produce un volcado de pila en CH51
       //return (valor instanceof MiConstructor)?"SÍ":"NO";
       return (valor.constructor.name==="MiConstructor")?"SÍ":"NO";
   }
});

let instancia = new MiConstructor();
// Debería ser "SÍ", pero CH51 y FF47 nos da true
console.log(instancia instanceof MiConstructor);
// "SÍ" en CH51 y FF47
console.log(MiConstructor[Symbol.hasInstance](instancia));


let str = new String("abc");
// Debería ser "NO", pero es true en CH51 (?) y false en FF47
console.log(str instanceof MiConstructor);
// "NO" en CH51 y FF47
console.log(MiConstructor[Symbol.hasInstance](str));
    

Las marcas entre paréntesis de los comentarios indican los valores obtenidos en los navegadores actuales indicados, que aún no soportan el cambio en el comportamiento de instanceof para que resulte igual que Symbol.hasInstance().

Aclaración (30 Junio 2016):

Este símbolo hasInstance aún no es soportado completamente por los navegadores actuales Chrome 51 ni Firefox 47. En Chrome 51 el código return (valor instanceof MiConstructor) ? "SÍ" : "NO" produce un volcado de pila (RangeError: Maximum call stack size exceeded), seguramente porque se producen llamadas entre instanceof y hasInstanceOf() que vuelcan la pila de ejecución. Sin embargo en Firefox no sucede. En todo caso ambos no devuelven los valores esperados "SÍ" y "NO" usando instanceof que tendría que ejecutarse llamando al símbolo que hemos modificado.

Cambiando la devolución por return (valor.constructor.name === "MiConstructor") ? "SÍ" : "NO" tampoco devuelve lo esperado cuando usamos instanceof. Incluso para CH51 la variable String dice erróneamente que es instancia del constructor. En resumen, que hay que seguir esperando hasta que los navegadores adopten completamente este símbolo.

Símbolo bien conocido Symbol.toPrimitive

Operaciones como la suma de números y cadenas o comparación simple (==) coercionan los objetos a tipos primitivos iguales para poder llevar a cabo dicha operación.

//La suma de un String y un Number coerciona el número a
//cadena, concatenando ambas cadenas
let x = "abc" + 123;
console.log(x); // "abc123"
console.log(typeof x); // "string"
//Si el número es un objeto también lo coerciona
let num = new Number(123);
x = "abc" + num;
console.log(x); // "abc123"
console.log(typeof x); // "string"
//La comparación simple también coerciona el número
console.log(123 == "123"); // true
//Pero la comparación estricta no lo hace
console.log(123 === "123"); // false
    

Con la comparación hay que tener un cuidado especial. De hecho deberíamos siempre usar la comparación estricta, como se evidencia en el siguiente ejemplo. Vemos que con la comparación simple el Array se coerciona a String como aplicándole arr.toString(), y ahí ambas variables son iguales:

let str = "1,2";
let arr = [1, 2];
console.log(arr.toString()) // "1,2"
console.log(str == arr); // true
console.log(str === arr); // false
    

Para probar el símbolo [Symbol.toPrimitive] primero declaremos un constructor y modifiquemos el símbolo con una función y un argumento hint necesario para funcionar la conversión a tipo primitivo. JavaScript envía ese argumento con los valores "string", "number" o "default". Según cada valor devolveremos un resultado:

function MiConstructor(valor){
    this.valor = valor;
}
MiConstructor.prototype[Symbol.toPrimitive] = function(hint){
    console.log("hint:" + hint);
    if (hint === "string"){
        return this.valor + " es un String";
    } else if (hint === "number"){        
        return this.valor;
    } else {
        return this.valor + " y ";
    }
};
    

A continuación creamos una instancia del constructor y comprobamos el objeto devuelto:

//Componemos un objeto con un valor String que podría
//coercionar a un número 123 válido
let instancia = new MiConstructor("123");
console.log(instancia); // MiConstructor {valor: "123"} 
    

Por último haremos algunas pruebas. Cuando JavaScript ejecuta String(arg) es que va a convertir su argumento a String. Así que JavaScript tendrá que coercionar el objeto instancia a un String por lo que necesitará usar el método Symbol.toPrimitive(hint) que hemos modificado, siendo ahora hint = "string". Observe que si no hubiésemos modificado el símbolo tendríamos "[object Object]" pues JavaScript representa un objeto como String con [object Object].

//hint: string
console.log(String(instancia)); // "123 es un String"
//Sin modificar el símbolo hubiésemos obtenido: // "[object Object]"
    

Pero si hacemos una operación de multiplicación JavaScrit necesitará dos números, por lo que hint = "number". En la operación coercionará la instancia a un Number por medio de nuestro método Symbol.toPrimitive("number"). Si no hubiésemos modificado el símbolo la operación no sería posible pues no puede coercionar un objeto a un número:

//hint: number
console.log(instancia * 2); // 246
//Sin modificar el símbolo hubiésemos obtenido: // NaN
    

En otros casos como cuando usamos el operador "+" o el comparador "==" tendremos hint = "default". El objeto instancia coercionará con el método Symbol.toPrimitive("default") que nos devuelve el String "123 y ":

//hint: default
console.log(instancia + "abc"); // "123 y abc"
//Sin modificar el símbolo hubiésemos obtenido: // "[object Object]abc"
console.log(instancia == "123 y "); // true
//Sin modificar el símbolo hubiésemos obtenido: // false
    

Observe que si no hubiésemos modificado el símbolo la comparación instancia == "123 y " resulta en "[object Object]" == "123 y " siendo por lo tanto falsa.

Símbolo bien conocido Symbol.isConcatSpreadable

El método concat() de Array acepta como argumentos otros Arrays cuyos elementos serán agregados al final del Array sobre el que se concatena. También los argumentos podrían ser elementos sueltos y una combinación de ambos:

let arr = [1, 2];
//concatenamos elementos de un array
console.log(arr.concat([3, 4])); // [1, 2, 3, 4]
//concatenamos elementos sueltos
console.log(arr.concat(3, 4)); // [1, 2, 3, 4]
//concatenamos elementos sueltos y en arrays
console.log(arr.concat(3, [4, 5], 6)); // [1, 2, 3, 4, 5, 6]
    

En el último caso quizás nos interese que no extienda la concatenación de los elementos del Array [4, 5], sino que lo concatene como un Array. Para conseguirlo modificamos el comportamiento poniendo [Symbol.isConcatSpreadable] con valor falso:

let arr = [1, 2];
let arr2 = [3, 4];
arr2[Symbol.isConcatSpreadable]  = false;
console.log(arr.concat(arr2, 5)); // [1, 2, [3, 4], 5]
    

De hecho el símbolo Symbol.isConcatSpreadable puede aplicarse a cualquier objeto sin ser un Array. Sólo basta que tenga claves numéricas y una propiedad length. Es entonces algo como un Array pues tiene una estructura equivalente. En el siguiente ejemplo si concatenamos un objeto a un array lo agregará como último elemento. Pero si hacemos que extienda la concatenación extraerá los elementos con clave numérica del objeto y los concatenará al Array:

let obj = {0: "b", 1: "c", length: 2};
console.log(obj); // Object {0: "b", 1: "c", length: 2}
//Concatenamos el objeto
console.log(["a"].concat(obj)); // ["a", {0: "b", 1: "c", length: 2}]
//O hacemos que extienda la concatenación a los elementos del objeto
obj[Symbol.isConcatSpreadable] = true;
console.log(["a"].concat(obj)); // ["a", "b", "c"]
    

Las claves deben ser consecutivas empezando en cero. Las no numéricas son ignoradas, como la clave "key" del siguiente ejemplo:

let obj = {0: "b", key: "x", 1: "c", length: 2};
obj[Symbol.isConcatSpreadable] = true;
console.log(["a"].concat(obj)); // ["a", "b", "c"]
    

Se extraerá hasta la longitud indicada en length. En este ejemplo hay tres claves y se indican sólo dos, que son las que se extraen:

let obj = {0: "b", 1: "c", 2: "d", length: 2};
obj[Symbol.isConcatSpreadable] = true;
console.log(["a"].concat(obj)); // ["a", "b", "c"]
    

Si la longitud indicada es mayor que el número de claves disponibles no cursará error y recupera las que hayan. En este ejemplo se indica una longitud de tres pero sólo hay dos claves que se recuperan:

let obj = {0: "b", 1: "c", length: 3};
obj[Symbol.isConcatSpreadable] = true;
console.log(["a"].concat(obj)); // ["a", "b", "c"]
    

Símbolo bien conocido Symbol.unscopables

La sentencia with nos permite acortar las referencias a un objeto. En el siguiente código tenemos un objeto con dos propiedades que son Arrays. Podemos concatenar el segundo al primero haciendo referencia a obj.prop.numeros y obj.prop.letras. Usando un bloque with podemos obviar la referencia obj.prop puesto que numeros y letras se referencian en el indicador del with.

let obj = {
    prop: {numeros: [1, 2], letras: ["a", "b"]}
}
console.log(obj.prop.numeros.concat(obj.prop.letras)); // [1, 2, "a", "b"]
with (obj.prop){
    console.log(numeros.concat(letras)); // [1, 2, "a", "b"]
}
    

El bloque with no está permitido en modo estricto y es algo que tendrá que desaparecer porque causa muchos problemas. Véase que si la finalidad es escribir menos, para acortar una cadena de referencias basta con crear otra intermedia:

let obj = {
    prop: {numeros: [1, 2], letras: ["a", "b"]}
}
//Con un acortador conseguimos ahorrarnos código
let x = obj.prop;
console.log(x.numeros.concat(x.letras)); // [1, 2, "a", "b"]
    

Con el símbolo Symbol.unscopables podemos forzar que las propiedades no sea vistas por el alcance de un bloque with. En el siguiente ejemplo tenemos un constructor con una propiedad conScope que se comporta por defecto con alcance en el with y otra sinScope a la que se lo negaremos. Para ello asignamos un objeto con las propiedades a las que se les niega el alcance (true) o se les permite (false). Repetimos que por defecto no se les niega el alcance con objeto de permitir la compatibilidad con código existente, por lo que podríamos omitir conScope: false.

function MiConstructor(){
    this.conScope = 123;
    this.sinScope = 456;
}
MiConstructor.prototype[Symbol.unscopables] = {
    conScope: false, 
    sinScope: true
};
let obj = new MiConstructor();
with (obj){
    console.log(conScope); // 123
    console.log(sinScope); // ReferenceError: sinScope is not defined
}
    

Símbolo bien conocido Symbol.species

El símbolo bien conocido Symbol.species nos permite definir el constructor que se usará al crear objetos derivados. Es más o menos lo que expone la especificación EcmaScript 6: Well-Known Symbols. Aún no he visto a fondo el tema de las nuevas clases de ES6, pero creo que debo entender objeto derivado como un objeto de una clase que hereda de otra clase. El problema con Symbol.species es que no tiene gran soporte en los navegadores actuales que puedo consultar Chrome 50 ni Firefox 46. La previsión es que Chrome 51 y Firefox 49 lo soporten (ver soportes en Kangak ES6 table).

Intentaré de todas formas exponer un ejemplo. En el siguiente código tenemos la clase MiArray que hereda de Array al declararlo con extends. Como ilustrativo le incorporamos un método que suma todos los elementos del Array. Es por tanto una forma práctica de incorporar nuevos métodos al built-in Array.

class MiArray extends Array {
    sumar(){
        return this.reduce((x, y) => x+y);
    }
}
let arr = new MiArray(1, 2);
console.log(arr); // [1, 2]
//Hereda métodos de Array
console.log(arr.join("X")); // "1X2"
//Hereda métodos de MiArray
console.log(arr.sumar()); // 3
//Es por tanto instancia de ambos
console.log(arr instanceof MiArray); // true
console.log(arr instanceof Array); // true
    

En la ejecución anterior tanto en Chrome 50 como en Firefox 46 vemos que la variable instanciada arr es a la vez una instancia de MiArray y de Array. Hasta aquí todo va como se espera.

Cuando a un objeto MiArray derivado de Array le aplicamos los métodos de Array que devuelven a su vez un Array, el devuelto debería ser una instancia de MiArray y no de Array. Esos métodos son concat(), filter(), map(), slice() y splice(). Veamos esto para concat():

//Obtenemos un nuevo array concatenando a la instancia anterior
let conc = arr.concat([3, 4]);
console.log(conc); // [1, 2, 3, 4]
//El concatenado debería ser instancia de MiArray...
console.log(conc instanceof MiArray); // true (CH50+FF46: false)
//...pero no de Array
console.log(conc instanceof Array); // false (CH50+FF46: true)
    

Se observa que en Chrome 50 y Firefox 46 el array devuelto por concat() no es instancia de MiArray y si lo es de Array. Esto no es lo que se espera, pues aunque el método concat() es de Array, su devolución debería ser del mismo tipo que la instancia arr sobre la que se aplica.

Aclaración (30 Junio 2016).

En Chrome 51 ahora obtenemos que el concatenado es a la vez instancia de Array y de la clase MiArray. En Firefox 47 da el mismo resultado que en la versión 46. El resultado que debería esperarse es que el concatenado fuera instancia de MiArray pero no de Array (true y false).

En cualquier caso si estos navegadores se hubiesen comportado como se espera y quisiéramos que el nuevo Array resultante de concat() fuera instancia de Array y no de MiArray tendríamos que usar el símbolo [Symbol.species] tal como sigue:

class MiArray extends Array {
    //Sobrescribe a Array 
    static get [Symbol.species]() {
        //Agregado esto para ver si el Navegador usa
        //esta modificación del símbolo
        console.log("OK");
        return Array;
    }
    //Método
    sumar(){
        return this.reduce((x,y) => x+y);
    }
}
let arr = new MiArray(1, 2);
let conc = arr.concat([3, 4]);
//Ahora el concatenado no debería ser instancia de MiArray...
//CH51: "OK" y false
//FF47: false
console.log(conc instanceof MiArray); // false (CH50+FF46: false)
//CH51: true
//FF47: true
console.log(conc instanceof Array); // true (CH50+FF46: true)
    

Casualmente el resultado esperado es ahora igual que el que nos da Chrome 50 y Firefox 46. De hecho el resultado sigue siendo el mismo. Pero esto no quiere decir que la sobrescritura de [Symbol.species] se haya llevado a cabo en esos navegadores. Esperaremos a las nuevas versiones para ver si este ejemplo sigue comportándose igual.

Aclaración (30 Junio 2016).

A efectos de saber si el navegador lleva a cabo la ejecución del símbolo modificado he agregado un "OK" para que salga por la consola cuando se ejecute. Chrome 51 lo ejecuta pero Firefox 47 no.

Símbolo bien conocido Symbol.iterator

Symbol-iterator para un Array (ES6)
Figura. Encontramos el símbolo bien conocido Symbol.iterator en un Array.

Un objeto es iterable si tiene el método Symbol.iterator. Este es un método iterador que sirve para obtener sus elementos en un bucle for-of, entre otros usos. Entre los objetos built-in de JavaScript que son iterables encontramos String, Array (ver Figura), Set y Map así como los encuadrados en TypedArray. No son iterables los WeakSet o WeakMap.

Es importante no olvidar que los objetos Object no son iterables. En la ejecución del siguiente código vemos que [Symbol.iterator] para un String o un Array es una función, mientras que no existe para un objeto.

//Un String es iterable
let str = "abc";
console.log(typeof str[Symbol.iterator]); // "function"
//Un Array es iterable
let arr = [1, 2];
console.log(typeof arr[Symbol.iterator]); // "function"
//Un Object no es iterable
let obj = {a: 1};
console.log(typeof obj[Symbol.iterator]); // "undefined"
    

Podemos hacer iterable un objeto si las claves de las propiedades son numéricas, le dotamos de una propiedad length y del método [Symbol.iterator]:

let obj = {
    0: "a", 
    1: "b",
    length: 2,
    [Symbol.iterator]: function* () {
        let index = 0;
        while (index < this.length) yield this[index++];
    }
};
console.log(typeof obj[Symbol.iterator]); // "function"
    

El método iterador anterior se construye con un generador (o función generadora), de tal forma que en cada ejecución devolverá el valor yield, deteniéndose la ejecución en ese punto hasta la siguiente llamada al método. Y así consecutivamente hasta finalizar todos los elementos. Sobre el objeto iterable anterior con ese método iterador podemos aplicar bucles For-of, operador propagación de Array, Destructuring, el método Array.from() y otras operaciones.

//Iterables y bucles for-of
for (let x of obj){
    console.log(x); // "a"
                    // "b"
}

//Iterables y Operador propagación
console.log([...obj]); // ["a", "b"]

//Iterables y Destructuring
let [x, y] = obj;
console.log(x); // "a"
console.log(y); // "b"

//Iterables y Array.from()
let z = Array.from(obj);
console.log(z); // ["a", "b"]
    

La ejecución de una función generadora y, en general, de un método [Symbol.iterator] devuelven un objeto iterador. Se trata de un objeto interno en el que no vamos a manipular su contenido, sino hacer uso de alguno de sus métodos. Como next() que sirve para ir obteniendo los elementos del objeto iterable. Mientras hayan elementos devuelve un objeto como {value: "a", done: false}. Cuando finalice la iteración devolverá {value: undefined, done: true}. Aplicado al iterable de los ejemplos anteriores resultará lo siguiente:

let iterador = obj[Symbol.iterator]();

//El iterador es un objeto interno que permite la iteración
console.log(typeof iterador); // "object"
console.log(iterador); // {[[GeneratorStatus]]: "suspended", 
                       // [[GeneratorReceiver]]: Object}

//El método next() del iterador nos va extrayendo elementos
//en cada ejecución
console.log(iterador.next()); // {value: "a", done: false}
console.log(iterador.next()); // {value: "b", done: false}
console.log(iterador.next()); // {value: undefined, done: true}
    

Este apartado sobre iterables requeriría de una exposición más extensa, pero creo que con lo anterior ya nos hacemos una idea general del asunto. En el siguiente ejemplo interactivo recopilamos los usos anteriores y otros para diversos objetos iterables.

Ejemplo: Symbol.iterator

Crear un Iterable de tipo
Este ejemplo usa ES6 en modo estricto. Puedes consultar el código JS original de este ejemplo.

Nota sobre la representación de objetos built-in

Un objeto o Array lo representamos textualmente tal como podría declararse literalmente. Un Object como {a: 1}. Un Array como [1, 2]. Pero hasta ahora no existen literales para los nuevos tipos Set, Map y otros. Al extraer los valores en el ejemplo anterior representamos un conjunto envolviéndola entre llaves. Usamos también llaves para el mapa, con las parejas clave-valor como si fuera un Array de dos posiciones (que no lo es).

En la consola de Chrome un conjunto se presenta igual, con {valor1, valor2}. Mientras que un mapa con {clave1 => valor1, clave2 => valor2}. No confundir esa flecha con la de las funciones flecha).

Firefox presenta un conjunto con [valor1, valor2] y un mapa con {clave1: valor1, clave2: valor2}. Pero no debemos olvidar que sólo son representaciones de texto de estructuras de datos, porque los conjuntos o mapas no son ni Array ni Object.