'this' vs $ scope en controladores AngularJS

1027

En la sección "Crear componentes" de la página de inicio de AngularJS , hay este ejemplo:

controller: function($scope, $element) {
  var panes = $scope.panes = [];
  $scope.select = function(pane) {
    angular.forEach(panes, function(pane) {
      pane.selected = false;
    });
    pane.selected = true;
  }
  this.addPane = function(pane) {
    if (panes.length == 0) $scope.select(pane);
    panes.push(pane);
  }
}

Observe cómo selectse agrega el método $scope, pero addPanese agrega el método this. Si lo cambio a $scope.addPane, el código se rompe.

La documentación dice que de hecho hay una diferencia, pero no menciona cuál es la diferencia:

Las versiones anteriores de Angular (pre 1.0 RC) le permitieron usar thisindistintamente con el $scopemétodo, pero este ya no es el caso. Dentro de los métodos definidos en el alcance thisy $scopeson intercambiables (conjuntos angulares thisa $scope), pero no de otra manera dentro del constructor de su controlador.

¿Cómo funciona thisy $scopefunciona en los controladores AngularJS?

Alexei Boronine
fuente
Esto me parece confuso también. Cuando una vista especifica un controlador (por ejemplo, ng-controller = '...'), el $ alcance asociado con ese controlador parece venir junto con él, porque la vista puede acceder a las propiedades de $ alcance. Pero cuando una directiva 'requiere otro controlador (y luego lo usa en su función de vinculación), ¿el $ alcance asociado con ese otro controlador no viene junto con él?
Mark Rajcok
¿Se ha eliminado esa cita confusa sobre "Versiones anteriores ..."? Entonces tal vez la actualización estaría en su lugar?
Dmitri Zaitsev
Para las pruebas unitarias, si usa 'this' en lugar de '$ scope', no puede inyectar el controlador con un alcance simulado, por lo que no puede hacer pruebas unitarias. No creo que sea una buena práctica usar 'esto'.
abentan el

Respuestas:

999

"¿Cómo funciona thisy $scopefunciona en los controladores AngularJS?"

Respuesta corta :

  • this
    • Cuando se llama a la función constructora del controlador, thises el controlador.
    • Cuando $scopese llama a una función definida en un objeto, thises el "alcance vigente cuando se llamó a la función". Esto puede (¡o no!) Ser en el $scopeque se define la función. Entonces, dentro de la función, thisy $scopepuede no ser lo mismo.
  • $scope
    • Cada controlador tiene un $scopeobjeto asociado .
    • Una función de controlador (constructor) es responsable de establecer las propiedades del modelo y las funciones / comportamiento en sus asociados $scope.
    • Solo los métodos definidos en este $scopeobjeto (y los objetos del ámbito principal, si la herencia prototípica está en juego) son accesibles desde la vista / HTML. Por ejemplo, desde ng-click, filtros, etc.

Respuesta larga :

Una función de controlador es una función constructora de JavaScript. Cuando se ejecuta la función del constructor (por ejemplo, cuando se carga una vista), this(es decir, el "contexto de la función") se establece en el objeto controlador. Entonces, en la función del constructor del controlador "pestañas", cuando se crea la función addPane

this.addPane = function(pane) { ... }

se crea en el objeto controlador, no en $ scope. Las vistas no pueden ver la función addPane: solo tienen acceso a las funciones definidas en $ scope. En otras palabras, en el HTML, esto no funcionará:

<a ng-click="addPane(newPane)">won't work</a>

Después de que se ejecuta la función del constructor del controlador "pestañas", tenemos lo siguiente:

función de constructor del controlador después de pestañas

La línea negra discontinua indica herencia prototípica: un alcance aislado hereda prototípicamente de Scope . (No hereda prototípicamente del alcance en efecto donde se encontró la directiva en el HTML).

