Aprendiendo Javascript (Scopes)

2015-08-25

En estas últimas semanas he estado aprendiendo JavaScript. Seguramente escribiré un post más largo sobre mis experiencias y opiniones del lenguaje, no obstante, este irá dedicado concretamente a los scopes de JavaScript. Esta semana resolviendo los Koans de JavaScript propuestos por Carlos he encontrado casos que realmente te hacen pensar e investigar acerca de los scopes de JavaScript. La verdad que estos Koans te hacen entender mejor el lenguaje. Personalmente después de estas semanas con JavaScript he de admitir que me está gustando mucho el lenguaje. Sin más, empezaré con uno de los ejemplos propuestos por Carlos:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
it("hoists variables the way you probably dont expect", function(){
function generate(){
var functions = [];
for (var i = 0; i < 5; i++){
functions.push(function(){
return i;
});
}
return functions;
}
expect(generate()[0]()).toEqual(5);
expect(generate()[1]()).toEqual(5);
});

En un principio este era bastante impactante, lo que pensé que tenía que pasar era que el primer assert debería ser 0 y el segundo 1. ¿Por qué no es así? Porque el scope al que pertenece la variable “i” es a la función “generate” por lo tanto el valor de esa “i” será el valor que la “i” tenga en su contexto. Al acabar el bucle esta “i” valdrá 5 y por lo tanto ese valor será el que se almacene en todas las functions que insertemos en nuestro array.

Este ejemplo nos hizo pensar a mi compañero Miguel y a mí como se haría lo mismo pero con el comportamiento que esperábamos fuera el que esperábamos en un principio. Es decir, que realmente las funciones insertadas devuelvan cada una los valores del 0 al 4. Este es el código al que llegamos para conseguirlo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
it("hoists variables the way you probably dont expect", function(){
function generate(){
var functions = [];
for (var i = 0; i < 5; i++){
(function(num) {
functions.push(function(){
return num;
});
})(i);
}
return functions;
}
expect(generate()[0]()).toEqual(0);
expect(generate()[1]()).toEqual(1);
});

Básicamente lo que hacemos es crearnos una función anónima auto-ejecutable que recibe un número, en este caso la “i”. Hacemos esto para crear una copia del valor de “i” en un determinado momento en el scope de la nueva función, por lo tanto, cuando ahora le decimos “return num”, ese “num” pertenece a la función auto-ejecutable y su valor será el que tiene en esta última. Lo más importante aquí es entender que en JavaScript el scope lo crean las funciones.

El segundo ejemplo que quería escribir está relacionado también con el scope. Más concretamente con el contexto en que una función se ejecuta. Veamos el ejemplo para luego poder reflexionar:

// "Class"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function Cat(){
this.kilos = 1;
this.feed = function(){
this.kilos++;
};
this.isPurring = function(){
return true;
};
}
it("works different on dettached functions", function(){
window.kilos = 10;
cat = new Cat();
var feed = cat.feed;
feed();
expect(window.kilos).toEqual(11); // WHY?
expect(cat.kilos).toEqual(1);
});

Tenemos una clase Cat y luego en el test almacenamos en una variable “feed” la función “cat.feed”. Si nos fijamos cuando ejecutamos esta función en lugar de afectarle a nuestro objeto cat, le afecta a una variable global que hemos llamado kilos. Lo que está pasando es que en JavaScript el scope se define en la ejecución. Al almacenar en una variable la función estamos cambiando su scope. Donde hacemos la asignación esto sería lo que está pasando:

1
2
3
var feed = function(){
this.kilos++;
};

Se está almacenando la función tal cual está en la clase Cat. Pero al ejecutarla no se le está asignando ningún contexto por lo que JavaScript por defecto ejecutará esta función en el scope global. Por esto cuando haces this.kilos++ this es equivalente a “window”. Como el scope se asigna cuando se ejecuta si quisiéramos que una función no se ejecute en el ámbito global debemos especificar en qué ámbito queremos que lo haga. Por ejemplo:

1
2
3
4
5
6
7
8
9
10
11
it("works different when function is attached to other object", function(){
window.kilos = 10;
this.kilos = 5;
var cat = new Cat();
this.feed = cat.feed;
this.feed();
expect(window.kilos).toEqual(10);
expect(this.kilos).toEqual(6); // Modified
expect(cat.kilos).toEqual(1);
});

Vemos que ahora sí que estamos asignando un scope. Estamos diciendo que esta function en lugar de ser global pertenece a la “function actual” (this). Por lo que cuando se ejecute y llegue a la línea “this.kilos++” buscará una variable kilos definida en el scope de la “function actual”. En nuestro ejemplo dicha variable kilos es la que está en la línea 3. Por ello finalmente esta variable si se modificará mientras que la global y la perteneciente a cat no.

Otra forma bastante sencilla de cambiar el contexto de la función sería esta:

1
2
3
4
5
6
7
8
9
10
it("can be bound in modern browsers with BIND", function(){
this.kilos = 5;
var cat = new Cat();
var bound = cat.feed.bind(this); // changing the scope for feed()
bound();
expect(this.kilos).toEqual(6);
expect(cat.kilos).toEqual(1);
});

La función bind recibirá por parámetro el scope en el cual quieres que se ejecute la función. En este caso como queremos que lo que se modifique sea el “kilos” de la perteneciente y no al objeto Cats ni alguna variable global, pues le pasamos el this (es decir, que el contexto ahora sería la función del test).

Bueno así es cómo yo he entendido el tema de los scopes en JavaScript, estas primeras semanas con JavaScript están siendo muy productivas. Todavía me queda mucho por aprender del lenguaje pero creo que haber entendido esto es un gran paso, lo que al principio era un WAT ahora tiene más sentido. Si alguien detecta que algo no lo he entendido bien o lo he explicado mal, le estaría muy agradecido de que lo comentara para seguir aprendiendo del lenguaje.