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

1028

La página Ámbito de referencia de API dice:

Un ámbito puede heredar de un ámbito primario.

La página Alcance de la Guía del desarrollador dice:

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

  • Entonces, ¿un ámbito secundario siempre hereda prototípicamente de su ámbito primario?
  • ¿Hay excepciones?
  • Cuando hereda, ¿es siempre una herencia prototípica de JavaScript normal?
Mark Rajcok
fuente

Respuestas:

1741

Respuesta rápida :
un ámbito secundario 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 de "aislamiento" que no hereda prototípicamente. Esta construcción se usa a menudo al crear una directiva de "componente reutilizable".

En cuanto a los matices, la herencia del alcance es normalmente directa ... hasta que necesite un enlace de datos bidireccional (es decir, elementos de formulario, modelo ng) en el alcance secundario. Ng-repeat, ng-switch y ng-include pueden hacer que te tropieces si intentas unirte a una primitiva (por ejemplo, número, cadena, booleano) en el ámbito primario desde dentro del ámbito secundario. No funciona de la manera en que 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 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. siempre que sea posible)
  3. definir una función en el ámbito primario y llamarla desde el elemento secundario (no siempre es posible)

AngularJS nuevos desarrolladores a menudo no se dan cuenta de que ng-repeat, ng-switch, ng-view, ng-includey ng-iftodo ello crea nuevos ámbitos secundarios, por lo que el problema aparece a menudo cuando se trata de estas directivas. (Vea 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 tener siempre un '.' en sus modelos ng : mire 3 minutos. Misko demuestra la primitiva cuestión 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 prototipos de JavaScript

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

Es importante tener una comprensión sólida de la herencia de prototipos, especialmente si proviene de un entorno del lado del servidor y está más familiarizado con la herencia clásica. Así que repasemos eso primero.

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

herencia prototípica

(Tenga en cuenta que para ahorrar espacio, muestro el anArray objeto como un único objeto azul con sus tres valores, en lugar de un solo 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 ámbito heredado y encontrará la propiedad. (Si no encuentra la propiedad en parentScope, continuará en la cadena de prototipos ... hasta el alcance raíz). Entonces, todo esto es cierto:

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'

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

ocultamiento de propiedad

Supongamos que luego hacemos esto:

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

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

sigue la cadena del prototipo

Supongamos que luego hacemos esto:

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

No se consulta la cadena del prototipo, y el ámbito secundario obtiene dos nuevas propiedades de objeto que ocultan / ocultan las propiedades del objeto parentScope con los mismos nombres.

Más propiedades escondidas

Comida para llevar:

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

Un último escenario:

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

Primero eliminamos la propiedad childScope, luego, cuando intentamos acceder a la propiedad nuevamente, se consulta la cadena del prototipo.

después de eliminar una propiedad secundaria


Herencia de alcance angular

Los contendientes:

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

Tenga en cuenta que, por defecto, 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 ámbito secundario, que hereda prototípicamente del ámbito primario.

ng-include ámbitos infantiles

Al escribir (digamos, "77") en el primer cuadro de texto de entrada, el ámbito secundario obtiene una nueva myPrimitivepropiedad de ámbito que oculta / oculta la propiedad de ámbito principal del mismo nombre. Esto probablemente no sea lo que quieres / esperas.

ng-include con un primitivo

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 de objeto, la herencia prototípica se activa cuando ngModel busca el objeto myObject, lo encuentra en el ámbito principal.

ng-include con un objeto

Podemos reescribir la primera plantilla para usar $ parent, si no queremos cambiar nuestro modelo de primitivo a 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 secundario que hace referencia al ámbito primario).

ng-include con $ 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 ámbito $ 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 primario 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 violín de muestra que utiliza este enfoque de "función principal". (El violín se escribió como parte de esta respuesta: https://stackoverflow.com/a/14104318/215945 ).

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

ng-switch

La herencia del alcance ng-switch funciona igual que ng-include. Entonces, 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 enlace a una propiedad de ese objeto. Esto evitará que el ámbito secundario oculte / sombree las propiedades del ámbito primario.

Ver también AngularJS, enlace de alcance de un caso de interruptor?

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 hereda prototípicamente del ámbito principal, pero también asigna el valor del elemento a una nueva propiedad en el nuevo ámbito secundario . (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:

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

Si el elemento es primitivo (como en myArrayOfPrimitives), esencialmente se asigna una copia del valor a la nueva propiedad de ámbito secundario. Cambiar el valor de la propiedad del ámbito secundario (es decir, usar ng-model, por lo tanto, el ámbito secundario num) no cambia la matriz a la que hace referencia el ámbito primario. Entonces, en la primera repetición ng anterior, cada ámbito secundario obtiene una numpropiedad que es independiente de la matriz myArrayOfPrimitives:

ng-repetir con primitivas

Esta repetición de ng no funcionará (como desea / 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 una propiedad primitiva de ámbito secundario. 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 de ámbito secundario. Cambiando el valor de la propiedad ámbito secundario (es decir, usando ng de modelo, por lo tanto obj.num) hace cambiar el objeto de las referencias ámbito padre. Entonces, en la segunda repetición ng anterior, tenemos:

ng-repetir con objetos

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

Esto funciona como se esperaba. Escribir en los cuadros de texto cambia los valores en los cuadros grises, que son visibles para los ámbitos primarios y secundarios.

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

ng-controller

La anidación de controladores que usan ng-controller da 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 la herencia de $ alcance" - http://onehungrymind.com/angularjs-sticky-notes-pt-1-architecture/ Se debe utilizar un servicio para compartir datos entre 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 alcance secundario tendrá acceso a todas las propiedades del alcance primario. Consulte 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, cuando en realidad está bloqueando una propiedad existente. Esta no es una buena opción para escribir directivas destinadas a componentes reutilizables.
  2. scope: true- la directiva crea un nuevo ámbito secundario que hereda prototípicamente del ámbito primario. Si más de una directiva (en el mismo elemento DOM) solicita un nuevo ámbito, solo se crea un nuevo ámbito secundario. 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 del alcance primario y el ocultamiento / sombreado del alcance secundario de las propiedades del alcance primario.
  3. scope: { ... }- la directiva crea un nuevo aislamiento / alcance 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 ámbito principal. Sin embargo, tales directivas a menudo necesitan acceso a algunas propiedades de ámbito principal. El hash del objeto se usa para configurar un enlace bidireccional (usando '=') o un enlace unidireccional (usando '@') entre el alcance principal y el alcance aislado. También hay '&' para enlazar a las expresiones de ámbito principal. Por lo tanto, todos estos crean propiedades de ámbito local que se derivan del ámbito primario. Tenga en cuenta que los atributos se usan para ayudar a configurar el enlace: no solo puede hacer referencia a los nombres de propiedades del ámbito primario en el hash del objeto, debe usar un atributo. Por ejemplo, esto no funcionará si desea vincular a la propiedad principalparentPropen el ámbito aislado: <div my-directive>y scope: { localProp: '@parentProp' }. Se debe usar un atributo para especificar cada propiedad principal a la que la directiva desea enlazar: <div my-directive the-Parent-Prop=parentProp>y scope: { localProp: '@theParentProp' }.
    Aislar las __proto__referencias del alcance Objeto. Aislar $ parent del ámbito hace referencia al ámbito principal, por lo que, aunque está aislado y no hereda prototípicamente del ámbito principal, sigue siendo un ámbito secundario.
    Para la imagen a continuación que 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: scope.someIsolateProp = "I'm isolated"
    alcance aislado
    Para obtener más información sobre los ámbitos de aislamiento, consulte http://onehungrymind.com/angularjs-sticky-notes-pt-2-isolated-scope/
  4. transclude: true- la directiva crea un nuevo ámbito secundario "transcluido", que hereda prototípicamente del ámbito primario. El alcance transcluido y el aislado (si corresponde) son hermanos: la propiedad $ parent de cada alcance hace referencia al mismo alcance padre. Cuando existen un alcance transcluido y uno aislado, la propiedad de alcance aislado $$ nextSibling hará referencia al alcance transcluido. No conozco 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
    alcance transcluido

Este violín tiene una showScope()función que puede usarse para examinar un alcance aislado y transcluido. Vea las instrucciones en los comentarios en el violín.


Resumen

Hay cuatro tipos de ámbitos:

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

Los diagramas se generaron con Archivos "* .dot", que están en github . " Aprendiendo JavaScript con gráficos de objetos " de Tim Caswell fue la inspiración para usar GraphViz para los diagramas.

Mark Rajcok
fuente
48
Artículo impresionante, demasiado largo para una respuesta SO, pero de todos modos muy útil. Póngalo en su blog antes de que un editor lo reduzca a su tamaño.
iwein
43
Puse una copia en el wiki de AngularJS .
Mark Rajcok
3
Corrección: "Aislar las __proto__referencias del alcance Objeto". en su lugar debería ser "Aislar las __proto__referencias del alcance a un objeto Scope". Por lo tanto, en las últimas dos imágenes, los cuadros naranjas de "Objeto" deberían ser cuadros de "Alcance".
Mark Rajcok el
15
Esta respuesta debe incluirse en la guía angularjs. Esto es mucho más didáctico ...
Marcelo De Zen
2
La wiki me deja perplejo, primero dice: "Se consulta la cadena del prototipo porque el objeto no se encuentra en el childScope". y luego dice: "Si configuramos childScope.propertyX, no se consulta la cadena del prototipo". El segundo implica una condición mientras que el primero no.
Stephane
140

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 la herencia de Javascript y su cadena de prototipos .

Solo las lecturas de propiedades buscan en la cadena del prototipo, no las escrituras. Entonces, cuando configuras

myObject.prop = '123';

No mira la cadena, pero cuando configuras

myObject.myThing.prop = '123';

hay una lectura sutil dentro de esa operación de escritura que intenta buscar myThing antes de escribir en su utilería. Es por eso que escribir en object.properties del niño llega a los objetos del padre.

Scott Driscoll
fuente
12
Si bien este es un concepto muy simple, puede no ser muy obvio ya que, creo, mucha gente lo extraña. Así poner.
moljac024
3
Excelente comentario Quito, la resolución de una propiedad no objeto no implica una lectura, mientras que la resolución de una propiedad de objeto sí.
Stephane
1
¿Por qué? ¿Cuál es la motivación para que las escrituras de propiedad no suban en la cadena de prototipos? Parece una locura ...
Jonathan.
1
Sería genial si agregaras un ejemplo realmente simple.
tylik
2
Tenga en cuenta que busca en la cadena de prototipos los setters . Si no se encuentra nada, crea una propiedad en el receptor.
Bergi
21

Me gustaría agregar un ejemplo de herencia prototípica con javascript a la respuesta de @Scott Driscoll. Utilizaremos el patrón de herencia clásico con Object.create (), que forma 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 "Principal"

 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 prototipo padre)

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

Asignar el constructor prototipo "Niño" apropiado

Child.prototype.constructor = Child;

Agregue el método "changeProps" a un prototipo secundario, que reescribirá el valor de la propiedad "primitiva" en el objeto secundario y cambiará el valor "object.one" tanto en los objetos secundarios como primarios

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

Iniciar objetos padre (padre) e hijo (hijo).

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

Llame al niño (hijo) método changeProps

son.changeProps();

Comprueba los resultados.

La propiedad primitiva padre no cambió

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

Propiedad primitiva secundaria modificada (reescrita)

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

Las propiedades object.one padre y niño 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

tylik
fuente