Ahora, la función de enlace de la directiva de panel quiere comunicarse con la directiva de pestañas (lo que realmente significa que debe afectar a las pestañas aislar $ scope de alguna manera). Se podrían usar eventos, pero otro mecanismo es hacer que el panel dirija requireel controlador de pestañas. (Parece que no hay ningún mecanismo para la directiva de panel para requirelas pestañas $ scope).

Entonces, esto plantea la pregunta: si solo tenemos acceso al controlador de pestañas, ¿cómo podemos acceder a las pestañas aislar $ scope (que es lo que realmente queremos)?

Bueno, la línea punteada roja es la respuesta. El "alcance" de la función addPane () (me refiero al alcance / cierres de la función de JavaScript aquí) le da a la función acceso a las pestañas aislar $ scope. Es decir, addPane () tiene acceso a las "pestañas IsolateScope" en el diagrama anterior debido a un cierre que se creó cuando se definió addPane (). (Si en su lugar definimos addPane () en el objeto de pestañas $ scope, la directiva de panel no tendría acceso a esta función y, por lo tanto, no tendría forma de comunicarse con las pestañas $ scope).

Para responder la otra parte de su pregunta how does $scope work in controllers?:

Dentro de las funciones definidas en $ scope, thisse establece en "el alcance de $ vigente donde / cuando se llamó a la función". Supongamos que tenemos el siguiente HTML:

<div ng-controller="ParentCtrl">
   <a ng-click="logThisAndScope()">log "this" and $scope</a> - parent scope
   <div ng-controller="ChildCtrl">
      <a ng-click="logThisAndScope()">log "this" and $scope</a> - child scope
   </div>
</div>

Y el ParentCtrl(Solamente) tiene

$scope.logThisAndScope = function() {
    console.log(this, $scope)
}

Al hacer clic en el primer enlace se mostrará eso thisy $scopeson lo mismo, ya que " el alcance en vigor cuando se llamó a la función " es el alcance asociado con el ParentCtrl.

Al hacer clic en el segundo enlace se revelará thisy no$scope es lo mismo, ya que " el alcance en vigor cuando se llamó a la función " es el alcance asociado con el ChildCtrl. Así que aquí, thisse establece en ChildCtrl's $scope. Dentro del método, $scopesigue siendo el ParentCtrlalcance de $.

Violín

Intento no usar thisdentro de una función definida en $ scope, ya que se vuelve confuso qué $ scope está siendo afectado, especialmente teniendo en cuenta que ng-repeat, ng-include, ng-switch y directivas pueden crear sus propios ámbitos secundarios.

Mark Rajcok
fuente
66
@tamakisquare, creo que el texto en negrita que citó se aplica cuando se llama a la función de constructor del controlador, es decir, cuando se crea el controlador = asociado con un $ scope. No se aplica más tarde, cuando el código arbitrario de JavaScript llama a un método definido en un objeto $ scope.
Mark Rajcok
79
Tenga en cuenta que ahora es posible llamar a la función addPane () directamente en la plantilla nombrando el controlador: "MyController as myctrl" y luego myctrl.addPane (). Ver docs.angularjs.org/guide/concepts#controller
Christophe Augier el
81
Demasiada complejidad inherente.
Inanc Gumus
11
Esta es una respuesta muy informativa, pero cuando volví con un problema práctico ( cómo invocar $ scope. $ Apply () en un método de controlador definido usando 'this' ) no pude resolverlo. Entonces, si bien esta sigue siendo una respuesta útil, encuentro desconcertante la "complejidad inherente".
dumbledad
11
Javascript: mucha cuerda [para ahorcarse].
AlikElzin-kilaka 01 de
55

La razón por la que 'addPane' se asigna a esto se debe a la <pane>directiva.

La panedirectiva sí require: '^tabs', que coloca el objeto controlador de pestañas de una directiva principal, en la función de enlace.

