Каковы нюансы объема прототипного / прототипического наследования в AngularJS?
На странице "Область действия API" написано:
Область может наследоваться от родительской области.
На странице Scope Guide Developer говорится:
Область (прототипически) наследует свойства от своей родительской области.
Итак, всегда ли дочерняя область прототипически наследуется от родительской области? Есть исключения? Когда это наследуется, всегда ли это нормальное наследование прототипа JavaScript?
3 ответа
Быстрый ответ:
Дочерняя область обычно прототипически наследуется от родительской области, но не всегда. Единственным исключением из этого правила является директива с scope: { ... }
- это создает "изолированную" область, которая не наследуется прототипами. Эта конструкция часто используется при создании директивы "повторно используемый компонент".
Что касается нюансов, то наследование области обычно прямолинейно... до тех пор, пока вам не понадобится двухстороннее связывание данных (т.е. элементы формы, ng-модель) в дочерней области. Ng-repeat, ng-switch и ng-include могут сбить вас с толку, если вы попытаетесь выполнить привязку к примитиву (например, число, строка, логическое значение) в родительской области изнутри дочерней области. Это не работает так, как ожидает большинство людей. Дочерняя область действия получает свое собственное свойство, которое скрывает / скрывает родительское свойство с тем же именем. Ваши обходные пути
- определить объекты в родительском для вашей модели, а затем сослаться на свойство этого объекта в дочернем: parentObj.someProp
- использовать $parent.parentScopeProperty (не всегда возможно, но проще, чем 1. где это возможно)
- определить функцию в родительской области и вызвать ее из дочернего элемента (не всегда возможно)
Новые разработчики AngularJS часто не понимают, что ng-repeat
, ng-switch
, ng-view
, ng-include
а также ng-if
Все они создают новые дочерние области, поэтому проблема часто проявляется, когда задействованы эти директивы. (См. Этот пример для быстрой иллюстрации проблемы.)
Эту проблему с примитивами можно легко избежать, следуя "лучшей практике" всегда иметь "." в ваших моделях ng - смотрите, стоит 3 минуты. Миско демонстрирует проблему примитивного связывания с ng-switch
,
Иметь '.' в ваших моделях будет гарантировать, что наследование прототипа находится в игре. Итак, используйте
<input type="text" ng-model="someObj.prop1">
<!--rather than
<input type="text" ng-model="prop1">`
-->
Длинный ответ:
Наследование прототипов JavaScript
Также размещен на вики AngularJS: https://github.com/angular/angular.js/wiki/Understanding-Scopes
Важно сначала иметь четкое представление о наследовании прототипов, особенно если вы работаете на серверной основе и более знакомы с классическим наследованием. Итак, давайте сначала рассмотрим это.
Предположим, parentScope имеет свойства aString, aNumber, anArray, anObject и aFunction. Если childScope прототипически наследуется от parentScope, мы имеем:
(Обратите внимание, что для экономии места я показываю anArray
объект как один синий объект с тремя значениями, а не как синий объект с тремя отдельными литералами серого цвета.)
Если мы попытаемся получить доступ к свойству, определенному в parentScope, из дочерней области, JavaScript сначала будет искать в дочерней области, а не находить свойство, затем искать в унаследованной области и находить свойство. (Если он не найдет свойство в parentScope, он продолжит цепочку прототипов... вплоть до корневой области). Итак, все это правда:
childScope.aString === 'parent string'
childScope.anArray[1] === 20
childScope.anObject.property1 === 'parent prop1'
childScope.aFunction() === 'parent output'
Предположим, что мы тогда делаем это:
childScope.aString = 'child string'
Цепочка прототипов не используется, и в childScope добавляется новое свойство aString. Это новое свойство скрывает / скрывает свойство parentScope с тем же именем. Это станет очень важным, когда мы обсудим ng-repeat и ng-include ниже.
Предположим, что мы тогда делаем это:
childScope.anArray[1] = '22'
childScope.anObject.property1 = 'child prop1'
С цепочкой прототипов обращаются, потому что объекты (anArray и anObject) не найдены в childScope. Объекты находятся в parentScope, а значения свойств обновляются в исходных объектах. Новые свойства не добавляются в childScope; новые объекты не создаются. (Обратите внимание, что в JavaScript массивы и функции также являются объектами.)
Предположим, что мы тогда делаем это:
childScope.anArray = [100, 555]
childScope.anObject = { name: 'Mark', country: 'USA' }
Цепочка прототипов не используется, и дочерняя область получает два новых свойства объекта, которые скрывают / скрывают свойства объекта parentScope с одинаковыми именами.
Takeaways:
- Если мы читаем childScope.propertyX, а childScope имеет свойство X, то цепочка прототипов не используется.
- Если мы установим childScope.propertyX, цепочка прототипов не используется.
Последний сценарий:
delete childScope.anArray
childScope.anArray[1] === 22 // true
Сначала мы удалили свойство childScope, а затем при повторном обращении к свойству просматриваем цепочку прототипов.
Угловое наследование
Претенденты:
- Следующие создают новые области и наследуют прототипы: 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 генерирует новую дочернюю область, которая прототипно наследуется от родительской области.
Ввод (скажем, "77") в первое текстовое поле ввода заставляет дочернюю область получить новый myPrimitive
свойство scope, которое скрывает / скрывает родительское свойство scope с тем же именем. Это, вероятно, не то, что вы хотите / ожидаете.
Ввод (скажем, "99") во второе текстовое поле ввода не приводит к появлению нового дочернего свойства. Поскольку tpl2.html связывает модель со свойством объекта, наследование прототипа включается, когда ngModel ищет объект myObject - он находит его в родительской области видимости.
Мы можем переписать первый шаблон для использования $parent, если мы не хотим изменять нашу модель с примитива на объект:
<input ng-model="$parent.myPrimitive">
Ввод (скажем, "22") в это текстовое поле ввода не приводит к появлению нового дочернего свойства. Теперь модель привязана к свойству родительской области (поскольку $ parent - это дочерняя область, которая ссылается на родительскую область).
Для всех областей (прототип или нет) Angular всегда отслеживает родительско-дочерние отношения (т. Е. Иерархию) через свойства области $parent, $$childHead и $$childTail. Обычно я не показываю эти свойства области на диаграммах.
Для сценариев, где элементы формы не задействованы, другое решение состоит в том, чтобы определить функцию в родительской области видимости для изменения примитива. Затем убедитесь, что дочерний элемент всегда вызывает эту функцию, которая будет доступна для дочерней области благодаря наследованию прототипа. Например,
// in the parent scope
$scope.setMyPrimitive = function(value) {
$scope.myPrimitive = value;
}
Вот пример скрипта, который использует этот подход "родительской функции". (Скрипка была написана как часть этого ответа: /questions/17767988/ng-include-zastavlyaet-blok-kontrollera-pererisovyivatsya/17767994#17767994.)
См. Также /questions/44953529/chto-takoe-angularjs-sposob-dlya-sbora-dannyih-so-mnogih-vhodov/44953579#44953579 и https://github.com/angular/angular.js/issues/1267.
нг-переключатель
Наследование области действия ng-switch работает так же, как ng-include. Поэтому, если вам требуется двусторонняя привязка данных к примитиву в родительской области, используйте $ parent или измените модель на объект, а затем привяжите к свойству этого объекта. Это позволит избежать скрытия / затенения дочерней области свойств родительской области.
Смотрите также AngularJS, связать область применения коммутатора?
нг-повтор
Нг-повтор работает немного по-другому. Предположим, у нас есть в нашем контроллере:
$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-модели, следовательно, дочерняя область num
) не изменяет массив ссылок родительской области. Таким образом, в первом ng-repeat выше, каждая дочерняя область получает num
свойство, которое не зависит от массива myArrayOfPrimitives:
Это ng-repeat не будет работать (как вы хотите / ожидаете). Ввод в текстовые поля изменяет значения в серых полях, которые видны только в дочерних областях. Мы хотим, чтобы входные данные влияли на массив myArrayOfPrimitives, а не на примитивное свойство дочерней области. Для этого нам нужно изменить модель на массив объектов.
Таким образом, если элемент является объектом, ссылка на исходный объект (не на копию) назначается новому дочернему свойству области. Изменение значения свойства дочерней области (т. Е. С использованием ng-модели, следовательно, obj.num
) действительно изменяет объект ссылки родительской области. Итак, во втором нг-повторе выше мы имеем:
(Я нарисовал одну линию серым, чтобы было ясно, куда она идет.)
Это работает как ожидалось. Ввод в текстовые поля изменяет значения в серых полях, которые видны как для дочерней, так и для родительской областей.
См. Также трудности с ng-моделью, ng-repeat и входными данными и /questions/44953529/chto-takoe-angularjs-sposob-dlya-sbora-dannyih-so-mnogih-vhodov/44953579#44953579
нг-контроллер
Вложение контроллеров с использованием ng-controller приводит к нормальному наследованию прототипов, так же, как ng-include и ng-switch, поэтому применяются те же методы. Однако "считается плохой формой для двух контроллеров обмениваться информацией через наследование $scope" - http://onehungrymind.com/angularjs-sticky-notes-pt-1-architecture/ Для обмена данными между службами необходимо использовать службу контроллеры вместо.
(Если вы действительно хотите обмениваться данными через наследование области контроллера, вам ничего не нужно делать. Дочерняя область будет иметь доступ ко всем свойствам родительской области. См. Также Порядок загрузки контроллера отличается при загрузке или навигации)
директивы
- дефолт (
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"
Для получения дополнительной информации об изолирующих областях см. http://onehungrymind.com/angularjs-sticky-notes-pt-2-isolated-scope/transclude: true
- директива создает новую "включенную" дочернюю область, которая прототипически наследуется от родительской области. Трансклюзивная и изолированная область (если таковые имеются) являются родственными - свойство $ parent каждой области ссылается на одну и ту же родительскую область. Если существуют оба включенных и изолированного контекста, свойство изолирующего контекста $$nextSibling будет ссылаться на трансклюзивную область. Мне не известны какие-либо нюансы с включенной областью.
Для рисунка ниже примите ту же директиву, что и выше, с этим дополнением:transclude: true
Эта скрипка имеет showScope()
функция, которая может быть использована для изучения изолированного и включенного объема. Смотрите инструкции в комментариях в скрипке.
Резюме
Существует четыре типа областей:
- обычное наследование прототипа - ng-include, ng-switch, ng-controller, директива с
scope: true
- обычное наследование прототипа с копированием / присваиванием - ng-repeat. Каждая итерация ng-repeat создает новую дочернюю область, и эта новая дочерняя область всегда получает новое свойство.
- изолировать область действия - директива с
scope: {...}
, Это не прототип, но '=', '@' и '&' предоставляют механизм доступа к свойствам родительской области через атрибуты. - включенная область действия - директива с
transclude: true
, Этот тип также является обычным наследованием прототипной области, но он также является родственным элементом любой изолированной области.
Для всех областей (прототип или нет) Angular всегда отслеживает отношения родитель-потомок (то есть иерархию) через свойства $ parent и $$ childHead и $$childTail.
Диаграммы были сгенерированы с помощью графических файлов "*.dot", которые находятся на github. " Изучение JavaScript с помощью графов объектов " Тима Касвелла послужило вдохновением для использования GraphViz для диаграмм.
Я ни в коем случае не хочу конкурировать с ответом Марка, но просто хотел выделить часть, которая, в конце концов, заставила все щелкнуть как новичка в наследовании Javascript и его цепочке прототипов.
Только свойство читает поиск по цепочке прототипов, а не пишет. Итак, когда вы установите
myObject.prop = '123';
Это не ищет цепочку, но когда вы установите
myObject.myThing.prop = '123';
внутри этой операции записи происходит тонкое чтение, которое пытается найти myThing перед записью в свой реквизит. Так вот почему запись в object.properties от дочернего объекта получает объекты родительского объекта.
Я хотел бы добавить пример прототипического наследования с 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"
Child.prototype.constructor = Child;
Добавьте метод "changeProps" к дочернему прототипу, который перезапишет значение свойства "примитив" в дочернем объекте и изменит значение "object.one" как в дочернем, так и в родительском объектах.
Child.prototype.changeProps = function(){
this.primitive = 2;
this.object.one = 2;
};
Инициировать объекты Parent (папа) и Child (сын).
var dad = new Parent();
var son = new Child();
Вызовите метод ChildPro (сын) changeProps
son.changeProps();
Проверьте результаты.
Родительское примитивное свойство не изменилось
console.log(dad.primitive); /* 1 */
Свойство дочернего примитива изменено (переписано)
console.log(son.primitive); /* 2 */
Свойства родительского и дочернего объекта.one изменены
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