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
- defina objetos no pai para o seu modelo e faça referência a uma propriedade desse objeto no filho: parentObj.someProp
- use $ parent.parentScopeProperty (nem sempre possível, mas mais fácil que 1. sempre que possível)
- 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-include
e ng-if
tudo 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:
(Observe que, para economizar espaço, mostro o anArray
objeto 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.
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.)
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.
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.
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.
Digitar (por exemplo, "77") na primeira caixa de texto de entrada faz com que o escopo filho obtenha uma nova myPrimitive
propriedade de escopo que oculta / oculta a propriedade do escopo pai com o mesmo nome. Provavelmente não é isso que você deseja / espera.
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.
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).
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 num
propriedade independente da matriz myArrayOfPrimitives:
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:
(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
- 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.
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.
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 paiparentProp
no 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"
Para mais informações sobre escopos isolar ver http://onehungrymind.com/angularjs-sticky-notes-pt-2-isolated-scope/
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
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:
- herança de escopo prototípico normal - ng-include, ng-switch, ng-controller, diretiva com
scope: true
- 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.
- 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.
- 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 graphvizArquivos "* .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.