addPaneestá asignado para thisque la panefunción de enlace pueda verlo. Luego, en la panefunción de enlace, addPanees solo una propiedad del tabscontrolador, y es solo tabsControllerObject.addPane. Por lo tanto, la función de enlace de la directiva de panel puede acceder al objeto controlador de pestañas y, por lo tanto, acceder al método addPane.

Espero que mi explicación sea lo suficientemente clara ... es un poco difícil de explicar.

Andrew Joslin
fuente
3
Gracias por la explicación. Los documentos hacen que parezca que el controlador es solo una función que configura el alcance. ¿Por qué se trata al controlador como un objeto si toda la acción ocurre en el ámbito? ¿Por qué no simplemente pasar el ámbito principal a la función de enlace? Editar: Para formular mejor esta pregunta, si los métodos de controlador y los métodos de alcance operan en la misma estructura de datos (el alcance), ¿por qué no ponerlos todos en un solo lugar?
Alexei Boronine
Parece que el ámbito primario no se pasa a la función lnk debido al deseo de admitir "componentes reutilizables, que no deberían leer o modificar accidentalmente datos en el ámbito primario". Pero si una directiva realmente quiere / necesita leer o modificar ALGUNOS datos ESPECÍFICOS en el ámbito primario (como lo hace la directiva 'panel'), requiere cierto esfuerzo: 'requerir' el controlador donde está el ámbito primario deseado, luego defina un método en ese controlador (use 'this' no $ scope) para acceder a datos específicos. Dado que el alcance primario deseado no se inyecta en la función lnk, supongo que esta es la única forma de hacerlo.
Mark Rajcok
1
Hola Mark, en realidad es más fácil modificar el alcance de la directiva. Puede usar la función de enlace jsfiddle.net/TuNyj
Andrew Joslin el
3
Gracias @Andy por el violín. En su violín, la directiva no está creando un nuevo alcance, por lo que puedo ver cómo la función de enlace puede acceder directamente al alcance del controlador aquí (ya que solo hay un alcance). Las pestañas y las directivas de panel usan ámbitos de aislamiento (es decir, se crean nuevos ámbitos secundarios que no se heredan prototípicamente del ámbito primario). Para el caso del alcance aislado, parece que definir un método en un controlador (usando 'esto') es la única forma de permitir que otra directiva obtenga acceso (indirecto) al otro alcance (aislado).
Mark Rajcok el
27

Acabo de leer una explicación bastante interesante sobre la diferencia entre los dos, y una creciente preferencia por adjuntar modelos al controlador y alias al controlador para vincular los modelos a la vista. http://toddmotto.com/digging-into-angulars-controller-as-syntax/ es el artículo.
No lo menciona, pero cuando define directivas, si necesita compartir algo entre varias directivas y no desea un servicio (hay casos legítimos en los que los servicios son una molestia), adjunte los datos al controlador de la directiva principal.

El $scopeservicio proporciona muchas cosas útiles, $watchsiendo lo más obvio, pero si todo lo que necesita para vincular datos a la vista, usar el controlador simple y 'controlador como' en la plantilla está bien y posiblemente sea preferible.

Derek
fuente
20

Le recomiendo que lea la siguiente publicación: AngularJS: "Controller as" o "$ scope"?

Describe muy bien las ventajas de usar "Controlador como" para exponer variables sobre "$ scope".

Sé que preguntaste específicamente sobre métodos y no sobre variables, pero creo que es mejor seguir una técnica y ser coherente con ella.

Entonces, en mi opinión, debido al problema de variables discutido en la publicación, es mejor usar la técnica "Controlador como" y también aplicarla a los métodos.

Liran Brimer
fuente
16

