Каковы нюансы объема прототипа/прототипного наследования в AngularJS?

Ссылка на справочную страницу API:

Область может наследоваться из родительской области.

Страница "Руководство разработчика" :

Объект (прототипно) наследует свойства из его родительской области.

Итак, всегда ли прототипна наследственная область дочернего объекта из его родительской области? Есть ли исключения? Когда он наследует, всегда ли это обычное прототипное наследование JavaScript?

Ответ 1

Быстрый ответ:
Дочерняя область обычно прототипически наследуется от родительской области, но не всегда. Единственным исключением из этого правила является директива с scope: { ... } - это создает область выделения, которая не прототипически наследуется. Эта конструкция часто используется при создании директивы "многоразового компонента".

Что касается нюансов, наследование области обычно справедливо... пока вам не понадобится привязка двухсторонних данных (т.е. элементы формы, ng-model) в области дочерних объектов. Ng-repeat, ng-switch и ng-include могут отключить вас, если вы попытаетесь связать с примитивным (например, числом, строкой, булевым) в родительской области внутри области содержимого. Это не работает так, как многие ожидают, что он должен работать. Область child получает свое собственное свойство, которое скрывает/затеняет родительское свойство с тем же именем. Ваши обходные пути

  • определить объекты в родительском для вашей модели, а затем ссылаться на свойство этого объекта в дочернем объекте: parentObj.someProp
  • используйте $parent.parentScopeProperty(не всегда возможно, но проще, чем 1. где возможно).
  • определить функцию в родительской области и вызвать ее из дочернего элемента (не всегда возможно)

Новые разработчики AngularJS часто не понимают, что ng-repeat, ng-switch, ng-view, ng-include и ng-if все создают новые дочерние области, поэтому проблема часто возникает, когда задействованы эти директивы. (См. этот пример для быстрой иллюстрации проблемы.)

Эту проблему с примитивами можно легко избежать, следуя "лучшей практике" всегда иметь "." . в ваших ng-моделях - смотрите 3 минуты. Misko демонстрирует проблему примитивного связывания с ng-switch.

Наличие символа '.' в ваших моделях будет гарантировать, что прототипное наследование находится в игре. Итак, используйте

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

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


Ответ L-o-n-g:

Прототипное наследование JavaScript

Также размещен на вики-странице AngularJS: https://github.com/angular/angular.js/wiki/Understanding-Scopes

Важно сначала иметь четкое представление о прототипальном наследовании, особенно если вы исходите с серверного фона, и вы более знакомы с классовым наследованием. Поэтому давайте сначала рассмотрим это.

Предположим, что parentScope имеет свойства aString, aNumber, anArray, anObject и aFunction. Если childScope прототипически наследуется от parentScope, мы имеем:

prototypal inheritance

(Обратите внимание, что для экономии места я показываю объект anArray как единственный синий объект с тремя его значениями, а не с одним синим объектом с тремя отдельными серыми буквами.)

Если мы попытаемся получить доступ к свойству, определенному на родительском объекте, из области дочернего объекта, JavaScript сначала будет искать в области дочернего объекта, а не найти свойство, а затем просмотреть в унаследованном объеме и найти свойство. (Если он не нашел свойство в parentScope, он продолжит цепочку прототипов... вплоть до области корня). Итак, все это верно:

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

Предположим, что мы сделаем следующее:

childScope.aString = 'child string'

Цепочка прототипов не запрашивается, и новое свойство aString добавляется в childScope. Это новое свойство скрывает/затеняет свойство parentScope с тем же именем. Это станет очень важным, когда мы обсудим ng-repeat и ng-include ниже.

property hiding

Предположим, что мы сделаем следующее:

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

Прослеживается цепочка прототипов, потому что объекты (anArray и anObject) не найдены в childScope. Объекты находятся в parentScope, а значения свойств обновляются на исходных объектах. Никакие новые свойства не добавляются в childScope; новые объекты не создаются. (Обратите внимание, что в JavaScript-массивы и функции также являются объектами.)

follow the prototype chain

