Quais são as nuances da herança prototípica / prototípica do escopo no AngularJS?

1028

A página Escopo de referência da API diz:

Um escopo pode herdar de um escopo pai.

A página Escopo do Guia do desenvolvedor diz:

Um escopo (prototipicamente) herda propriedades do seu escopo pai.

  • Então, um escopo filho sempre herda prototipicamente de seu escopo pai?
  • Existem exceções?
  • Quando ele herda, é sempre uma herança prototípica normal do JavaScript?
Mark Rajcok
fonte

Respostas:

1741

Resposta rápida :
um escopo filho normalmente herda prototipicamente do escopo pai, mas nem sempre. Uma exceção a essa regra é uma diretiva com scope: { ... }- isso cria um escopo "isolado" que não herda prototipicamente. Essa construção é frequentemente usada ao criar uma diretiva "componente reutilizável".

Quanto às nuances, a herança do escopo é normalmente direta ... até que você precise da ligação de dados bidirecional (ou seja, elementos de formulário, modelo ng) no escopo filho. Repetir Ng, alternar entre ng e incluir como ng pode fazer com que você tente se ligar a uma primitiva (por exemplo, número, sequência, booleano) no escopo pai de dentro do escopo filho. Não funciona da maneira que a maioria das pessoas espera que funcione. O escopo filho obtém sua própria propriedade que oculta / oculta a propriedade pai com o mesmo nome. Suas soluções alternativas são

  1. defina objetos no pai para o seu modelo e faça referência a uma propriedade desse objeto no filho: parentObj.someProp
  2. use $ parent.parentScopeProperty (nem sempre possível, mas mais fácil que 1. sempre que possível)
  3. defina uma função no escopo pai e chame-a do filho (nem sempre é possível)

Novos desenvolvedores AngularJS muitas vezes não percebem que ng-repeat, ng-switch, ng-view, ng-includee ng-iftudo criar novos escopos filhos, de modo que o problema geralmente aparece quando estas directivas estão envolvidos. (Veja este exemplo para uma ilustração rápida do problema.)

Esse problema com os primitivos pode ser facilmente evitado seguindo a "melhor prática" de sempre ter um '.' nos seus modelos ng - assista a 3 minutos. Misko demonstra o problema de ligação primitivo com ng-switch.

Tendo uma '.' nos seus modelos garantirá que a herança prototípica esteja em jogo. Então use

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

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


Resposta longa :

Herança Prototípica do JavaScript

Também colocado no wiki do AngularJS: https://github.com/angular/angular.js/wiki/Understanding-Scopes

É importante primeiro ter um entendimento sólido da herança prototípica, especialmente se você é proveniente de um plano de fundo do servidor e está mais familiarizado com a herança clássica. Então, vamos revisar isso primeiro.

Suponha que parentScope tenha as propriedades aString, aNumber, anArray, anObject e aFunction. Se childScope herda prototipicamente de parentScope, temos:

herança prototípica

(Observe que, para economizar espaço, mostro o anArrayobjeto como um único objeto azul com seus três valores, em vez de um único objeto azul com três literais cinza separados.)

Se tentarmos acessar uma propriedade definida no parentScope no escopo filho, o JavaScript primeiro procurará no escopo filho, não encontrará a propriedade, depois procurará no escopo herdado e encontrará a propriedade. (Se não encontrasse a propriedade no parentScope, continuaria a cadeia de protótipos ... até o escopo raiz). Então, tudo isso é verdade:

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

Suponha que façamos o seguinte:

childScope.aString = 'child string'

A cadeia de protótipos não é consultada e uma nova propriedade aString é adicionada ao childScope. Essa nova propriedade oculta / oculta a propriedade parentScope com o mesmo nome. Isso se tornará muito importante quando discutirmos ng-repeat e ng-include abaixo.

esconderijo de propriedade

Suponha que façamos o seguinte:

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

A cadeia de protótipos é consultada porque os objetos (anArray e anObject) não são encontrados no childScope. Os objetos são encontrados no parentScope e os valores da propriedade são atualizados nos objetos originais. Nenhuma nova propriedade é adicionada ao childScope; nenhum novo objeto é criado. (Observe que em matrizes e funções JavaScript também são objetos.)

siga a cadeia de protótipos

Suponha que façamos o seguinte:

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

A cadeia de protótipos não é consultada e o escopo filho obtém duas novas propriedades de objetos que ocultam / sombream as propriedades do objeto parentScope com os mesmos nomes.

mais propriedade escondida

Aprendizado:

  • Se lermos childScope.propertyX e childScope tiver a propriedade X, a cadeia de protótipos não será consultada.
  • Se definirmos childScope.propertyX, a cadeia de protótipos não será consultada.