En este curso ( https://www.codeschool.com/courses/shaping-up-with-angular-js ) explican cómo usar "esto" y muchas otras cosas.

Si agrega un método al controlador a través de "este" método, debe llamarlo en la vista con el nombre del controlador "dot" de su propiedad o método.

Por ejemplo, al usar su controlador en la vista, puede tener un código como este:

    <div data-ng-controller="YourController as aliasOfYourController">

       Your first pane is {{aliasOfYourController.panes[0]}}

    </div>
Sandro
fuente
66
Después de pasar por el curso, el código me confundió de inmediato $scope, así que gracias por mencionarlo.
Matt Montag
16
Ese curso no menciona $ scope en absoluto, solo usan asy, thisentonces, ¿cómo puede ayudar a explicar la diferencia?
dumbledad
10
Mi primer contacto con Angular fue del curso mencionado, y como $scopenunca se mencionó, aprendí a usar solo thisen los controladores. El problema es que cuando comienza a tener que manejar promesas en su controlador, tiene un problema de muchas referencias thisy tiene que comenzar a hacer cosas como var me = thishacer referencia al modelo thisdesde dentro de la función de devolución de promesa. Por eso, todavía estoy muy confundido acerca de qué método debería usar, $scopeo this.
Bruno Finger
@BrunoFinger Desafortunadamente, necesitará var me = thiso .bind(this)cuando haga Promesas u otras cosas pesadas para el cierre. No tiene nada que ver con Angular.
Dzmitry Lazerka
1
Lo importante es saber que ng-controller="MyCtrl as MC"es equivalente a poner $scope.MC = thisen el propio controlador - que define una instancia (esto) de MyCtrl sobre el alcance para su uso en la plantilla a través de{{ MC.foo }}
William B
3

Las versiones anteriores de Angular (pre 1.0 RC) le permitieron usar esto de manera intercambiable con el método $ scope, pero este ya no es el caso. Dentro de los métodos definidos en el alcance, este y $ alcance son intercambiables (angular establece esto en $ alcance), pero no dentro del constructor de su controlador.

Para recuperar este comportamiento (¿alguien sabe por qué se cambió?) Puede agregar:

return angular.extend($scope, this);

al final de su función de controlador (siempre que se inyectó $ scope a esta función de controlador).

Esto tiene un buen efecto de tener acceso al ámbito primario a través del objeto controlador que puede obtener en el niño con require: '^myParentDirective'

Kamil Szot
fuente
77
Este artículo proporciona una buena explicación de por qué esto y $ scope son diferentes.
Robert Martin
1

$ scope tiene un 'this' diferente que el controlador 'this'. Por lo tanto, si coloca un console.log (this) dentro del controlador, le da un objeto (controller) y this.addPane () agrega el método addPane al Object del controlador. Pero $ scope tiene un alcance diferente y todos los métodos en su alcance deben ser accedidos por $ scope.methodName (). this.methodName()Controlador interno significa agregar metos dentro del objeto controlador. $scope.functionName()está en HTML y dentro

$scope.functionName(){
    this.name="Name";
    //or
    $scope.myname="myname"//are same}

Pegue este código en su editor y abra la consola para ver ...

 <!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>this $sope vs controller</title>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.7/angular.min.js"></script>
    <script>
        var app=angular.module("myApp",[]);
app.controller("ctrlExample",function($scope){
          console.log("ctrl 'this'",this);
          //this(object) of controller different then $scope
          $scope.firstName="Andy";
          $scope.lastName="Bot";
          this.nickName="ABot";
          this.controllerMethod=function(){

            console.log("controllerMethod ",this);
          }
          $scope.show=function(){
              console.log("$scope 'this",this);
              //this of $scope
              $scope.message="Welcome User";
          }

        });
</script>
</head>
<body ng-app="myApp" >
<div ng-controller="ctrlExample">
       Comming From $SCOPE :{{firstName}}
       <br><br>
       Comming from $SCOPE:{{lastName}}
       <br><br>
       Should Come From Controller:{{nickName}}
       <p>
            Blank nickName is because nickName is attached to 
           'this' of controller.
       </p>

       <br><br>
       <button ng-click="controllerMethod()">Controller Method</button>

       <br><br>
       <button ng-click="show()">Show</button>
       <p>{{message}}</p>

   </div>

</body>
</html>
Aniket Jha
fuente