Duck Typing en Javascript Chequeando los tipos de datos

07 Feb 2011

El Duck Typing, en los lenguajes de programación, es una técnica para determinar si un objeto es una instacia de una determinada clase basándose únicamente en los métodos que implementa.

La idea tras este paradigma es sencillo: si el objeto analizado posée un cierto conjunto de propiedades características (o exclusivas) de un objeto determinado, es muy probable que se trate de ese mismo objeto.

«Si parece un pato, anda como un pato y hace ‘cuack’ como los patos, es un pato

Chequeando tipos de datos en Javascript

En Javascript, esta práctica es especialmente intersante dado que hablamos de un lenguaje de tipado blando. Las variables, al no definirse de acuerdo a un tipo específico, tampoco cuentan con métodos nativos precisos que los identifiquen con posterioridad. Una de las herramientas que ofrece el lenguaje para aproximarnos es el comando typeof(), cuya sintaxis es la siguiente:

typeof operand;

O bien:

typeof( operand );

El operador typeof devuelve una cadena indicando el tipo de datos del operando.

La siguiente tabla muestra el valor devuelto por typeof para cada uno de los tipos de datos existentes en Javascript:

Value               Class      Type
-------------------------------------
"foo"               String     string
new String("foo")   String     object
1.2                 Number     number
new Number(1.2)     Number     object
true                Boolean    boolean
new Boolean(true)   Boolean    object
new Date()          Date       object
new Error()         Error      object
[1,2,3]             Array      object
new Array(1, 2, 3)  Array      object
new Function("")    Function   function
/abc/g              RegExp     object (function in Nitro/V8)
new RegExp("meow")  RegExp     object (function in Nitro/V8)
{}                  Object     object
new Object()        Object     object

Como podemos observar, la tabla no es consistente: todos las variables declaradas mediante el comando new y el correspondiente constructor dan como resultado ‘object‘ cuando consultamos su tipo:

console.log(  typeof new String( 'Hello World' ) );
console.log(  typeof new Number( 5 ) );

Sin embargo, resulta aún menos intuitivo en el caso de los arrays, donde la notación literal da también como resultado un tipo de datos ‘object‘:

console.log( typeof [ 1, 2, 3, 4, 5 ] );
console.log( typeof new Array( 1, 2, 3, 4, 5 ) );

En definitiva, typeof no devuelve una información demasiado útil para una cantidad considerable de casos.

Otro operador Javascript al que podemos recurrir para determinar un tipo concreto de datos es instanceof(). Este operador compara el constructor de sus dos operandos.

objectName instanceof objectType

A diferencia de typeof, instanceof es un operador Booleano que evalúa si un objeto dado se corresponde con un tipo de datos concreto devolviendo true en caso afirmativo y false en el contrario.

Instanceof resulta útil cuando lo aplicamos sobre nuestros propios objetos. Usado sobre aquellos predefinidos por Javascript resulta tan poco interesante como typeof:

// Using with custom objects:
function Foo(){}
function Bar(){}
Bar.prototype = Foo;
console.log( new Foo() instanceof Foo ); // true
console.log( new Bar() instanceof Bar ); // true
console.log( new Bar() instanceof Foo ); // false
 
// Using with native types:
console.log( new String( 'foo' ) instanceof String ); // true
console.log( new String( 'foo' ) instanceof Object ); // true
console.log( new Array( 1, 2, 3 ) instanceof Array ); // true
console.log( new Array( 1, 2, 3 ) instanceof Object ); // true
 
console.log( 'foo' instanceof String ); // false
console.log( 'foo' instanceof Object ); // false
console.log( [ 1, 2, 3, 4 ] instanceof Array ); // true
console.log( [ 1, 2, 3, 4 ] instanceof Object ); // true

NOTA: Siguiendo las buenas prácticas Javascript, los anteriores constructores Foo y Bar comienzan con mayúscula para distinguirlos rápidamente del resto de funciones.

Tal y como nos recuerda Kangax de Perfection Kills, tanto typeof como instanceof ofrecen una comprobación muy inocente para casos concretos como el chequeo de un array: tanto en su notación literal como utilizando el constructor Array, obtenemos que es a la vez instancia de Object y de Array.

Algunas de las librerías Javacript más utilizadas, implementan la funcion isArray. Por ejemplo, la versión 1.5 de Dojo lo hace de la siguiente forma:

