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
- defina objetos en el elemento primario para su modelo, luego haga referencia a una propiedad de ese objeto en el elemento secundario: parentObj.someProp
- use $ parent.parentScopeProperty (no siempre es posible, pero es más fácil que 1. siempre que sea posible)
- 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-include
y ng-if
todo 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:
(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.
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).
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.
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.
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.
Al escribir (digamos, "77") en el primer cuadro de texto de entrada, el ámbito secundario obtiene una nueva myPrimitive
propiedad de ámbito que oculta / oculta la propiedad de ámbito principal del mismo nombre. Esto probablemente no sea lo que quieres / esperas.
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.
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).
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 num
propiedad que es independiente de la matriz myArrayOfPrimitives:
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:
(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
- 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.
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.
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 principalparentProp
en 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"
Para obtener más información sobre los ámbitos de aislamiento, consulte http://onehungrymind.com/angularjs-sticky-notes-pt-2-isolated-scope/
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
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:
- herencia normal del alcance prototípico: ng-include, ng-switch, ng-controller, directiva con
scope: true
- 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.
- 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.
- 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 graphvizArchivos "* .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.