This page looks best with JavaScript enabled

La dualidad objeto-función en JavaScript

 ·  ☕ 5 min  ·  ✍️ eiximenis

Nota: Este post ha sido importado de mi blog de geeks.ms. Es posible que algo no se vea del todo "correctamente". En cualquier caso puedes acceder a la versión original aquí

No sé si Brendan Eich es un amante de la física cuántica, pero al menos viendo algunas de las características, así lo parece. No solo tenemos el principio de incertidumbre de this si no que también tenemos el hecho de que en JavaScript un objeto puede comportarse como una función y viceversa, es decir una dualidad objeto-función.

Tomemos jQuery p. ej. Por defecto nos define el archiconocido $, con el cual podemos hacer algo como $(“#mydiv”), es decir, usarlo como una función, pero también podemos hacer $.getJSON(“…”), es decir, usarlo como un objeto. A ver como podemos conseguir esa dualidad es a lo que vamos a dedicar este post 🙂

La dualidad ya existente

La verdad… es que JavaScript ya viene con esa dualidad de serie… Todas las funciones de JavaScript se comportan a su vez como objetos (de hecho, en JavaScript las funciones son un tipo específico de objeto). O si no, observa el siguiente código:

  1. var f = function () {
  2.     console.log("i am a function, or an object?");
  3. }
  4.  
  5. console.log(typeof (f));
  6. console.log(f.toString());

El primer console.log, imprime “function”, dejando bien claro que f es una función. Pero luego observa que se está llamando al método toString de la función f. De la función en sí, no del valor devuelto por dicha función (lo que, ya puestos, generaría un error, pues f devuelve undefined). Así pues, las funciones tienen propiedades y métodos. Puede parecer chocante, pero en JavaScript los funciones, son también, objetos.

En JavaScript cada objeto tiene asociado su prototipo que es otro objeto (y dado que el prototipo es un objeto tiene asociado su propio prototipo, lo que genera una cadena de prototipos, que termina en un objeto llamado Object.prototype y que su prototipo es null).

Existe una propiedad, llamada __proto__, que nos permite acceder al prototipo de un objeto en concreto. Cuando creamos un objeto, usando object notation dicha propiedad se asigna a Object.prototype (como puedes comprobar en esa captura de pantalla de las herramientas de desarrollador de Chrome):

image

Las funciones, como objeto que son, tienen todas su propio prototipo que es… Function.prototype, que es por cierto el objeto que define las funciones call, apply y bind, que podemos llamar sobre cualquier función.

image

(Por si lo estás suponiendo, sí: el prototipo de Function.prototype es Object.prototype).

Entender bien la cadena de prototipado es fundamental, pues en ella se basa todo el sistema de herencia de JavaScript.

Ahora que ya vemos que las funciones en realidad son objetos, vamos a ver como podemos hacer como hace jQuery y colocar métodos propios en una función que definamos.

No nos sirve agregar métodos a Function.prototype: si lo hacemos esos métodos van a estar disponibles para todas las funciones y nosotros queremos que dichos métodos estén disponibles solo para una función en concreta (al igual que hace jQuery, que agrega métodos específicos pero solo a la función $).

Creando nuestra propia dualidad

Por extraño que parezca crear nuestra propia dualidad es muy sencillo, basta con el siguiente código:

  1. var f = function (name) {
  2.     return "Hello " + name;
  3. }
  4. f.foo = function () {
  5.     return "foo Utility method";
  6. }

Una vez tenemos esto podemos invocar a la función f, pero también a la función foo de la función f:

<div style="background: #ddd; max-height: 300px; overflow: auto">
  <ol start="1" style="background: #ffffff; margin: 0 0 0 2em; padding: 0 0 0 5px;">
    <li>
      <span style="background:#ffffff;color:#0000ff">var</span><span style="background:#ffffff;color:#000000"> x = f(</span><span style="background:#ffffff;color:#a31515">"edu"</span><span style="background:#ffffff;color:#000000">);</span>
    </li>
    <li>
      <span style="background:#ffffff;color:#0000ff">var</span><span style="background:#ffffff;color:#000000"> x2 = f.foo();</span>
    </li>
  </ol>
</div></p>

Ya tenemos a nuestra variable f, comportándose dualmente como una función y un objeto a la vez.

Por supuesto podemos refinar un poco esa técnica:

  1. var f = function (name) {
  2.     return "Hello " + name;
  3. }
  4. var fn = {
  5.     foo: function () {
  6.         console.log("Utility method");
  7.     }
  8. }
  9. Object.assign(f, fn);

Este código es equivalente al anterior, pero usa Object.assign (método de ES6) para copiar las propiedades contenidas en fn (el método foo) dentro del objeto (función) f.

Usando ES6 es muy fácil crearnos una función factoría que nos permita establecer un objeto con un conjunto de operaciones comunes a una función. Para ello podemos establecer este objeto como prototipo de la función (a su vez este objeto prototipo de la función debe “heredar” de Function.prototype para no perder las funciones que este provee).

  1. var factory = function (p, f) {
  2.     var fp = Object.create(Function.prototype);
  3.     Object.assign(fp, p);
  4.     Object.setPrototypeOf(f, fp);
  5.     return f;
  6. }

De este modo si tenemos un objeto cualquiera y queremos que todas sus operaciones sean heredadas por una función:

  1. var f = function () { return this; };
  2. factory({
  3.     dump: function () {
  4.         console.log('dump...');
  5.     }
  6. }, f);
  7. f.dump();

Ahora, la función f, además de los métodos definidos en Function.prototype (tales como apply o call) tiene también todos los métodos definidos en el objeto pasado como primer parámetro de factory, es decir el método dump.

Fácil y sencillo, ¿no?

Por cierto, ya puestos a divertirnos un poco. Cual es la salida del siguiente código si lo ejecutas en la consola JS del navegador?

  1. var f = function () { return this; };
  2. var obj =
    {
  3.     foo: f
  4. };
  5. factory({
  6.     unbind: function () {
  7.         return this.bind(this);
  8.     }
  9. }, f);
  10. console.log("f()", f());
  11. console.log("f.bind({a:10})()", f.bind({ a: 10 })());
  12. console.log("f.unbind()()", f.unbind()());
  13. console.log("obj.foo()", obj.foo());
  14. console.log("obj.foo.unbind()()", obj.foo.unbind()());

Piénsalo un poco con calma… si aciertas los valores de los 5 console.log eres de los que dominan el principio de incertidumbre de this 😉

Saludos!

Si quieres, puedes invitarme a un café xD

eiximenis
ESCRITO POR
eiximenis
Compulsive Developer