function isArray( obj ){
  return obj && (obj instanceof Array || typeof obj == "array");
}

Por su parte, tanto la versión 1.7 de Prototype y la 1.1.4 de Underscore.js utilizan una técnica similar:

function isArray( obj ){
  return toString.call( obj ) === '[object Array]';
}

Mientras que Dojo opta por evaluar el objeto utilizando tanto instanceof como typeof, Prototype y Undescore proponen una comparación mediante el método toString aplicado junto a la API call al objeto. El resultado de éste último método es más robusto que el anterior pero no deja de resultar poco elegante.

Inconvenientes de las funciones nativas

El mayor inconveniente de uilizar instanceof para determinar un tipo de datos llega cuando encontramos un documento compuestos por marcos (frames). En estos casos de múltiples entornos DOM, un objeto creado dentro de un marco no comparte prototipo con los mismos elementos creados en otro. Sus constructores son objetos diferentes y es entonces cuando instanceof falla.

Tal y como explicaba Crockford, cuando hablamos de un ‘Array‘, estamos refiriéndonos a ‘window.Array‘. ‘window‘ es el objeto que define el contexto en el navegador y tenemos uno por cada página o frame. Es por eso que, pese a que ECMAScript es capaz de manejar múltiples contextos en la práctica, el estándar no lo soporta en su definición. Cuando creamos objetos semejantes en contextos diferentes, no comparten el constructor y, por tanto, no disponemos de una técnica infalible para testear su tipo de datos:

(Ejemplo extraído de Perfection Kills)

var iframe = document.createElement( 'iframe' );
document.body.appendChild( iframe );
xArray = window.frames[window.frames.length-1].Array;
var arr = new xArray(1,2,3); // [1,2,3]  
 
// Boom!
arr instanceof Array; // false  
 
// Boom!
arr.constructor === Array; // false

Es en este escenario, donde debemos recurrir a técnicas menos estrictas como el Typing Duck.

Chequeando tipos por aproximación (Duck-Typing)

Como se puede deducir, por la definición del principio, el Typing Dust no es de hecho una práctica robusta e infalible: se basa en una estimación subjetiva de si el objeto es de un tipo u otro según las propiedades y métodos que lo compongan. Sin embargo, en Javascript, puede ser una herramienta útil para determinar tanto el tipo de datos de un objeto concreto hasta para facilitar una pseudo implementación de interfaces cuando programamos en POO puro.

Para el caso de los arrays, una comprobación de tipos por este sistema, incluiría el chequear la presencia de métodos nativos como push(), join() o splice().

Una aproximación válida sería:

function isArray( obj ){
  return obj ? !!obj.push : false;
}

En el ejemplo anterior, se comprueba si existe el método push (nativo del objeto Array) mediante la conversión booleana de la doble admiración. Si el objeto evaluado es un array, devolverá true, mientras que si carece de ese método, devolverá false.

Otra forma de escribir el mismo código sería:

function isArray( obj ){
  return obj && typeof obj === "object" && "push" in obj;
}
 
console.log( isArray( [ 1, 2, 3 ) ) );
console.log( isArray( new Array( 1, 2, 3 ) ) );

En este caso volvemos a chequear la presencia del método push en el objeto a evaluar devolvíendo true o false en consecuencia. En esta ocasión conviene recordar como Javascript evalúa los condicionales por orden de izquierda a derecha siguiendo el sistema booleano lógico en cortocircuito.

Con la función anterior, sólo hemos buscado la presencia del método push, una comprobación suficientemente estricta para la mayoría de los casos. Si precisáramos de un mayor control sobre el objeto, incluiríamos tantas evaluaciones como considerásemos necesarias en la función:

// Paranoic mode
function isArray( obj ){
  return obj &&
  typeof obj === "object" &&
  "splice" in obj &&
  "join" in obj &&
  "push" in obj;
}
 
console.log( isArray( [ 1, 2, 3 ) ) ); // true
console.log( isArray( new Array( 1, 2, 3 ) ) ); // true

Conclusión

En Javascript, comprobar el tipo de datos de un objeto dado puede resultar complicado. Ya que estamos hablando de un lenguaje de tipado blando, las variables se declaran sin un tipo concreto. Comandos nativos como typeof, instanceof o constructor, no resultan especialmente útiles cuando trabajamos con entornos múltiples, haciendo necesarias otras comprobaciones de granularidad más fina. Las soluciones aportadas en algunas librerías como Prototype o jQuery suponen herramientas útiles para este respecto.

