Material‎ > ‎Software‎ > ‎Angular JS‎ > ‎

AngularJS - Scope

Tipos de alcance

En Javascript las variables tienen dos tipos de alcance o scope, que define la posibilidad de acceder a ellas 
  • en forma global
  • en forma local, en el contexto de ejecución de una función (un closure)

Variables globales

Las primeras se definen fuera del alcance de su función
var cliente;
function xxxx() {
   cliente = new Cliente();  <== cliente es de acceso global
}

Variables locales

Las segundas se definen en una función o closure y son accesibles dentro de ese contexto
function xxxx() {
  var cliente = new Cliente(); <== cliente está definida localmente dentro de la función xxxx
}

El tema es que si la variable cliente no existe, se crea con scope global, y si ya hay una referencia se pisa el valor en el contexto actual:
function xxxx() {
  cliente = new Cliente(); <== si no existe se crea una variable cliente de scope global
}

Modo estricto
Esto a menos de que definamos el modo estricto en cada una de las funciones:
'use strict';
en cuyo caso aparecerá un mensaje de error:
ReferenceError: cliente is not defined

Juego de variables en un closure

A su vez, una función o closure puede definir su propio juego de variables, dentro del entorno local, pero incluir otro closure con su propio juego de variables:
var numero = 20;
alert(sumarUno(3));  --> devuelve 23, claro

function sumarUno(otroNumero) {
   function sumar() {
          return numero + otroNumero;
   }
   var resultado = sumar();
   return resultado;
}

En este ejemplo tenemos variables de diferente scope:
  • numero: global
  • otroNumero: un parámetro de scope local a la función sumarUno
    • que es accesible en el closure sumar() porque está dentro del scope del closure sumarUno
  • resultado: una variable de scope local dentro de la funciòn sumarUno

El problema de this

Cuando trabajamos con objetos javascript, el uso de this puede resultar confuso, si creemos que guarda una referencia al objeto "actual". En realidad esto no siempre se cumple...

Tenemos que resolver el siguiente requerimiento: "Saber si un auto fue bien tratado, esto implica que todos sus dueños lo trataron bien.
Un dueño trata bien al auto si el dueño tiene más de 40 años o el auto es de marca Lamborghini"
function Duenio(nombre, edad) {
    this.nombre = nombre;
    this.edad = edad;
    
    this.trataBienAlAuto = function(marca) {
        return marca == "Lamborghini" || edad > 40;
    }
}

function Auto(anio, marca, duenio) {
    this.anio = anio;
    this.marca = marca;
    this.duenios = [duenio];

    this.transferir = function(nuevoDuenio) {
        this.duenios.push(nuevoDuenio);
    };
    
    this.fueBienTratado = function() {
        return this.duenios.every(function(duenio) {
            return duenio.trataBienAlAuto(this.marca);
        });
    };
}
En el contexto de la definición del método fueBienTratado del objeto Auto, estamos usando otro closure, el que le pasamos a every. Dentro de esa función this no está definido, por lo que debemos trabajar la referencia a this con una variable local fuera del contexto del closure:
function Auto(anio, marca, duenio) {
    this.anio = anio;
    this.marca = marca;
    this.duenios = [duenio];

    var self = this;
    
    this.transferir = function(nuevoDuenio) {
        this.duenios.push(nuevoDuenio);
    };
    
    this.fueBienTratado = function() {
        return this.duenios.every(function(duenio) {
            return duenio.trataBienAlAuto(self.marca);
        });
    };
}
Ahora sí, podremos saber si el auto viejo y el nuevo fueron bien tratados:
var juan = new Duenio("Juan Contardo", 29);
var fer = new Duenio("Fernando Dodino", 42);
var dodge = new Auto(1979, 140000, fer);
var renoleta = new Auto(2016, 0, juan);
dodge.fueBienTratado() ==> "true" 
dodge.transferir(juan); 
dodge.fueBienTratado() ==> "false"
renoleta.fueBienTratado() ==> "true"

Las variables de Angular y sus contextos
Esta forma laxa de chequeo que utiliza javascript sumada al esquema de jerarquías de los contextos en Angular suele causar gran parte de las confusiones en las aplicaciones, ya que
  • la application engloba todos sus controllers
  • un controller engloba todas sus directivas y filtros
  • y los filtros y directivas pueden llegar a compartir -sin quererlo- los mismos nombres de variables. Este efecto indeseado de alterar los valores de las variables fuera del contexto en el que fueron pensados se llama efecto colateral.
  • además los pedidos del cliente hacia el servidor son todos asincrónicos, lo que obliga a definir callbacks (closures) para manejar las operaciones posteriores a cada llamada, además del manejo de errores

Uso de controllers en la vista

Si no utilizamos la sintaxis "Controller As", entender qué juego de variables está en uso puede resultar confuso:

angular.module('myApp', [])
.controller("pedidoController", ["$scope", function($scope) {
  $scope.total = 200;
}])
.controller("productoController", ["$scope", function($scope) {
  $scope.total = 30;
}]);

En la vista tenemos la misma directiva {{total}}:
<div ng-app="myApp">
    <div class="widget" ng-controller="pedidoController">
        <p>{{total}}</p>
        <div class="menu" ng-controller="productoController">
            <p>{{total}}</p>
        </div>
    </div>
</div>

Para evitar confusiones, utilizamos la sintaxis "controller as" en la vista:

<div ng-app="myApp">
    <div class="widget" ng-controller="pedidoController as pedidoCtrl">
        <p>{{pedidoCtrl.total}}</p>
        <div class="menu" ng-controller="productoController as productoCtrl">
            <p>{{productoCtrl.total}}</p>
        </div>
    </div>
</div>

Eso necesita que los controllers devuelvan objetos, en lugar de usar al $scope como una gran variable compartida:

angular.module('myApp', [])
.controller("pedidoController", function() {
  return {
    total: 200
  }
})
.controller("productoController", function() {
  return {
    total: 30
  }
});

Llamada al service

En el siguiente ejemplo tenemos un controller que hace la consulta de las tareas a un objeto service de Angular, pasándole un callback que termina de generar el binding con la vista...

app.controller('TareasController', function(tareasService) {

    this.getTareas = function() {
        tareasService.findAll(function(data) {
            this.tareas = data;
        });
    }   

    this.getTareas();
});

Pero la línea subrayada no va a funcionar como queremos. Para asignar en la variable tareas del controller debemos utilizar una referencia local:

app.controller('TareasController', function(tareasService) {
    var self = this;

    this.getTareas = function() {
        tareasService.findAll(function(data) {
            self.tareas = data;
        });
    }   

    this.getTareas();
});

Links relacionados
Comments