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

1028

Страница « Область действия API» гласит:

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

На странице Scope Guide Developer говорится:

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

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

Ответы:

1741

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

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

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

Новые разработчики 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, директива with scope: true, директива with 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свойство области, которое скрывает / скрывает родительское свойство области с тем же именем. Это, вероятно, не то, что вы хотите / ожидаете.

нг-включить с примитивом

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

нг-включить с объектом

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

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

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

ng-include с $ parent

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

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

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

Вот пример скрипта, который использует этот подход «родительская функция». (Скрипка была написана как часть этого ответа: https://stackoverflow.com/a/14104318/215945 .)

См. Также https://stackoverflow.com/a/13782671/215945 и 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-повторе каждая numдочерняя область получает свойство, которое не зависит от массива myArrayOfPrimitives:

нг-повтор с примитивами

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

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

нг-повтор с объектами

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

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

См. Также Трудности с ng-моделью, ng-repeat и входными данными и https://stackoverflow.com/a/13782671/215945

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

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

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

директивы

  1. default ( scope: false) - директива не создает новую область видимости, поэтому здесь нет наследования. Это легко, но также и опасно, потому что, например, директива может подумать, что создает новое свойство в области действия, когда фактически забивает существующее свойство. Это не очень хороший выбор для написания директив, предназначенных для повторного использования.
  2. scope: true- директива создает новую дочернюю область, которая прототипно наследуется от родительской области. Если более одной директивы (для одного элемента DOM) запрашивает новую область, создается только одна новая дочерняя область. Так как у нас есть «нормальное» наследование прототипов, это похоже на ng-include и ng-switch, так что будьте осторожны с двухсторонней привязкой данных к родительским областям примитивов и дочерним областям скрытия / теневого копирования свойств родительских областей.
  3. 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/
  4. transclude: true- директива создает новую «включенную» дочернюю область, которая прототипически наследуется от родительской области. Трансклюзивная и изолированная область (если таковые имеются) являются одноуровневыми - свойство $ parent каждой области ссылается на одну и ту же родительскую область. Если существуют оба включенных и изолированного контекста, свойство изолирующего контекста $$ nextSibling будет ссылаться на трансклюзивную область. Мне не известны какие-либо нюансы с включенной областью.
    Для рисунка ниже примите ту же директиву, что и выше, с этим дополнением:transclude: true
    заключенная область

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


Резюме

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

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

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

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

Марк Райкок
источник
48
Потрясающая статья, слишком длинная для SO ответа, но в любом случае очень полезная. Пожалуйста, поместите его в свой блог, прежде чем редактор уменьшит его размер.
Iwein
43
Я положил копию на вики AngularJS .
Марк Райкок
3
Исправление: «Изолировать __proto__ссылки области видимости объекта». вместо этого должно быть «Изолировать __proto__ссылки на область видимости объекта Scope». Таким образом, на последних двух рисунках оранжевые поля «Объект» должны вместо этого быть «областями видимости».
Марк Райкок
15
Этот ответ должен быть включен в руководство angularjs. Это гораздо более диадический ...
Марсело Де Дзен,
2
Вики оставляет меня озадаченным, сначала гласит: «С цепочкой прототипов обращаются, потому что объект не найден в childScope». и затем он гласит: «Если мы установим childScope.propertyX, цепочка прототипов не используется». Второй подразумевает условие, тогда как первый - нет.
Стефан
140

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

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

myObject.prop = '123';

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

myObject.myThing.prop = '123';

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

Скотт Дрисколл
источник
12
Хотя это очень простая концепция, она может быть не очень очевидной, так как, я полагаю, многие люди упускают ее. Хорошо сказано.
moljac024
3
Отличное замечание. Я убираю, разрешение свойства не объекта не включает чтение, тогда как разрешение свойства объекта делает это.
Стефан
1
Почему? Какова мотивация для записи собственности, которая не идет вверх по цепочке прототипов? Это кажется сумасшедшим ...
Джонатан.
1
Было бы здорово, если бы вы добавили очень простой пример.
Тылик
2
Обратите внимание, что он выполняет поиск в цепочке прототипов для сеттеров . Если ничего не найдено, создается свойство на получателе.
Берги
21

Я хотел бы добавить пример прототипического наследования с 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

Тылик
источник