Um último cenário:

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

Excluímos a propriedade childScope primeiro e, quando tentamos acessar a propriedade novamente, a cadeia de protótipos é consultada.

depois de remover uma propriedade filho


Herança do escopo angular

Os candidatos:

  • A seguir, crie novos escopos e herda prototipicamente: ng-repeat, ng-include, ng-switch, ng-controller, diretiva com scope: true, diretiva com transclude: true.
  • A seguir, cria um novo escopo que não herda prototipicamente: diretiva with scope: { ... }. Isso cria um escopo "isolado".

Observe que, por padrão, as diretivas não criam um novo escopo - ou seja, o padrão é scope: false.

ng-include

Suponha que tenhamos em nosso controlador:

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

E no nosso 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 gera um novo escopo filho, que herda prototipicamente do escopo pai.

ng-include escopos filho

Digitar (por exemplo, "77") na primeira caixa de texto de entrada faz com que o escopo filho obtenha uma nova myPrimitivepropriedade de escopo que oculta / oculta a propriedade do escopo pai com o mesmo nome. Provavelmente não é isso que você deseja / espera.

ng-include com um primitivo

Digitar (por exemplo, "99") na segunda caixa de texto de entrada não resulta em uma nova propriedade filho. Como tpl2.html vincula o modelo a uma propriedade de objeto, a herança prototípica entra em ação quando o ngModel procura pelo objeto myObject - ele o encontra no escopo pai.

ng-include com um objeto

Podemos reescrever o primeiro modelo para usar $ parent, se não quisermos mudar nosso modelo de primitivo para objeto:

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

Digitar (por exemplo, "22") nesta caixa de texto de entrada não resulta em uma nova propriedade filho. O modelo agora está vinculado a uma propriedade do escopo pai (porque $ parent é uma propriedade do escopo filho que faz referência ao escopo pai).

ng-include com $ parent

Para todos os escopos (prototípicos ou não), o Angular sempre rastreia um relacionamento pai-filho (ou seja, uma hierarquia), através das propriedades do escopo $ parent, $$ childHead e $$ childTail. Normalmente não mostro essas propriedades de escopo nos diagramas.

Para cenários em que os elementos do formulário não estão envolvidos, outra solução é definir uma função no escopo pai para modificar a primitiva. Em seguida, verifique se o filho sempre chama essa função, que estará disponível para o escopo filho devido à herança prototípica. Por exemplo,

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