Предположим, что мы сделаем следующее:

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

Цепочка прототипов не запрашивается, а область с правами пользователя получает два новых свойства объекта, которые скрывают/тятят свойства объекта parentScope с одинаковыми именами.

more property hiding

Takeaways:

  • Если мы читаем childScope.propertyX, а childScope имеет свойствоX, то цепочку прототипов не проконсультируют.
  • Если мы установим childScope.propertyX, цепочку прототипов не проконсультируют.

Последний сценарий:

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

Сначала мы удалили свойство childScope, затем, когда мы снова попытаемся получить доступ к свойству, проконсультируйтесь с цепочкой прототипов.

after removing a child property


Angular Наследование области

Участники:

  • Следующие создаются новые области и наследуют прототип: ng-repeat, ng-include, ng-switch, ng-controller, директиву с scope: true, директиву с transclude: true.
  • Далее создается новая область, которая не наследует прототип: директива с scope: { ... }. Вместо этого создается "изолировать" область.

Обратите внимание, что по умолчанию директивы не создают новую область действия - то есть по умолчанию это scope: false.

нг-включают в себя

Предположим, что мы имеем в нашем контроллере:

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

И в нашем 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>

Каждый ng-include создает новую дочернюю область, которая прототипно наследуется от родительской области.

ng-include child scopes

Ввод текста (скажем, "77" ) в первое текстовое поле ввода приводит к тому, что дочерняя область получает новое свойство области myPrimitive, которое скрывает/затеняет свойство родительской области с тем же именем. Вероятно, это не то, что вы хотите/ожидаете.

ng-include with a primitive

Ввод текста (скажем, "99" ) во второе текстовое поле ввода не приводит к новому дочернему свойству. Поскольку tpl2.html привязывает модель к свойству объекта, прототипное наследование срабатывает, когда ngModel ищет объект myObject - он находит его в родительской области.

ng-include with an object

Мы можем переписать первый шаблон для использования $parent, если мы не хотим менять нашу модель из примитива в объект:

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

Ввод текста (скажем, "22" ) в это текстовое поле ввода не приводит к новому дочернему свойству. Теперь модель привязана к свойству родительской области (поскольку $parent - это свойство дочерней области, которое ссылается на родительскую область).

ng-include with $parent

Для всех областей (прототип или нет), Angular всегда отслеживает отношения родитель-потомок (то есть иерархию), через свойства области $parent, $$ childHead и $$ childTail. Обычно я не показываю эти свойства области на диаграммах.

Для сценариев, в которых элементы формы не задействованы, другим решением является определение функции в родительской области для изменения примитива. Затем убедитесь, что ребенок всегда вызывает эту функцию, которая будет доступна для дочерней области из-за прототипального наследования. Например.

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

Вот образец скрипта, который использует этот подход "родительской функции". (Скрипка была написана как часть этого ответа: fooobar.com/questions/2232/....)

См. также fooobar.com/questions/2233/... и https://github.com/angular/angular.js/issues/1267.

нг-переключатель

Наследование ng-switch работает так же, как ng-include. Поэтому, если для привязки данных к примитиву в родительской области требуется двухсторонняя привязка данных, используйте $parent или измените модель как объект, а затем привяжитесь к свойству этого объекта. Это позволит избежать скрытия/затенения дочерних объектов свойств родительской области.

См. также AngularJS, связать область с коммутационным футляром?

нг-повтора

Ng-repeat работает несколько иначе. Предположим, что мы имеем в нашем контроллере:

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

И в нашем 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>

Для каждого элемента/итерации ng-repeat создает новую область, которая прототипически наследуется от родительской области, , но также присваивает значение элемента новому свойству в новой области содержимого. (Имя нового свойства - это имя переменной цикла). Здесь исходный код Angular для ng-repeat на самом деле:

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

Если элемент является примитивным (как в myArrayOfPrimitives), по существу копия значения присваивается новому свойству дочерней области. Изменение значения свойства дочерней области (т.е. С использованием ng-model, следовательно, сфера child num) делает не изменение массива, ссылающегося на родительскую область. Таким образом, в первом ng-повторе выше каждая дочерняя область получает свойство num, которое не зависит от массива myArrayOfPrimitives:

