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




ng-if directive (4)

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

Un ámbito puede heredar de un ámbito primario.

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

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

Entonces, ¿un ámbito secundario siempre se hereda prototípicamente de su ámbito primario? ¿Hay excepciones? Cuando se hereda, ¿es siempre la herencia prototípica de JavaScript normal?


Answers

De ninguna manera quiero competir con la respuesta de Mark, pero 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 .

Sólo las lecturas de propiedades buscan en la cadena del prototipo, no las escrituras. Así que cuando te pones

myObject.prop = '123';

No busca la cadena, pero cuando lo pones

myObject.myThing.prop = '123';

se está realizando una lectura sutil dentro de esa operación de escritura que intenta buscar myThing antes de escribir en su prop. Por eso es que escribir en object.properties del hijo obtiene en los objetos del padre.


Me gustaría agregar un ejemplo de herencia prototípica con javascript a la respuesta de @Scott Driscoll. Usaremos un 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 "Padre"

function Parent(){

}

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

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

Crear la función de objeto "Child"

function Child(){

}

Asignar prototipo secundario (Hacer prototipo secundario heredado del prototipo principal)

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

Asignar el constructor de prototipo "Niño" adecuado

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 de "object.one" tanto en el objeto primario como en el primario.

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

Inicia los objetos Padre (papá) y Niño (hijo).

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

Método de cambio de hijo de Call (hijo)

son.changeProps();

Revisa los resultados.

La propiedad primitiva padre no cambió

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

Propiedad primitiva del niño cambiada (reescrita)

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

Se modificaron las propiedades del objeto primario y secundario.

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


Respuesta rápida :
Un ámbito secundario normalmente se 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 se hereda de forma prototípica. Esta construcción se utiliza a menudo cuando se crea 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 hacerte tropezar si intentas enlazar a un primitivo (por ejemplo, número, cadena, booleano) en el ámbito primario desde dentro del ámbito secundario. 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.parentScopeProperty (no siempre es posible, pero es más fácil que 1. cuando sea posible)
  3. defina una función en el ámbito principal y llámela desde el 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 secundarios, por lo que el problema suele aparecer cuando se trata de estas directivas. (Vea este ejemplo para una ilustración rápida del problema.)

Este problema con los primitivos se puede evitar fácilmente siguiendo las "mejores prácticas" de tener siempre un '.' en tus modelos ng - mira el valor de 3 minutos. Misko demuestra el problema de enlace primitivo con ng-switch .

Teniendo un '.' en sus modelos se asegurará de que la herencia prototípica esté en juego. Por lo tanto, utilizar

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

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

Respuesta larga :

Herencia prototípica de JavaScript

También se encuentra en la wiki de AngularJS: 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 proviene de un entorno de servidor y está más familiarizado con la herencia clásica. Así que revisemos eso primero.

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

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

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

Supongamos que entonces hacemos esto:

childScope.aString = 'child string'

La cadena del prototipo no se consulta 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 entonces 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 el childScope. Los objetos se encuentran en el parentScope y los valores de las propiedades se actualizan en los objetos originales. No se agregan nuevas propiedades al childScope; no se crean nuevos objetos. (Tenga en cuenta que en las matrices y funciones de JavaScript también son objetos).

Supongamos que entonces hacemos esto:

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

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

Para llevar:

  • Si leemos childScope.propertyX, y childScope tiene propertyX, entonces no se consulta la cadena del prototipo.
  • Si configuramos 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 de forma prototípica: ng-repeat, ng-include, ng-switch, ng-controller, directiva con scope: true , directiva with transclude: true .
  • Lo siguiente 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 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 se hereda prototípicamente del ámbito principal.

Si escribe (por ejemplo, "77") en el primer cuadro de texto de entrada, el ámbito secundario obtendrá una nueva propiedad de ámbito myPrimitive que oculta / sombrea la propiedad de ámbito principal del mismo nombre. Esto probablemente no es lo que quieres / esperas.

Escribir (por ejemplo, "99") en el segundo cuadro de texto de entrada no da como resultado una nueva propiedad secundaria. Debido a que tpl2.html une el modelo a una propiedad de objeto, la herencia prototípica se activa cuando el 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 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 principal (porque $ parent es una propiedad de ámbito secundario que hace referencia al ámbito principal).

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

