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.
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
Buen artículo. Me gustaría realizar un pequeño comentario respecto a la implementación de
isArray
basada en[[Class]]
:Esa función
podría despistar a algún lector. Tal vez sería conveniente aclarar que en algún lugar debemos tener la siguiente asignación:
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!
El comportamiento de esa versión de
isArray
se basa en la propiedadtoString
deObject.prototype
. A continuación puedes ver su especificación ECMAScrip (versión 5):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 deObject.prototype
. Pero lo más probable es que si usamos objetos prefabricados tengamos definido untoString
en algún prototipo cercano.Si invocamos
toString.call
sin especificar propietario acabaremos utlizando eltoString
del objeto global, que en el caso de los navegadores es el objetowindow
. Dicho objeto tiene untoString
propio en su prototipo cuya funcionalidad dependerá de la implementación.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!
Por cierto, en el código fuente de Underscore.js 1.1.4 puedes ver lo siguiente:
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 😉