ng-repeat with primitives

Этот ng-repeat не будет работать (как вы хотите/ожидаете). Ввод текстовых полей изменяет значения в серых прямоугольниках, которые видны только в дочерних областях. Мы хотим, чтобы входные данные влияли на массив myArrayOfPrimitives, а не на примитивное свойство дочерней области. Для этого нам нужно изменить модель как массив объектов.

Итак, если элемент является объектом, ссылка на исходный объект (а не на копию) присваивается новому свойству дочерней области. Изменение значения свойства дочерней области (т.е. С использованием ng-model, следовательно obj.num) выполняет изменение объекта, на который ссылается родительская область. Итак, во втором ng-повторе выше мы имеем:

ng-repeat with objects

(Я покрасил одну серию только для того, чтобы было ясно, куда она идет.)

Это работает так, как ожидалось. Ввод в текстовые поля изменяет значения в серых прямоугольниках, которые видны как дочерним, так и родительским областям.

См. также Сложность с ng-моделью, ng-repeat и входами и fooobar.com/questions/2233/...

нг-контроллер

Контроллеры вложения с использованием ng-controller приводят к нормальному прототипному наследованию, точно так же, как ng-include и ng-switch, поэтому применяются те же методы. Однако "для двух контроллеров считается дурной формой для обмена информацией через наследование $scope" - http://onehungrymind.com/angularjs-sticky-notes-pt-1-architecture/ Служба должна использоваться для обмена данными между контроллерами.

(Если вы действительно хотите обмениваться данными с помощью наследования наследования контроллеров, вам нечего делать. Дочерняя область будет иметь доступ ко всем свойствам родительской области. См. Также Порядок загрузки контроллера отличается при загрузке или навигации)

директивы

  • default (scope: false) - директива не создает новую область видимости, поэтому здесь нет наследования. Это легко, но также и опасно, потому что, например, директива может думать, что она создает новое свойство в области видимости, когда на самом деле оно сбивает существующее свойство. Это не является хорошим выбором для написания директив, которые предназначены для использования в качестве компонентов многократного использования.
  • scope: true - директива создает новую дочернюю область, которая прототипически наследуется от родительской области. Если более чем одна директива (в том же элементе DOM) запрашивает новую область, создается только одна новая дочерняя область. Так как у нас есть "нормальное" прототипное наследование, это похоже на ng-include и ng-switch, поэтому будьте осторожны с привязкой двухсторонних данных к примитивам родительской области и скрытием/скрытием дочерних объектов свойств родительской области.
  • scope: { ... } - директива создает новый изолированный/изолированный объем. Это не прототипно наследует. Обычно это ваш лучший выбор при создании повторно используемых компонентов, поскольку директива не может случайно прочитать или изменить родительскую область. Однако для таких директив часто требуется доступ к нескольким свойствам родительской области. Хэш объекта используется для настройки двусторонней привязки (с использованием "=" ) или односторонней привязки (с использованием "@" ) между родительской областью и областью выделения. Существует также "&" для привязки к выражению родительской области. Таким образом, все они создают свойства локальной области, которые получены из родительской области. Обратите внимание, что атрибуты используются для настройки привязки - вы не можете просто ссылаться на имена свойств родительской области в хеше объекта, вы должны использовать атрибут. Например, это не будет работать, если вы хотите связать с родительским свойством parentProp в изолированной области: <div my-directive> и scope: { localProp: '@parentProp' }. Атрибут должен использоваться для указания каждого родительского свойства, которое директива хочет связать с: <div my-directive the-Parent-Prop=parentProp> и scope: { localProp: '@theParentProp' }.
    Изолировать область __proto__ ссылки Объект. Изолировать область $parent ссылается на родительскую область, поэтому, хотя она изолирована и не наследуется прототипом из родительской области, она по-прежнему является дочерней областью.
    На рисунке ниже мы имеем
    <my-directive interpolated="{{parentProp1}}" twowayBinding="parentProp2"> и
    scope: { interpolatedProp: '@interpolated', twowayBindingProp: '=twowayBinding' } Также предположим, что директива делает это в своей функции связывания: scope.someIsolateProp = "I'm isolated"
    isolated scope
    Дополнительные сведения о областях изоляции см. http://onehungrymind.com/angularjs-sticky-notes-pt-2-isolated-scope/
  • transclude: true - директива создает новую "трансключенную" дочернюю область, которая прототипически наследуется от родительской области. Переведенная и изолированная область (если есть) являются братьями и сестрами - свойство $parent каждой области ссылается на одну и ту же родительскую область. Когда существует сущность transcluded и изоляция, изолируйте свойство scope $$ nextSibling будет ссылаться на выделенную область. Я не знаю никаких нюансов с расширением.
    Для изображения ниже возьмите ту же директиву, что и выше, с этим добавлением: transclude: true
    transcluded scope