Para los 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 ejemplo de violín que utiliza este enfoque de "función principal". (El violín se escribió como parte de esta respuesta: https://.com/a/14104318/215945 ).

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

interruptor de ng

La herencia del alcance de 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 vincúlelo a una propiedad de ese objeto. Esto evitará la ocultación / el sombreado del ámbito secundario de las propiedades del ámbito principal.

Véase también AngularJS, ¿enlazar el alcance de una caja de conmutación?

ng-repetir

La repetición ng 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 se hereda de forma prototípica del ámbito primario, 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 realmente es el código fuente angular para ng-repeat:

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

Si el elemento es una primitiva (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 número de ámbito secundario) no cambia la matriz a la que se refiere el ámbito principal. Entonces, en la primera repetición ng anterior, cada ámbito secundario obtiene una propiedad num que es independiente de la matriz myArrayOfPrimitives:

Esta repetición ng no funcionará (como usted quiere / espera que lo haga). Al escribir en los cuadros de texto, se cambian 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 artículo es un objeto, se asigna una referencia al objeto original (no una copia) a la nueva propiedad de ámbito hijo. Cambiar el valor de la propiedad de ámbito secundario (es decir, usar ng-model, por obj.num tanto obj.num ) cambia el objeto al que se refiere el ámbito principal. Así que en la segunda repetición ng anterior, tenemos:

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

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

Consulte también Dificultad con el modelo ng, ng-repeat y entradas y https://.com/a/13782671/215945

controlador ng

Los controladores de anidamiento que utilizan el controlador ng dan como resultado la 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 incorrecto que dos controladores compartan información a través de la herencia $ scope" - http://onehungrymind.com/angularjs-sticky-notes-pt-1-architecture/ Se debe usar 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 ámbito secundario tendrá acceso a todas las propiedades del ámbito principal. Consulte también La 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 alcance, cuando en realidad está obstruyendo una propiedad existente. Esta no es una buena opción para escribir directivas que estén diseñadas como componentes reutilizables.
  2. scope: true : la directiva crea un nuevo ámbito secundario que se hereda prototípicamente del ámbito principal. Si más de una directiva (en el mismo elemento DOM) solicita un nuevo ámbito, solo se creará un nuevo ámbito secundario. Debido a que tenemos una herencia prototípica "normal", esto es como ng-include y ng-switch, por lo que desconfíe de la vinculación de datos bidireccional a las primitivas de alcance primario, y del ocultamiento / ocultación de alcance secundario de las propiedades del alcance principal
  3. scope: { ... } : la directiva crea un nuevo alcance aislado / aislado. No se hereda prototípicamente. Esta suele ser su mejor opción al crear componentes reutilizables, ya que la directiva no puede leer ni modificar accidentalmente el ámbito principal. Sin embargo, tales directivas a menudo necesitan acceso a unas pocas propiedades de alcance principal. El objeto hash se utiliza para configurar la vinculación bidireccional (utilizando '=') o la vinculación unidireccional (utilizando '@') entre el ámbito principal y el ámbito aislado. También hay '&' para enlazar a las expresiones del ámbito principal. Por lo tanto, todos estos crean propiedades de ámbito local que se derivan del ámbito principal. Tenga en cuenta que los atributos se utilizan para ayudar a configurar el enlace: no puede simplemente hacer referencia a los nombres de propiedad del ámbito principal en el hash del objeto, tiene que usar un atributo. Por ejemplo, esto no funcionará si desea enlazar con la propiedad principal parentProp 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 vincularse: <div my-directive the-Parent-Prop=parentProp> y scope: { localProp: '@theParentProp' } .
    Aislar el objeto __proto__ referencias del alcance. Aislar el $ parent del ámbito hace referencia al ámbito principal, por lo que, aunque está aislado y no se hereda de forma prototípica del ámbito principal, sigue siendo un ámbito secundario.
    Para la foto 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 enlace: scope.someIsolateProp = "I'm isolated"

    Para obtener más información sobre los ámbitos aislados, visite http://onehungrymind.com/angularjs-sticky-notes-pt-2-isolated-scope/
  4. transclude: true : la directiva crea un nuevo ámbito secundario "transcluido", que se hereda prototípicamente del ámbito principal. El ámbito transcluido y el ámbito aislado (si corresponde) son hermanos: la propiedad $ parent de cada ámbito hace referencia al mismo ámbito principal. Cuando existe un ámbito transcluido y un ámbito aislado, la propiedad de ámbito aislado $$ nextSibling hará referencia al ámbito transcluido. No tengo conocimiento de ningún matiz con el alcance transcluido.
    Para la imagen de abajo, asuma la misma directiva que arriba con esta adición: transclude: true

Este fiddle tiene una función showScope() que se puede usar 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 prototípica normal - ng-include, ng-switch, ng-controller, directiva con scope: true
  2. Herencia prototípica normal 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 ámbito - 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 transcluido - Directiva con transclude: true . Esta también es la herencia normal del ámbito prototípico, 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 y $$ childHead y $$ childTail.

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


¿Cuál es la forma correcta en C # de representar una estructura de datos ...

Recuerde: "Todos los modelos están mal, pero algunos son útiles". - George EP Box

No hay una "forma correcta", solo una útil.

Elija uno que sea útil para usted y / o sus usuarios. Eso es. Desarrollar económicamente, no sobre ingeniar. Cuanto menos código escriba, menos código necesitará depurar. (lea las siguientes ediciones).

- Editado

Mi mejor respuesta sería ... depende. La herencia de una lista expondría a los clientes de esta clase a métodos que pueden no estar expuestos, principalmente porque FootballTeam se parece a una entidad comercial.

- Edición 2

Sinceramente, no recuerdo a qué me refería en el comentario de "no hacer ingeniería excesiva". Si bien creo que la mentalidad de KISS es una buena guía, quiero enfatizar que heredar una clase de negocios de la Lista crearía más problemas de los que resuelve, debido a la pérdida de abstracción .

Por otro lado, creo que hay un número limitado de casos en los que simplemente heredar de la Lista es útil. Como escribí en la edición anterior, depende. La respuesta a cada caso está fuertemente influenciada por el conocimiento, la experiencia y las preferencias personales.

Gracias a @kai por ayudarme a pensar más precisamente sobre la respuesta.





javascript angularjs inheritance prototype prototypal-inheritance