¿Cuáles son los matices de la herencia prototípica / prototípica de alcance en AngularJS?


La página de Alcance de Referencia de la API dice:

Un ámbito puede heredar de un ámbito padre.

La página de alcance de la Guía del Desarrollador dice:

Un ámbito (prototípicamente) hereda propiedades de su ámbito padre.

Entonces, ¿un ámbito hijo siempre hereda prototípicamente de su ámbito padre? ¿Hay excepciones? Cuando hereda, ¿es siempre una herencia prototípica de JavaScript normal?

Author: Peter Mortensen, 2012-12-27

3 answers

Respuesta Rápida:
Un ámbito hijo normalmente hereda prototípicamente de su ámbito padre, pero no siempre. Una excepción a esta regla es una directiva con scope: { ... } creates esto crea un ámbito "aislado" que no hereda prototípicamente. Esta construcción se usa a menudo cuando se crea una directiva de "componente reutilizable".

En cuanto a los matices, la herencia de ámbito es normalmente directa... hasta que necesite enlace de datos de 2 vías (es decir, elementos de formulario, ng-model) en el visor infantil. Ng-repeat, ng-switch, y ng-include pueden dispararle si intenta enlazar a un primitivo (por ejemplo, número, cadena, booleano) en el ámbito padre desde dentro del ámbito hijo. No funciona como la mayoría de la gente espera que funcione. El ámbito secundario obtiene su propia propiedad que oculta/sombrea la propiedad principal del mismo nombre. Sus soluciones son

  1. defina objetos en el padre para su modelo, luego haga referencia a una propiedad de ese objeto en el hijo: parentObj.someProp
  2. use parent parent.Parentescopeproperty (no siempre es posible, pero más fácil que 1. cuando sea posible)
  3. definir una función en el ámbito padre, y llamarla desde el hijo (no siempre es posible)

Los nuevos desarrolladores de AngularJS a menudo no se dan cuenta de que ng-repeat, ng-switch, ng-view, ng-include y ng-if todos crean nuevos ámbitos secundarios, por lo que el problema a menudo aparece cuando estas directivas están involucradas. (Ver este ejemplo para una ilustración rápida de la problema.)

Este problema con los primitivos se puede evitar fácilmente siguiendo la "mejor práctica" de siempre tienen un '.'en sus modelos ng-ver 3 minutos vale la pena. Misko demuestra el problema primitivo de la unión con ng-switch.

Con un".'en sus modelos se asegurará de que la herencia prototípica está en juego. Por lo tanto, use

<input type="text" ng-model="someObj.prop1">