Этот fiddle имеет функцию showScope(), которая может быть использована для проверки изолированной и переводимой области. См. Инструкции в комментариях в скрипте.


Резюме

Существует четыре типа областей:

  • нормальное наследование прототипической области - ng-include, ng-switch, ng-controller, директива с scope: true
  • нормальное наследование прототипической области с копией/присваиванием - ng-repeat. Каждая итерация ng-repeat создает новую дочернюю область и новая область содержимого всегда получает новое свойство.
  • выделить область действия - директива с scope: {...}. Это не прототип, а '=', '@' и '&' предоставить механизм доступа к свойствам родительской области с помощью атрибутов.
  • transcluded scope - директива с transclude: true. Это тоже нормальное наследование прототипического объема, но оно также является братом любого изоляционного пространства.

Для всех областей (прототипных или нет), Angular всегда отслеживает отношения родитель-потомок (то есть иерархию) через свойства $parent и $$ childHead и $$ childTail.

Диаграммы были сгенерированы с помощью "*.dot", которые находятся на github. Тим Касуэлл "Изучение JavaScript с графическими объектами - вдохновляло использование GraphViz для диаграмм.

Ответ 2

Я никоим образом не хочу конкурировать с ответом Марка, но просто хотел выделить часть, которая, наконец, заставила все щелкнуть как кого-то нового в наследовании Javascript и цепочке прототипов.

Только свойство читает поиск цепи прототипа, а не пишет. Поэтому, когда вы устанавливаете

myObject.prop = '123';

Он не ищет цепочку, но когда вы устанавливаете

myObject.myThing.prop = '123';

там происходит тонкое чтение внутри этой операции записи, которая пытается найти myThing перед тем, как писать в свою поддержку. Поэтому, почему запись в object.properties из дочернего объекта происходит у родительских объектов.

Ответ 3

Я хотел бы добавить пример прототипического наследования с ответом javascript на @Scott Driscoll. Мы будем использовать классический шаблон наследования с Object.create(), который является частью спецификации EcmaScript 5.

Сначала мы создаем функцию объекта "Родитель"

function Parent(){

}

Затем добавьте прототип к функции объекта "Родитель"

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

Создать функцию объекта "Ребенок"

function Child(){

}

Назначить прототип ребенка (сделать прототип прототипа ребенка из родительского прототипа)

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

Назначить собственный конструктор прототипов "Ребенок"

Child.prototype.constructor = Child;

Добавьте метод "changeProps" к прототипу ребенка, который перепишет значение свойства "примитив" в объекте Child и изменит значение "object.one" как в дочерних, так и родительских объектах

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

Инициировать родительские (папы) и дочерние (дочерние) объекты.

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

Вызов метода ChildPage (сын) changeProps

son.changeProps();

Проверьте результаты.

Родительское примитивное свойство не изменилось

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

Изменено (переписано) дочернее примитивное свойство

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

Изменены свойства родителя и дочернего объекта.

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

Рабочий пример здесь http://jsbin.com/xexurukiso/1/edit/

Дополнительная информация о Object.create здесь https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/create