Aqui está um exemplo de violino que usa essa abordagem de "função pai". (O violino foi escrito como parte desta resposta: https://stackoverflow.com/a/14104318/215945 .)

Consulte também https://stackoverflow.com/a/13782671/215945 e https://github.com/angular/angular.js/issues/1267 .

ng-switch

A herança de escopo ng-switch funciona como ng-include. Portanto, se você precisar de uma ligação de dados bidirecional para uma primitiva no escopo pai, use $ parent ou altere o modelo para ser um objeto e, em seguida, vincule a uma propriedade desse objeto. Isso evitará que o escopo filho oculte / oculte as propriedades do escopo pai.

Veja também AngularJS, escopo de ligação de um caso de switch?

ng-repeat

Ng-repeat funciona um pouco diferente. Suponha que tenhamos em nosso controlador:

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

E no nosso 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 item / iteração, ng-repeat cria um novo escopo, que herda prototipicamente do escopo pai, mas também atribui o valor do item a uma nova propriedade no novo escopo filho . (O nome da nova propriedade é o nome da variável de loop.) Aqui está o que o código-fonte angular para ng-repeat é realmente:

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

Se o item for um primitivo (como em myArrayOfPrimitives), essencialmente uma cópia do valor será atribuída à nova propriedade do escopo filho. Alterar o valor da propriedade do escopo filho (ou seja, usar o modelo ng, portanto o escopo filho num) não altera a matriz que o escopo pai faz referência. Portanto, na primeira repetição ng acima, cada escopo filho obtém uma numpropriedade independente da matriz myArrayOfPrimitives:

ng-repita com primitivas

Este ng-repeat não funcionará (como você deseja / espera). Digitar nas caixas de texto altera os valores nas caixas cinzas, que são visíveis apenas nos escopos filho. O que queremos é que as entradas afetem a matriz myArrayOfPrimitives, não uma propriedade primitiva do escopo filho. Para fazer isso, precisamos alterar o modelo para ser uma matriz de objetos.

Portanto, se item é um objeto, uma referência ao objeto original (não uma cópia) é atribuída à nova propriedade do escopo filho. Alterando o valor da propriedade âmbito criança (ou seja, usando-ng de modelo, daí obj.num) faz mudar o objecto as referências escopo pai. Então, no segundo ng-repeat acima, temos:

ng-repita com objetos

(Eu pintei uma linha em cinza apenas para que fique claro para onde está indo.)

Isso funciona como esperado. Digitar nas caixas de texto altera os valores nas caixas cinzas, que são visíveis para os escopos filho e pai.

Consulte também Dificuldade com ng-model, ng-repeat e entradas e https://stackoverflow.com/a/13782671/215945

ng-controller

Aninhar controladores usando ng-controller resulta em herança prototípica normal, assim como ng-include e ng-switch, portanto as mesmas técnicas se aplicam. No entanto, "é considerado péssimo para dois controladores compartilhar informações via herança $ scope" - http://onehungrymind.com/angularjs-sticky-notes-pt-1-architecture/ Um serviço deve ser usado para compartilhar dados entre controladores.

(Se você realmente deseja compartilhar dados através da herança do escopo dos controladores, não há nada a fazer. O escopo filho terá acesso a todas as propriedades do escopo pai. Consulte também A ordem de carregamento do controlador difere ao carregar ou navegar )

diretrizes

  1. default ( scope: false) - a diretiva não cria um novo escopo; portanto, não há herança aqui. Isso é fácil, mas também perigoso, porque, por exemplo, uma diretiva pode pensar que está criando uma nova propriedade no escopo, quando na verdade está roubando uma propriedade existente. Essa não é uma boa opção para escrever diretivas que se destinam a componentes reutilizáveis.
  2. scope: true- a diretiva cria um novo escopo filho que herda prototipicamente do escopo pai. Se mais de uma diretiva (no mesmo elemento DOM) solicitar um novo escopo, apenas um novo escopo filho será criado. Como temos herança prototípica "normal", isso é como ng-include e ng-switch, portanto, tenha cuidado com a ligação de dados bidirecional com as primitivas do escopo pai e com o escopo filho oculto / sombreado das propriedades do escopo pai.
  3. scope: { ... }- a diretiva cria um novo escopo isolado / isolado. Não herda prototipicamente. Geralmente, é a melhor opção ao criar componentes reutilizáveis, pois a diretiva não pode ler ou modificar acidentalmente o escopo pai. No entanto, essas diretivas geralmente precisam acessar algumas propriedades do escopo pai. O hash do objeto é usado para configurar a ligação bidirecional (usando '=') ou ligação unidirecional (usando '@') entre o escopo pai e o escopo isolado. Também há '&' para vincular às expressões de escopo pai. Portanto, todos eles criam propriedades de escopo local que são derivadas do escopo pai. Observe que os atributos são usados ​​para ajudar a configurar a ligação - você não pode apenas fazer referência aos nomes de propriedades do escopo pai no hash do objeto; é necessário usar um atributo. Por exemplo, isso não funcionará se você desejar vincular à propriedade paiparentPropno escopo isolado: <div my-directive>e scope: { localProp: '@parentProp' }. Um atributo deve ser usado para especificar cada propriedade pai à qual a diretiva deseja vincular: <div my-directive the-Parent-Prop=parentProp>e scope: { localProp: '@theParentProp' }.
    Isole as __proto__referências do escopo Object. Isolar $ parent do escopo faz referência ao escopo pai, portanto, embora seja isolado e não herda prototipicamente do escopo pai, ainda é um escopo filho.
    Para a imagem abaixo temos
    <my-directive interpolated="{{parentProp1}}" twowayBinding="parentProp2">e
    scope: { interpolatedProp: '@interpolated', twowayBindingProp: '=twowayBinding' }
    também, assumir a directiva faz isso na sua função de interligação: scope.someIsolateProp = "I'm isolated"
    escopo isolado
    Para mais informações sobre escopos isolar ver http://onehungrymind.com/angularjs-sticky-notes-pt-2-isolated-scope/
  4. transclude: true- a diretiva cria um novo escopo filho "transcluído", que herda prototipicamente do escopo pai. O escopo transcluído e o isolado (se houver) são irmãos - a propriedade $ parent de cada escopo faz referência ao mesmo escopo pai. Quando um escopo transcluído e um isolado existem, a propriedade $$ scopeSibling do escopo isolado fará referência ao escopo transcluído. Não conheço nenhuma nuance com o escopo transcluído.
    Para a figura abaixo, assuma a mesma diretiva acima com esta adição:transclude: true
    escopo transcluído

Esse violino tem uma showScope()função que pode ser usada para examinar um escopo isolado e transcluído. Veja as instruções nos comentários no violino.


Sumário

Existem quatro tipos de escopos:

  1. herança de escopo prototípico normal - ng-include, ng-switch, ng-controller, diretiva com scope: true
  2. herança de escopo prototípico normal com uma cópia / atribuição - ng-repeat. Cada iteração do ng-repeat cria um novo escopo filho, e esse novo escopo filho sempre obtém uma nova propriedade.
  3. isolar escopo - diretiva com scope: {...}. Este não é um protótipo, mas '=', '@' e '&' fornecem um mecanismo para acessar as propriedades do escopo pai, por meio de atributos.
  4. escopo transcluído - diretiva com transclude: true. Este também é uma herança de escopo prototípico normal, mas também é um irmão de qualquer escopo isolado.

Para todos os escopos (prototípicos ou não), o Angular sempre rastreia um relacionamento pai-filho (ou seja, uma hierarquia), através das propriedades $ parent e $$ childHead e $$ childTail.

Os diagramas foram gerados com Arquivos "* .dot", que estão no github . " Aprendendo JavaScript com gráficos de objetos ", de Tim Caswell, foi a inspiração para o uso do GraphViz nos diagramas.

Mark Rajcok
fonte
48
Artigo incrível, muito longo para uma resposta SO, mas muito útil de qualquer maneira. Coloque-o no seu blog antes que um editor o reduza.
Iwein
43
Eu coloquei uma cópia no wiki do AngularJS .
Mark Rajcok
3
Correção: "Isole as __proto__referências do escopo Object." em vez disso, deve ser "Isolar as __proto__referências do escopo como um objeto Scope". Portanto, nas duas últimas imagens, as caixas laranja "Objeto" devem ser caixas "Escopo".
Mark Rajcok
15
Esta resposta deve ser incluída no guia angularjs. Isto é muito mais didático ...
Marcelo De Zen
2
O wiki me deixa perplexo, primeiro ele lê: "A cadeia de protótipos é consultada porque o objeto não foi encontrado no childScope." e então ele lê: "Se definirmos childScope.propertyX, a cadeia de protótipos não será consultada.". O segundo implica uma condição, enquanto o primeiro não.
Stephane
140

Eu não quero competir com a resposta de Mark, mas só queria destacar a peça que finalmente fez tudo clicar como alguém novo na herança Javascript e em sua cadeia de protótipos .

Somente as leituras de propriedades pesquisam a cadeia de protótipos, não as gravações. Então, quando você define

myObject.prop = '123';

Ele não procura a cadeia, mas quando você define

myObject.myThing.prop = '123';

há uma leitura sutil acontecendo nessa operação de gravação que tenta procurar o myThing antes de gravar em seu suporte. É por isso que escrever para object.properties a partir da criança chega aos objetos dos pais.

Scott Driscoll
fonte
12
Embora este seja um conceito muito simples, pode não ser muito óbvio, já que, acredito, muitas pessoas sentem falta dele. Bem colocado.
precisa saber é o seguinte
3
Excelente observação. Afasto, a resolução de uma propriedade não-objeto não envolve uma leitura, enquanto a resolução de uma propriedade de objeto não.
Stephane
1
Por quê? Qual é a motivação para que as gravações de propriedades não subam na cadeia de protótipos? Parece loucura ...
Jonathan.
1
Seria ótimo se você adicionasse um exemplo muito simples.
tylik
2
Aviso que faz procurar a cadeia de protótipos para setters . Se nada for encontrado, ele cria uma propriedade no receptor.
Bergi 11/01/19
21

Gostaria de adicionar um exemplo de herança prototípica com javascript à resposta do @Scott Driscoll. Usaremos o padrão clássico de herança com Object.create (), que faz parte da especificação do EcmaScript 5.

Primeiro, criamos a função de objeto "Pai"

function Parent(){

}

Em seguida, adicione um protótipo à função de objeto "Pai"

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

Criar função de objeto "Filho"

function Child(){

}

Atribuir protótipo filho (tornar o protótipo filho herdado do protótipo pai)

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

Atribua o construtor de protótipo "filho" adequado

Child.prototype.constructor = Child;

Adicione o método "changeProps" a um protótipo filho, que reescreverá o valor da propriedade "primitiva" no objeto Filho e alterará o valor "object.one" nos objetos Filho e Pai.

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

Iniciar objetos Pai (pai) e Filho (filho).

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

Chamar método changeProps filho (filho)

son.changeProps();

Verifique os resultados.

A propriedade primitiva pai não foi alterada

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

Propriedade primitiva filho alterada (reescrita)

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

Objeto pai e filho. Uma propriedade foi alterada

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

Exemplo de trabalho aqui http://jsbin.com/xexurukiso/1/edit/

Mais informações sobre Object.create aqui https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/create

tylik
fonte