Sin embargo, contamos también con la posibilidad de recurrir a las técnicas del Duck-Typing que bien pueden echarnos una mano para la identificación: buscando una serie de métodos exclusivos de determinados tipos, podemos concluir que nuestro objeto pertenece a uno u otro. Es un tipo de prueba basado en el ensayo y error, que además tiene un coste en términos de rendimiento. Sin embargo, aunque no resulten sobre la teoría todo lo ortodoxos que buscan muchos puristas, en la práctica funcionan correctamente, por lo que podemos hablar casi de un patrón de diseño definido y seguro.

Más información:

Kangax, ‘instanceof’ considered harmful (or how to write a robust ‘isArray’)
Matthias Reuter, All about types in Javascript – Type detection
Vantage Point of Queens, Duck Typing on JavaScript, and getter/setter

Más:

{7} Comentarios.

  1. joseanpg

    Buen artículo. Me gustaría realizar un pequeño comentario respecto a la implementación de isArray basada en [[Class]]:

    function isArray( obj ){
      return toString.call( obj ) === '[object Array]';
    }
    

    Esa función

    toString

    podría despistar a algún lector. Tal vez sería conveniente aclarar que en algún lugar debemos tener la siguiente asignación:

    var toString = Object.prototype.toString
    
    • Carlos Benítez

      Hola JoseAnpg!
      No es necesario definir toString previamente.

      Te comento: toString() es un método nativo que convierte un valor booleano en un string. La documentación, la encontramos en:

      http://www.w3schools.com/jsref/jsref_toString_boolean.asp

      Por lo tanto, podemos aplicar ‘call’ directamente sobre él y obtener el funcionamiento que hemos descrito. Digamos que la ‘magia’ de este método es precisamente el uso del API ‘call’.

      Puedes comprobarlo en una consola JS si la tienes a mano.

      De todos modos, tu implementación sería correcta ya que estaríamos cacheando el prototipo directamente, pero insisto en que no es necesario.

      Un saludo!

  2. joseanpg

    El comportamiento de esa versión de isArray se basa en la propiedad toString de Object.prototype. A continuación puedes ver su especificación ECMAScrip (versión 5):

    15.2.4.2 Object.prototype.toString ( ) 
    When the toString method is called, the following steps are taken:
    - If the this value is undefined, return "[object Undefined]".
    - If the this value is null, return "[object Null]".
    - Let O be the result of calling ToObject passing the this value as the argument.
    - Let class be the value of the [[Class]] internal property of O.
    - Return the String value that is the result of concatenating the three Strings "[object ", class, and "]".
    

    Si un un objeto no tiene definida una propiedad propia toString, ni ésta se encuentra definida en ninguno de los objetos de su cadena de prototipos distintos de la raíz, se utilizará la propiedad anterior, la de Object.prototype. Pero lo más probable es que si usamos objetos prefabricados tengamos definido un toString en algún prototipo cercano.

    Si invocamos toString.call sin especificar propietario acabaremos utlizando el toString del objeto global, que en el caso de los navegadores es el objeto window. Dicho objeto tiene un toString propio en su prototipo cuya funcionalidad dependerá de la implementación.

    • Carlos Benítez

      Tienes toda la razón. Estoy dando por hecho de que todas las implementaciones de toString() son idénticas en cada uno de los navegadores.

      Me corrijo a mi mismo y actualizo el post.

      Gracias joseanpg!

  3. joseanpg

    Por cierto, en el código fuente de Underscore.js 1.1.4 puedes ver lo siguiente:

      var ArrayProto = Array.prototype, ObjProto = Object.prototype;
    
      // Create quick reference variables for speed access to core prototypes.
      var slice     = ArrayProto.slice,
          unshift   = ArrayProto.unshift,
          toString = ObjProto.toString,
    
      ...
    
      // Is a given value an array?
      // Delegates to ECMA5's native Array.isArray
         _.isArray = nativeIsArray || function(obj) {
              return toString.call(obj) === '[object Array]';
         };
    

    Por otra parte este es un patrón clásico que, por ejemplo, se describe en la página 48 del
    JavaScript Patterns de Stoyan Stefanov.

    Un saludo también 😉

Deja un comentario

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