Які нюанси обсягу прототипічного / прототипного успадкування в AngularJS?


1028

На сторінці довідкового діапазону API зазначено:

Область може успадковувати з батьківської області.

На сторінці "Посібник для розробників" розміщено :

Область дії (прототипно) успадковує властивості з її батьківської області.

  • Отже, чи дочірній обсяг завжди успадковується від його батьківського простору?
  • Чи є винятки?
  • Коли це успадковується, чи завжди це нормальне наслідування прототиповим JavaScript?

Відповіді:


1740

Швидка відповідь :
Дочірній обсяг, як правило, прототипно успадковує від своєї батьківської області, але не завжди. Одним винятком із цього правила є директива з scope: { ... }- це створює область "ізоляції", яка не успадковується прототипічно. Ця конструкція часто використовується при створенні директиви "компонент для багаторазового використання".

Що стосується нюансів, то успадкування сфери звичайно прямолінійно ... поки вам не потрібно двостороння прив'язка даних (тобто елементи форми, ng-модель) у дочірній області. Ng-повтор, ng-перемикач і 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-повторення та ng-включення нижче.

приховування майна

Припустимо, ми зробимо це так:

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

Звертається до прототипу ланцюга, оскільки об'єкти (anArray та anObject) не знайдені у дочірньому огляді. Об'єкти знаходять у parentScope, а значення властивостей оновлюються на вихідні об'єкти. До дитиниScope не додано нових властивостей; нові об’єкти не створюються. (Зверніть увагу, що в JavaScript масиви та функції також є об'єктами.)

слідувати прототипу ланцюга

Припустимо, ми зробимо це так:

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

Лабораторія прототипу не проводиться, і дочірня область отримує дві нові властивості об'єкта, які приховують / затінюють властивості об'єкта parentScope з тими ж іменами.

більше приховування майна

Винос:

  • Якщо ми читаємо childScope.propertyX, а childScope має властивістьX, то ланцюжок прототипу не проводиться.
  • Якщо ми встановимо childScope.propertyX, ланцюжок прототипу не проводиться.

Останній сценарій:

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

Спочатку ми видалили властивість childScope, потім, коли ми знову спробуємо отримати доступ до ресурсу, звертається до прототипу.

після вилучення майна дитини


Кутове спадкування

Учасники:

  • Наступні створюють нові області застосування та успадковують прототипно: ng-повтор, ng-include, ng-перемикач, ng-контролер, директива з scope: true, директива з transclude: true.
  • Далі створюється нова область, яка не успадковується прототипно: директива з scope: { ... }. Це натомість створює область "ізолювати".

Зауважте, за замовчуванням директиви не створюють нової області застосування - тобто за замовчуванням є scope: false.

ng-включати

Припустимо, у нас в контролері:

$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-включають сфери застосування дітей

Введення тексту (скажімо, "77") у перше вхідне текстове поле змушує дочірнє поле отримати нове myPrimitiveвластивість області, яке приховує / затінює властивість батьківського діапазону з тим самим іменем. Це, мабуть, не те, чого ти хочеш / очікуєш.

ng-включати з примітивом

Введення тексту (скажімо, "99") у друге поле введення тексту не призводить до появи нового дочірнього властивості. Оскільки tpl2.html пов'язує модель з властивістю об'єкта, прототипічне успадкування починається, коли ngModel шукає об'єкт myObject - він знаходить його в батьківській області.

ng-включати з об’єктом

Ми можемо переписати перший шаблон для використання $ 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/isissue/1267 .

ng-перемикач

Успадкування області ng-switch працює так само, як і ng-include. Отже, якщо вам потрібно двостороння прив'язка даних до примітиву в батьківській області, використовуйте $ parent або змініть модель, щоб бути об'єктом, а потім прив’яжіть до властивості цього об'єкта. Це дозволить уникнути приховування / тінізації властивостей батьківського діапазону.

Дивіться також AngularJS, прив'язуйте сферу корпусу комутатора?

ng-повторити

Ng-повтор працює трохи інакше. Припустимо, у нас в контролері:

$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-repet створює нову область, яка прототипічно успадковується від батьківської області, але також присвоює значення елемента новому властивості в новій дочірній області . (Ім'я нової властивості - це ім'я змінної циклу.) Ось що насправді має кутовий вихідний код для ng-повтору:

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