<!--rather than
<input type="text" ng-model="prop1">`
-->


L-o-n-g respuesta:

Herencia prototípica de JavaScript

También se coloca en el AngularJS wiki: https://github.com/angular/angular.js/wiki/Understanding-Scopes

Es importante tener primero una comprensión sólida de la herencia prototípica, especialmente si viene de un fondo del lado del servidor y está más familiarizado con la herencia de clase -al. Así que repasemos eso primero.

Supongamos que parentScope tiene propiedades aString, aNumber, anArray, anObject y aFunction. Si childScope hereda prototípicamente de parentScope, tienen:

herencia prototípica

(Tenga en cuenta que para ahorrar espacio, muestro el objeto anArray como un único objeto azul con sus tres valores, en lugar de un único objeto azul con tres literales grises separados.)

Si intentamos acceder a una propiedad definida en el parentScope desde el ámbito hijo, JavaScript buscará primero en el ámbito hijo, no encontrará la propiedad, luego buscará en el ámbito heredado y encontrará la propiedad. (Si no encontraba la propiedad en el parentScope, continuaría en la cadena de prototipos... hasta el alcance de la raíz). Por lo tanto, todos estos son verdaderos:

childScope.aString === 'parent string'
childScope.anArray[1] === 20
childScope.anObject.property1 === 'parent prop1'
childScope.aFunction() === 'parent output'

Supongamos que hacemos esto: {[51]]}

childScope.aString = 'child string'

No se consulta la cadena de prototipos, y se agrega una nueva propiedad aString al childScope. Esta nueva propiedad oculta/sombrea la propiedad parentScope con el mismo nombre. Esto se volverá muy importante cuando discutamos ng-repeat y ng-include a continuación.

ocultar la propiedad

Supongamos que hacemos esto: {[51]]}

childScope.anArray[1] = '22'
childScope.anObject.property1 = 'child prop1'

El se consulta la cadena de prototipos porque los objetos (anArray y anObject) no se encuentran en el childScope. Los objetos se encuentran en el parentScope y los valores de propiedad se actualizan en los objetos originales. No se agregan propiedades nuevas al childScope; no se crean objetos nuevos. (Tenga en cuenta que en JavaScript arrays y funciones también son objetos.)

siga la cadena de prototipos

Supongamos que hacemos esto: {[51]]}

childScope.anArray = [100, 555]
childScope.anObject = { name: 'Mark', country: 'USA' }

La cadena de prototipos no se consulta, y el ámbito hijo obtiene dos nuevos propiedades de objeto que ocultan / sombrean las propiedades de objeto de parentScope con los mismos nombres.

más propiedades ocultando

Conclusiones:

  • Si leemos childScope.propertyX, y childScope tiene propertyX, entonces la cadena prototipo no es consultada.
  • Si establecemos childScope.propertyX, la cadena prototipo no es consultada.

Un último escenario:

delete childScope.anArray
childScope.anArray[1] === 22  // true

Primero eliminamos la propiedad childScope, luego cuando intentamos acceder a la propiedad de nuevo, el se consulta la cadena de prototipos.

después de quitar una propiedad del niño


Herencia de Alcance angular

Los contendientes:

  • Los siguientes crean nuevos ámbitos y heredan prototípicamente: ng-repeat, ng-include, ng-switch, ng-controller, directive with scope: true, directive with transclude: true.
  • Lo siguiente crea un nuevo ámbito que no hereda prototípicamente: directiva con scope: { ... }. Esto crea un ámbito de" aislamiento " en su lugar.

Nota, por defecto, las directivas no crean un nuevo ámbito i es decir, el valor predeterminado es scope: false.

Ng-include

Supongamos que tenemos en nuestro controlador:

$scope.myPrimitive = 50;
$scope.myObject    = {aNumber: 11};

Y en nuestro HTML:

<script type="text/ng-template" id="/tpl1.html">
<input ng-model="myPrimitive">
</script>
<div ng-include src="'/tpl1.html'"></div>

<script type="text/ng-template" id="/tpl2.html">
<input ng-model="myObject.aNumber">
</script>
<div ng-include src="'/tpl2.html'"></div>

Cada ng-include genera un nuevo ámbito hijo, que prototípicamente hereda del ámbito padre.

ng-incluir alcances infantiles

Escribir (digamos, "77") en el primer cuadro de texto de entrada hace que el ámbito hijo obtenga una nueva propiedad de ámbito myPrimitive que oculta/sombrea la propiedad de ámbito padre del mismo nombre. Esto probablemente no es lo que quieres/esperas.

ng-incluir con un primitivo

Escribir (digamos, "99") en el segundo cuadro de texto de entrada no resulta en una nueva propiedad secundaria. Porque tpl2.html enlaza el modelo a una propiedad de objeto, la herencia prototípica se activa cuando el ngModel busca el objeto myObject finds lo encuentra en el ámbito padre.

ng-include con un objeto

Podemos reescribir la primera plantilla a usar parent parent, si no queremos cambiar nuestro modelo de un primitivo a un objeto:

<input ng-model="$parent.myPrimitive">

Escribir (digamos, "22") en este cuadro de texto de entrada no resulta en una nueva propiedad secundaria. El modelo ahora está vinculado a una propiedad del ámbito padre (porque $parent es una propiedad de ámbito hijo que hace referencia al ámbito padre).

ng-incluir con parent padre

Para todos los ámbitos (prototípicos o no), Angular siempre rastrea una relación padre-hijo (es decir, una jerarquía), a través de las propiedades de ámbito parent parent, child childHead y child childTail. Normalmente no muestro estas propiedades de ámbito en diagrama.

Para escenarios donde los elementos del formulario no están involucrados, otra solución es definir una función en el ámbito padre para modificar la primitiva. A continuación, asegúrese de que el hijo siempre llama a esta función, que estará disponible para el ámbito hijo debido a la herencia prototípica. Por ejemplo,

// in the parent scope
$scope.setMyPrimitive = function(value) {
     $scope.myPrimitive = value;
}

Aquí hay un fiddle ejemplo que usa este enfoque de "función padre". (El violín fue escrito como parte de esta respuesta: https://stackoverflow.com/a/14104318/215945.)

Ver también https://stackoverflow.com/a/13782671/215945 y https://github.com/angular/angular.js/issues/1267.

Ng-switch

La herencia de alcance de Ng-switch funciona igual que ng-include. Por lo tanto, si necesita un enlace de datos de 2 vías a una primitiva en el ámbito padre, use $parent o cambie el modelo para que sea un objeto y luego se vincule a una propiedad de ese objeto. Esto evitará el ámbito secundario ocultar / sombrear las propiedades del ámbito principal.

Véase también AngularJS, bind scope of a switch-case?

Ng-repeat

Ng-repeat funciona un poco diferente. Supongamos que tenemos en nuestro controlador:

$scope.myArrayOfPrimitives = [ 11, 22 ];
$scope.myArrayOfObjects    = [{num: 101}, {num: 202}]

Y en nuestro HTML:

<ul><li ng-repeat="num in myArrayOfPrimitives">
       <input ng-model="num">
    </li>
<ul>
<ul><li ng-repeat="obj in myArrayOfObjects">
       <input ng-model="obj.num">
    </li>
<ul>

Para cada elemento/iteración, ng-repeat crea un nuevo ámbito, que prototípicamente hereda del ámbito padre, pero también asigna el valor del elemento a una nueva propiedad en el nuevo ámbito hijo. (El el nombre de la nueva propiedad es el nombre de la variable de bucle. Esto es lo que el código fuente Angular para ng-repeat es en realidad:

childScope = scope.$new();  // child scope prototypically inherits from parent scope
...
childScope[valueIdent] = value;  // creates a new childScope property

Si item es una primitiva (como en myArrayOfPrimitives), esencialmente se asigna una copia del valor a la nueva propiedad secundaria scope. Cambiar el valor de la propiedad child scope (es decir, usar ng-model, por lo tanto child scope num) hace no cambiar el array al que hace referencia el ámbito padre. Así que en la primera repetición de ng anterior, cada ámbito hijo obtiene un num propiedad que es independiente del array myArrayOfPrimitives:

ng-repetir con primitivas

Este ng-repeat no funcionará (como quieres/esperas que funcione). Escribir en los cuadros de texto cambia los valores de los cuadros grises, que solo son visibles en los ámbitos secundarios. Lo que queremos es que las entradas afecten a la matriz myArrayOfPrimitives, no a una propiedad primitiva de ámbito hijo. Para lograr esto, necesitamos cambiar el modelo para que sea una matriz de objetos.

Entonces, si item es un objeto, un la referencia al objeto original (no a una copia) se asigna a la nueva propiedad de ámbito secundario. Cambiar el valor de la propiedad de ámbito secundario (es decir, usar ng-model, por lo tanto obj.num) cambia el objeto al que hace referencia el ámbito padre. Así que en la segunda ng-repetir arriba, tenemos:

ng-repetir con objetos

(Coloreé una línea de gris solo para que quede claro hacia dónde va.)

Esto funciona como se esperaba. Escribir en los cuadros de texto cambia los valores en los cuadros grises, que son visible tanto para los ámbitos hijo como padre.

Véase también Dificultad con ng-model, ng-repeat, y entradas y https://stackoverflow.com/a/13782671/215945

Ng-controller

Los controladores de anidamiento que usan ng-controller dan como resultado una herencia prototípica normal, al igual que ng-include y ng-switch, por lo que se aplican las mismas técnicas. Sin embargo, " se considera que es una mala forma para dos controladores compartir información a través de inheritance" -- http://onehungrymind.com/angularjs-sticky-notes-pt-1-architecture / En su lugar, se debe usar un servicio para compartir datos entre los controladores.

(Si realmente desea compartir datos a través de la herencia de alcance de los controladores, no hay nada que deba hacer. El ámbito hijo tendrá acceso a todas las propiedades del ámbito padre. Véase también El orden de carga del controlador difiere al cargar o navegar )

Directivas

  1. default (scope: false) - la directiva no crea un nuevo ámbito, por lo que no hay herencia aquí. Esto es fácil, pero también peligroso porque, por ejemplo, una directiva podría pensar que está creando una nueva propiedad en el ámbito de aplicación, cuando en realidad está golpeando una propiedad existente. Esta no es una buena opción para escribir directivas que están pensadas como componentes reutilizables.
  2. scope: true - la directiva crea un nuevo ámbito hijo que hereda prototípicamente del ámbito padre. Si hay más de una directiva (en el mismo elemento DOM) solicita un nuevo ámbito, solo se crea un nuevo ámbito secundario. Dado que tenemos una herencia prototípica "normal", esto es como ng-include y ng-switch, así que tenga cuidado con el enlace de datos de 2 vías a las primitivas del ámbito padre, y el ocultación/sombreado del ámbito hijo de las propiedades del ámbito padre.
  3. scope: { ... } - la directiva crea un nuevo ámbito aislado/aislado. No hereda prototípicamente. Esta suele ser su mejor opción al crear componentes reutilizables, ya que la directiva no puede leer accidentalmente o modificar el ámbito principal. Sin embargo, tales directivas a menudo necesitan acceso a algunas propiedades de ámbito padre. El hash de objeto se usa para configurar el enlace bidireccional (usando '=') o el enlace unidireccional (usando '@') entre el ámbito padre y el ámbito aislado. También hay ' & ' para enlazar a expresiones de ámbito padre. Por lo tanto, todos estos crean propiedades de ámbito local que se derivan del ámbito padre. Tenga en cuenta que los atributos se utilizan para ayudar a configurar el enlace -- no solo puede hacer referencia a los nombres de propiedad del ámbito principal en el hash del objeto, debe usar un atributo. Por ejemplo, esto no funcionará si desea enlazar a la propiedad padre parentProp en el ámbito aislado: <div my-directive> y scope: { localProp: '@parentProp' }. Se debe usar un atributo para especificar cada propiedad padre a la que la directiva quiere enlazar: <div my-directive the-Parent-Prop=parentProp> y scope: { localProp: '@theParentProp' }.
    Aislar el objeto de referencias del ámbito __proto__. Isolate scope references parent hace referencia al ámbito padre, por lo que aunque está aislado y no hereda prototípicamente del ámbito padre, sigue siendo un ámbito hijo.
    Para la imagen de abajo tenemos
    <my-directive interpolated="{{parentProp1}}" twowayBinding="parentProp2"> y
    scope: { interpolatedProp: '@interpolated', twowayBindingProp: '=twowayBinding' }
    También, supongamos que la directiva hace esto en su función de enlace: {[40]]}
    ámbito de aplicación aislado
    Para más información sobre alcances aislados ver http://onehungrymind.com/angularjs-sticky-notes-pt-2-isolated-scope /
  4. transclude: true - la directiva crea un nuevo ámbito hijo "transcluded", que prototípicamente hereda del ámbito padre. El ámbito transcluded y el ámbito aislado (si lo hay) son hermanos the la propiedad parent parent de cada ámbito hace referencia al mismo ámbito principal. Cuando existe un ámbito transcluded y un ámbito isolate, la propiedad isolate scope property nextSibling hará referencia al ámbito transcluded. No soy consciente de ningún matiz con el alcance transcluido.
    Para la imagen de abajo, asuma la misma directiva que la anterior con esta adición: transclude: true
    transcluded alcance

Este violín tiene una función showScope() que se puede utilizar para examinar un ámbito aislado y transcluido. Ver las instrucciones en el comentarios en el violín.


Resumen

Hay cuatro tipos de ámbitos:

  1. herencia de alcance prototípico normal ng ng-include, ng-switch, ng-controller, directiva con scope: true
  2. herencia de alcance prototípico normal con una copia/asignación ng ng-repeat. Cada iteración de ng-repeat crea un nuevo ámbito secundario, y ese nuevo ámbito secundario siempre obtiene una nueva propiedad.
  3. ámbito de aplicación aislado directive directiva con scope: {...}. Este no es prototípico, pero '=', '@'y' & ' proporcionan un mecanismo para acceder a las propiedades del ámbito principal, a través de atributos.
  4. ámbito de aplicación transcluido directive directiva con transclude: true. Este también es una herencia de ámbito prototípico normal, pero también es un hermano de cualquier ámbito aislado.

Para todos los ámbitos (prototípicos o no), Angular siempre rastrea una relación padre-hijo (es decir, una jerarquía), a través de las propiedades parent parent y child childHead y child childTail.

Los diagramas se generaron con graphviz "*.dot " archivos, que están en github . Tim Caswell" Learning JavaScript with Object Graphs " fue la inspiración para usar GraphViz para los diagramas.

 1707
Author: Mark Rajcok,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 12:02:49

De ninguna manera quiero competir con la respuesta de Mark, sino que solo quería resaltar la pieza que finalmente hizo que todo hiciera clic como alguien nuevo en la herencia de Javascript y su cadena de prototipos.

Solo la propiedad lee busca en la cadena de prototipos, no escribe. Así que cuando se establece

myObject.prop = '123';

No mira hacia arriba la cadena, pero cuando se establece

myObject.myThing.prop = '123';

Hay una lectura sutil dentro de esa operación de escritura que trata de buscar MyThing antes escribiendo a su prop. Por eso escribir para objetar.propiedades del hijo obtiene en los objetos del padre.

 136
Author: Scott Driscoll,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-07-17 12:09:38

Me gustaría añadir un ejemplo de herencia prototípica con javascript a la respuesta de @Scott Driscoll. Usaremos el patrón de herencia clásico con Objeto.create () que es parte de la especificación ECMAScript 5.

Primero creamos la función de objeto "Padre"

function Parent(){

}

Luego agregue un prototipo a la función de objeto" Padre "

 Parent.prototype = {
 primitive : 1,
 object : {
    one : 1
   }
}

Crear función de objeto "Hijo"

function Child(){

}

Asignar prototipo hijo (Hacer que el prototipo hijo herede del padre prototipo)

Child.prototype = Object.create(Parent.prototype);

Asignar un constructor de prototipo "Hijo" apropiado

Child.prototype.constructor = Child;

Agregue el método "changeProps" a un prototipo hijo, que reescribirá el valor de la propiedad "primitiva" en el objeto Hijo y cambiará el objeto".un " valor en los objetos Hijo y Padre

Child.prototype.changeProps = function(){
    this.primitive = 2;
    this.object.one = 2;
};

Iniciar objetos Padre (dad) e Hijo (son).

var dad = new Parent();
var son = new Child();

Call Child (son) changeProps method

son.changeProps();

Comprueba los resultados.

La propiedad primitiva padre no cambio

console.log(dad.primitive); /* 1 */

Propiedad primitiva infantil cambiada (reescrita)

console.log(son.primitive); /* 2 */

Objeto padre e hijo.una propiedad cambió

console.log(dad.object.one); /* 2 */
console.log(son.object.one); /* 2 */

Ejemplo de Trabajo aquí http://jsbin.com/xexurukiso/1/edit/

Más información sobre Object.crear aquí https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/create

 19
Author: tylik,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-11-08 22:51:55