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


los Página de alcance de referencia de API dice:

Un alcance poder heredar de un alcance principal.

los Guía del desarrollador Página del alcance dice:

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

Entonces, ¿el alcance de un hijo siempre hereda prototípicamente de su alcance principal? ¿Hay excepciones? Cuando hereda, ¿es siempre una herencia de prototipos normal de JavaScript?


965
2017-12-27 04:48


origen


Respuestas:


Respuesta rápida:
Un ámbito hijo normalmente hereda prototípicamente de su ámbito primario, pero no siempre. Una excepción a esta regla es una directiva con scope: { ... } - esto crea un alcance "aislado" que no hereda de manera prototípica. Esta construcción se usa a menudo al crear una directiva de "componente reutilizable".

En cuanto a los matices, la herencia del alcance normalmente es recta ... hasta que necesites Enlace de datos bidireccional (es decir, elementos de forma, ng-modelo) en el ámbito hijo. Ng-repeat, ng-switch y ng-include pueden hacerle tropezar si intenta vincularse a un primitivo (por ejemplo, número, cadena, booleano) en el ámbito principal desde dentro del ámbito secundario. No funciona de la forma en que la mayoría de la gente espera que funcione. El ámbito secundario tiene su propia propiedad que oculta / sombrea la propiedad primaria del mismo nombre. Sus soluciones son

  1. defina objetos en el elemento primario para su modelo, luego haga referencia a una propiedad de ese objeto en el elemento secundario: parentObj.someProp
  2. use $ parent.parentScopeProperty (no siempre es posible, pero es más fácil que 1. cuando sea posible)
  3. define una función en el ámbito principal y la llama desde el elemento secundario (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 para niños, por lo que el problema a menudo aparece cuando estas directivas están involucradas. (Ver este ejemplo para una ilustración rápida del problema).

Este problema con las primitivas se puede evitar fácilmente siguiendo la "mejor práctica" de siempre tenga un '.' en tus ng-models - mira 3 minutos vale la pena. Misko demuestra el primitivo problema vinculante con ng-switch.

Teniendo un '.' en sus modelos se asegurará de que la herencia prototípica esté en juego. Entonces, usa

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

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


Respuesta larga:

Herencia de Prototypal de JavaScript

También colocado en la wiki de AngularJS:  https://github.com/angular/angular.js/wiki/En Entendiendo-Escopios

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

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

prototypal inheritance

(Tenga en cuenta que para ahorrar espacio, muestro el anArray objeto como un solo 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 parentScope desde el ámbito secundario, JavaScript primero buscará en el ámbito secundario, no encontrará la propiedad, luego buscará en el alcance heredado y encontrará la propiedad. (Si no encuentra la propiedad en parentScope, continuará en la cadena de prototipos ... hasta el alcance raíz). Entonces, estos son todos verdaderos:

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

Supongamos que luego hacemos esto:

childScope.aString = 'child string'

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

property hiding

Supongamos que luego hacemos esto:

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

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

follow the prototype chain

Supongamos que luego hacemos esto:

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

No se consulta la cadena de prototipos, y el alcance hijo obtiene dos nuevas propiedades de objetos que ocultan o sombrean las propiedades del objeto parentScope con los mismos nombres.

more property hiding

Para llevar:

  • Si leemos childScope.propertyX y childScope tiene propertyX, entonces no se consulta la cadena de prototipos.
  • Si establecemos childScope.propertyX, no se consulta la cadena de prototipos.

Un último escenario:

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

Primero borramos la propiedad childScope, luego cuando intentamos acceder nuevamente a la propiedad, se consulta la cadena de prototipos.

after removing a child property


Herencia de Alcance Angular

Los contendientes:

  • Los siguientes crean nuevos ámbitos y heredan prototípicamente: ng-repeat, ng-include, ng-switch, ng-controller, directiva con scope: true, directiva con transclude: true.
  • A continuación, se crea un nuevo ámbito que no hereda prototípicamente: directiva con scope: { ... }. Esto crea un alcance "aislado" en su lugar.

Tenga en cuenta que, de forma predeterminada, las directivas no crean un nuevo ámbito, 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 alcance hijo, que prototípicamente hereda del alcance principal.

ng-include child scopes

Escribir (digamos, "77") en el primer cuadro de texto de entrada hace que el alcance del niño obtenga un nuevo myPrimitive propiedad de ámbito que oculta / sombrea la propiedad de ámbito principal del mismo nombre. Esto probablemente no es lo que quiere / espera.

ng-include with a primitive

Escribir (digamos, "99") en el segundo cuadro de texto de entrada no da como resultado una nueva propiedad secundaria. Debido a que tpl2.html vincula el modelo a una propiedad del objeto, la herencia del prototipo se inicia cuando el ngModel busca el objeto myObject, lo encuentra en el ámbito principal.

ng-include with an object

Podemos reescribir la primera plantilla para usar $ parent, si no queremos cambiar nuestro modelo de una primitiva a un objeto:

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

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

ng-include with $parent

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

Para escenarios donde los elementos de formulario no están involucrados, otra solución es definir una función en el ámbito principal para modificar la primitiva. Luego, asegúrese de que el niño siempre llame a esta función, que estará disponible para el alcance del niño debido a la herencia prototípica. P.ej.,

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

Aquí hay un muestra violín que usa este enfoque de "función parental". (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

ng-switch scope inheritance funciona igual que ng-include. Por lo tanto, si necesita un enlace de datos bidireccional a una primitiva en el ámbito primario, use $ parent o cambie el modelo para que sea un objeto y luego vincule a una propiedad de ese objeto. Esto evitará que el alcance del niño oculte / sombree las propiedades del alcance principal.

Ver también AngularJS, vincula el alcance de una caja de conmutación?

ng-repeat

Ng-repeat funciona de forma 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 principal, pero también asigna el valor del elemento a una nueva propiedad en el nuevo alcance del niño. (El nombre de la nueva propiedad es el nombre de la variable de bucle). Esto es lo que en realidad es el código fuente angular de ng-repeat:

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 de ámbito hijo. Cambiar el valor de la propiedad de ámbito hijo (es decir, usar ng-model, de ahí alcance de hijo num) hace no cambie la matriz a la que hace referencia el alcance principal. Por lo tanto, en la primera repetición ng anterior, cada ámbito infantil obtiene un num propiedad que es independiente de la matriz myArrayOfPrimitives:

ng-repeat with primitives

Esta ng-repeat no funcionará (como quiere / espera). Escribir en los cuadros de texto cambia los valores en 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 la propiedad primitiva de ámbito hijo. Para lograr esto, necesitamos cambiar el modelo para que sea una matriz de objetos.

Por lo tanto, si el elemento es un objeto, se asigna una referencia al objeto original (no una copia) a la nueva propiedad del ámbito secundario. Cambiar el valor de la propiedad del ámbito secundario (es decir, usar ng-model, por lo tanto, obj.num) hace cambiar el objeto al que hace referencia el ámbito principal. Entonces, en la segunda ng-repetición anterior, tenemos:

ng-repeat with objects

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

Esto funciona como se esperaba Al escribir en los cuadros de texto, se modifican los valores en los cuadros grises, que son visibles tanto para el ámbito secundario como para el primario.

Ver 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 una mala forma para que dos controladores compartan información a través de $ scope inheritance" - http://onehungrymind.com/angularjs-sticky-notes-pt-1-architecture/ Un servicio se debe utilizar para compartir datos entre los controladores en su lugar.

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

directivas

  1. defecto (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 puede pensar que está creando una nueva propiedad en el alcance, cuando en realidad está destruyendo una propiedad existente. Esta no es una buena opción para escribir directivas que están destinadas a ser componentes reutilizables.
  2. scope: true - la directiva crea un nuevo ámbito infantil que prototípicamente hereda del alcance principal. Si más de una directiva (en el mismo elemento DOM) solicita un nuevo ámbito, solo se crea un nuevo ámbito hijo. Como tenemos una herencia prototípica "normal", esto es como ng-include y ng-switch, así que tenga cuidado con el enlace de datos bidireccional a las primitivas primarias del alcance y el ocultamiento / sombreado del alcance del niño de las propiedades del alcance principal.
  3. scope: { ... } - la directiva crea un nuevo alcance aislado / aislado. No hereda prototípicamente. Esta suele ser su mejor opción al crear componentes reutilizables, ya que la directiva no puede leer o modificar accidentalmente el alcance principal. Sin embargo, tales directivas a menudo necesitan acceso a unas pocas propiedades del alcance principal. El hash del objeto se usa para configurar el enlace bidireccional (usando '=') o el enlace unidireccional (usando '@') entre el alcance padre y el alcance aislado. También hay '&' para enlazar a expresiones de alcance padre. Por lo tanto, todos estos crean propiedades de ámbito local que se derivan del alcance principal. Tenga en cuenta que los atributos se utilizan para ayudar a configurar el enlace: no puede simplemente hacer referencia a los nombres de propiedades del ámbito principal en el hash del objeto, sino que debe usar un atributo. Por ejemplo, esto no funcionará si desea enlazar a la propiedad principal parentProp en el alcance aislado: <div my-directive> y scope: { localProp: '@parentProp' }. Se debe usar un atributo para especificar cada propiedad principal a la que la directiva desea vincular: <div my-directive the-Parent-Prop=parentProp> y scope: { localProp: '@theParentProp' }.
    Aislar el alcance __proto__ referencias Objeto. Aislar las $ parent de scope hace referencia al ámbito primario, por lo tanto, aunque está aislado y no hereda prototípicamente del ámbito principal, sigue siendo un ámbito secundario.
    Para la imagen de abajo tenemos
      <my-directive interpolated="{{parentProp1}}" twowayBinding="parentProp2"> y
      scope: { interpolatedProp: '@interpolated', twowayBindingProp: '=twowayBinding' }
    Además, suponga que la directiva hace esto en su función de vinculación: scope.someIsolateProp = "I'm isolated"
      isolated scope
    Para obtener más información sobre los ámbitos aislados, consulte http://onehungrymind.com/angularjs-sticky-notes-pt-2-isolated-scope/
  4. transclude: true- la directiva crea un nuevo ámbito infantil "transcluido", que prototípicamente hereda del alcance principal. El ámbito transcluido y aislado (si lo hay) son hermanos: la propiedad $ parent de cada ámbito hace referencia al mismo ámbito principal. Cuando existen tanto un alcance transcluido como un ámbito aislado, la propiedad de ámbito aislado $$ nextSibling hará referencia al ámbito transcluido. No estoy al tanto de ningún matiz con el alcance transcluido.
    Para la imagen a continuación, asuma la misma directiva que la anterior con esta adición: transclude: true
    transcluded scope

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


Resumen

Hay cuatro tipos de ámbitos:

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

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

Diagramas fueron generados con  Archivos "* .dot", que están en github. Tim Caswell "Aprendiendo JavaScript con Object Graphs"fue la inspiración para usar GraphViz para los diagramas.


1694
2017-12-27 04:48



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

Solo la propiedad lee la cadena del prototipo, no escribe. Entonces cuando estableces

myObject.prop = '123';

No busca la cadena, pero cuando configura

myObject.myThing.prop = '123';

hay una lectura sutil pasando dentro de esa operación de escritura que intenta buscar myThing antes de escribir en su prop. Entonces, esa es la razón por la cual escribir a object.properties del niño llega a los objetos de los padres.


135
2018-05-16 15:22



Me gustaría agregar un ejemplo de herencia prototípica con javascript a @Scott Driscoll answer. Usaremos el patrón de herencia clásico con Object.create () que es parte de la especificación EcmaScript 5.

Primero creamos la función de objeto "principal"

function Parent(){

}

A continuación, agregue un prototipo a la función de objeto "principal"

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

Crear la función de objeto "Niño"

function Child(){

}

Asignar prototipo infantil (hacer que el prototipo hijo herede del prototipo principal)

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

Asignar el constructor de prototipo "Niño" apropiado

Child.prototype.constructor = Child;

Agregue el método "changeProps" a un prototipo hijo, que reescribirá el valor de propiedad "primitivo" en el objeto Child y cambiará el valor "object.one" tanto en objetos secundarios como secundarios

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

Inicia objetos de padre (padre) e hijo (hijo).

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

Llame al método Child (son) changeProps

son.changeProps();

Verifica los resultados

La propiedad primaria primitiva no cambió

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

Propiedad primitiva de hijo modificada (reescrita)

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

Las propiedades padre e hijo object.one cambiaron

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.create aquí https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/create


18
2017-11-08 22:45