Якщо елемент є примітивним (як у myArrayOfPrimitive), по суті копія цього значення присвоюється новому дочірньому властивості області застосування. Зміна значення властивості дочірнього діапазону (тобто, використовуючи ng-модель, отже, дочірня область num) не змінює масив батьківських посилань на область застосування. Отже, у першому ng-повторі вище кожен дочірній область отримує numвластивість, незалежне від масиву myArrayOfPrimitive:

ng-повторити з примітивами

Це повторення ng не працюватиме (як ви хочете / очікуєте). Введення в текстові поля змінює значення в сірих полях, які видно лише в області дочірнього діапазону. Ми хочемо, щоб входи впливали на масив myArrayOfPrimitive, а не на дочірнє примітивне властивість. Для цього нам потрібно змінити модель на масив об’єктів.

Отже, якщо елемент є об’єктом, новій властивості дочірнього діапазону присвоюється посилання на оригінальний об'єкт (а не його копія). Зміна вартості майна дитини Scope (тобто, використовуючи нг-модель, отже obj.num) робить змінити об'єкт посилання області дій батька. Отже, у другому ng-повторі вище, ми маємо:

ng-повтор із об’єктами

(Я пофарбував один рядок сірим лише для того, щоб було зрозуміло, куди йде.)

Це працює як очікувалося. Введення в текстові поля змінює значення в сірих полях, які видно як дочірньому, так і батьківському діапазонам.

Дивіться також Складність з ng-моделлю, ng-повтором та входами та https://stackoverflow.com/a/13782671/215945

ng-контролер

Вкладення контролерів, що використовують ng-контролер, призводить до нормального успадкування прототипу, як і ng-include і ng-switch, тому застосовуються ті самі методи. Однак "два контролери вважають поганою формою обміну інформацією через спадщину $ domain" - 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__посилання на область Об'єкт. $ Батьківський ізолятор області посилається на батьківський діапазон, тому, хоча він ізольований і не успадковує прототипно від батьківської області, він все ще є дочірньою областю.
    Для наведеної нижче картини ми маємо,
    <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- Директива створює нову "переключену" дочірню область, яка прототипічно успадковується від батьківської області. Переключений та ізольований обсяг (якщо такий є) є побратимами - властивість $ батьків кожної області посилається на одну і ту ж батьківську область. Коли переключені і ізоляційні області існують, ізолюйте властивість області $$ nextSibling буде посилатися на трансклюдовану область. Мені невідомі будь-які нюанси із виключеною сферою застосування.
    Для малюнка нижче візьміть ту саму директиву, що і вище, з цим доповненням:transclude: true
    виключається сфера застосування

Ця скрипка має showScope()функцію, яку можна використовувати для дослідження ізоляту та переключеної області. Дивіться інструкцію в коментарях у скрипці.


Підсумок

Існує чотири типи областей застосування:

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

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

Діаграми генерували за допомогою Файли "* .dot", які знаходяться на github . Тім Касвелл " Навчання JavaScript за допомогою об'єктних графіків " був натхненником для використання GraphViz для діаграм.


48
Дивовижна стаття, занадто довга для відповіді ТА, але дуже корисна в будь-якому випадку. Будь ласка, розмістіть його у своєму блозі, перш ніж редактор скоротить його до розміру.
iwein

43
Я поклав копію на вікі AngularJS .
Марк Райкок

3
Виправлення: "Виділити __proto__посилання на область Об'єкт." замість цього має бути "Ізолювати __proto__посилання на область об'єкта". Отже, на останніх двох малюнках помаранчеві поля "Об'єкт" мають бути замість поля "Обсяг".
Марк Райкок

15
Ця відповідність повинна бути включена в посібник з angularjs. Це набагато більш дидактично ...
Marcelo De Zen

2
Вікі залишає мене спантеличеною, спочатку на ній написано: "Прототипний ланцюг проводиться з огляду на те, що об'єкт не знайдений у дочірньому огляді". а потім він говорить: "Якщо ми встановимо 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.prototype.constructor = Child;

Додайте метод "changeProps" до дочірнього прототипу, який перепише "примітивне" значення властивості в дочірній об'єкт і змінить значення "object.one" як у дочірніх, так і батьківських об'єктах

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

Ініціювати предмети Батько (тато) та Дитина (син).

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

Виклик методу зміни дитини (сина)

son.changeProps();

Перевірте результати.

Батьківська примітивна властивість не змінилася

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

Змінено примітивне властивість дитини (переписано)

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

Батьківські та дочірні властивості object.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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.