'this' vs $ scope в контроллерах AngularJS

1027

В разделе «Создание компонентов» на домашней странице AngularJS есть следующий пример:

controller: function($scope, $element) {
  var panes = $scope.panes = [];
  $scope.select = function(pane) {
    angular.forEach(panes, function(pane) {
      pane.selected = false;
    });
    pane.selected = true;
  }
  this.addPane = function(pane) {
    if (panes.length == 0) $scope.select(pane);
    panes.push(pane);
  }
}

Обратите внимание, как selectметод добавляется $scope, но addPaneметод добавляется в this. Если я изменю это на $scope.addPane, код сломается.

Документация говорит, что на самом деле есть разница, но она не упоминает, в чем разница:

Предыдущие версии Angular (до 1.0 RC) позволяли вам использовать метод thisвзаимозаменяемо $scope, но это уже не так. Внутри методов , определенных на объем thisи $scopeявляются взаимозаменяемыми (угловые наборы thisв $scope), но не иначе внутри конструктора контроллера.

Как работает thisи $scopeработает в контроллерах AngularJS?

Алексей Боронин
источник
Я нахожу это смущающим также. Когда представление задает контроллер (например, ng-controller = '...'), $ scope, связанный с этим контроллером, кажется, идет вместе с ним, потому что представление может получить доступ к свойствам $ scope. Но когда директива 'требует другого контроллера (а затем использует его в своей функции связывания), область $, связанная с этим другим контроллером, не приходит вместе с ней?
Марк Райкок
Эта запутанная цитата о "Предыдущих версиях ..." уже удалена? Тогда, может быть, обновление будет на месте?
Дмитрий Зайцев
Для модульного тестирования, если вы используете 'this' вместо '$ scope', вы не можете ввести контроллер с поддельной областью действия, и, следовательно, вы не можете выполнить модульное тестирование. Я не думаю, что это хорошая практика - использовать это.
Абентан

Ответы:

999

"Как работает thisи $scopeработает в контроллерах AngularJS?"

Краткий ответ :

  • this
    • Когда вызывается функция конструктора контроллера, thisэто контроллер.
    • Когда $scopeвызывается функция, определенная для объекта, thisэто «область действия, когда функция была вызвана». Это может (или не может!) Быть тем $scope, на котором определена функция. Итак, внутри функции thisи не$scope могут быть одинаковыми.
  • $scope
    • Каждый контроллер имеет связанный $scopeобъект.
    • Функция контроллера (конструктора) отвечает за установку свойств модели и функций / поведения ее связанного $scope.
    • Только методы, определенные на этом $scopeобъекте (и родительские объекты области, если прототипное наследование находится в игре) доступны из HTML / представления. Например, от ng-click, фильтры и т. Д.

Длинный ответ :

Функция контроллера - это функция конструктора JavaScript. Когда выполняется функция конструктора (например, когда загружается представление) this(то есть «контекст функции») устанавливается на объект контроллера. Так в функции конструктора контроллера «вкладки» при создании функции addPane

this.addPane = function(pane) { ... }

он создается на объекте контроллера, а не на $ scope. Представления не могут видеть функцию addPane - они имеют доступ только к функциям, определенным в $ scope. Другими словами, в HTML это не будет работать:

<a ng-click="addPane(newPane)">won't work</a>

После выполнения функции конструктора контроллера «tabs» имеем следующее:

после конструктора контроллера вкладок

Пунктирная черная линия указывает на наследование прототипа - область изолята, прототипно наследуемая от Scope . (Он не наследуется по прототипу от области действия, в которой директива встречалась в HTML.)

Теперь функция link директивы pane хочет связаться с директивой tabs (что на самом деле означает, что она должна каким-то образом влиять на вкладки, изолируя $ scope). События могут быть использованы, но другой механизм должен иметь директиву pane requireконтроллера tabs. (По-видимому, не существует механизма для директивы панели для requireвкладок $ scope.)

Итак, напрашивается вопрос: если у нас есть доступ только к контроллеру вкладок, как нам получить доступ к вкладкам, изолирующим $ scope (что мы действительно хотим)?

Ну, красная пунктирная линия - это ответ. «Область» функции addPane () (я имею в виду область / замыкания функции JavaScript здесь) дает функции доступ к вкладкам, изолирующим область $. То есть addPane () имеет доступ к «вкладкам IsolateScope» на диаграмме выше из-за замыкания, которое было создано при определении addPane (). (Если бы мы вместо этого определили addPane () на объекте $ scope вкладок, директива pane не будет иметь доступа к этой функции, и, следовательно, не будет иметь возможности связаться с вкладками $ scope.)

Чтобы ответить на другую часть вашего вопроса how does $scope work in controllers?:

Внутри функций, определенных в $ scope, thisустанавливается значение «действительная область $, где / когда была вызвана функция». Предположим, у нас есть следующий HTML:

<div ng-controller="ParentCtrl">
   <a ng-click="logThisAndScope()">log "this" and $scope</a> - parent scope
   <div ng-controller="ChildCtrl">
      <a ng-click="logThisAndScope()">log "this" and $scope</a> - child scope
   </div>
</div>

И ParentCtrl(только) имеет

$scope.logThisAndScope = function() {
    console.log(this, $scope)
}

Нажатие на первую ссылку покажет , что thisи $scopeто же, так как « сфера действует , когда функция была вызвана » является областью , связанная с ParentCtrl.

При нажатии на вторую ссылку покажет , thisи $scopeэто не то же самое, так как « сфера действует , когда функция была вызвана » является областью , связанный с ChildCtrl. Итак, здесь thisустановлено значение ChildCtrls $scope. Внутри метода $scopeвсе еще находится ParentCtrlобласть действия $.

скрипка

Я стараюсь не использовать thisвнутри функции, определенной в $ scope, так как становится непонятно, какая область $ затрагивается, особенно с учетом того, что ng-repeat, ng-include, ng-switch и директивы могут создавать свои собственные дочерние области.

Марк Райкок
источник
6
@tamakisquare, я полагаю, что текст, который вы цитируете жирным шрифтом, применяется при вызове функции конструктора контроллера, т. е. при создании контроллера, связанного с областью $. Это не применяется позже, когда произвольный код JavaScript вызывает метод, определенный для объекта $ scope.
Марк Райкок
79
Обратите внимание, что теперь можно вызывать функцию addPane () непосредственно в шаблоне, называя контроллер: «MyController as myctrl», а затем myctrl.addPane (). См. Docs.angularjs.org/guide/concepts#controller
Кристоф Огье
81
Слишком много врожденной сложности.
Inanc Gumus
11
Это очень информативный ответ, но когда я вернулся с практической проблемой ( как вызвать $ scope. $ Apply () в методе контроллера, определенном с помощью 'this' ), я не смог ее решить. Так что, хотя это все еще полезный ответ, я нахожу сбивающим с толку «присущую ему сложность».
Дамблдад
11
Javascript - много веревки [чтобы повеситься].
AlikElzin-kilaka
55

Причина, по которой этому назначается addPane, связана с <pane>директивой.

paneДиректива делает require: '^tabs', что ставит вкладки контроллера объекта из родительской директивы, в функции связи.

addPaneназначается thisтак, чтобы paneфункция ссылки могла его видеть. Тогда в paneфункции ссылки addPaneэто просто свойство tabsконтроллера, а это просто tabsControllerObject.addPane. Таким образом, функция связывания директивы панели может обращаться к объекту контроллера вкладок и, следовательно, к методу addPane.

Надеюсь, мои объяснения достаточно ясны ... это сложно объяснить.

Эндрю Джослин
источник
3
Спасибо за объяснение. Документы создают впечатление, что контроллер - это просто функция, которая устанавливает область действия. Почему контроллер рассматривается как объект, если все действие происходит в области видимости? Почему бы просто не передать родительскую область в функцию связывания? Изменить: Чтобы лучше сформулировать этот вопрос, если методы контроллера и методы области действия работают с одной и той же структурой данных (область действия), почему бы не разместить их все в одном месте?
Алексей Боронин
Похоже, родительская область не передается в функцию lnk из-за желания поддерживать «повторно используемые компоненты, которые не должны случайно читать или изменять данные в родительской области». Но если директива действительно хочет / нуждается в чтении или изменении НЕКОТОРЫХ СПЕЦИФИЧЕСКИХ данных в родительской области (как это делает директива pane), она требует определенных усилий: «требуют» контроллера, где находится желаемая родительская область, затем определяют метод на этом контроллере (используйте «this», а не $ scope) для доступа к конкретным данным. Поскольку требуемая родительская область не вводится в функцию lnk, я полагаю, что это единственный способ сделать это.
Марк Райкок
1
Эй, отметьте, на самом деле проще изменить область действия директивы. Вы можете просто использовать функцию ссылки jsfiddle.net/TuNyj
Эндрю Джослин
3
Спасибо @Andy за скрипку. В вашем скрипте директива не создает новую область видимости, поэтому я могу видеть, как функция link может напрямую получить доступ к области видимости контроллера (поскольку существует только одна область видимости). В директивах tab и pane используются отдельные области (т. Е. Создаются новые дочерние области, которые не наследуются прототипами от родительской области). В случае изолированной области видимости кажется, что определение метода в контроллере (с использованием 'this') является единственным способом, позволяющим другой директиве получить (косвенный) доступ к другой (изолированной) области.
Марк Райкок
27

Я только что прочитал довольно интересное объяснение различий между ними и растущего предпочтения присоединять модели к контроллеру и псевдоним контроллера для привязки моделей к представлению. http://toddmotto.com/digging-into-angulars-controller-as-syntax/ это статья.
Он не упоминает об этом, но при определении директив, если вам нужно разделить что-то между несколькими директивами и не хотите, чтобы сервис (есть законные случаи, когда сервисы доставляют неудобства), тогда присоедините данные к контроллеру родительской директивы.

$scopeСлужба предоставляет множество полезных вещей, $watchбудучи наиболее очевидными, но если все , что вам нужно привязать данные к представлению, используя обычный контроллер и контроллер «как» в шаблоне хорошо и , возможно , предпочтительнее.

Дерек
источник
20

Я рекомендую вам прочитать следующий пост: AngularJS: "Controller as" или "$ scope"?

Он очень хорошо описывает преимущества использования «Controller as» для выставления переменных над «$ scope».

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

Поэтому, на мой взгляд, из-за проблемы с переменными, обсуждавшейся в посте, лучше просто использовать технику «Контроллер как», а также применить ее к методам.

Лиран Бример
источник
16

В этом курсе ( https://www.codeschool.com/courses/shaping-up-with-angular-js ) они объясняют, как использовать «это» и многие другие вещи.

Если вы добавляете метод к контроллеру через метод «this», вы должны вызывать его в представлении с именем контроллера «dot» вашего свойства или метода.

Например, используя ваш контроллер в представлении, вы можете иметь такой код:

    <div data-ng-controller="YourController as aliasOfYourController">

       Your first pane is {{aliasOfYourController.panes[0]}}

    </div>
Sandro
источник
6
После прохождения курса меня сразу смутило использование кода $scope, так что спасибо, что упомянули его.
Мэтт Монтег
16
Этот курс вообще не упоминает $ scope, он просто использует, asи thisкак он может помочь объяснить разницу?
Дамблдад
10
Мое первое знакомство с Angular было с упомянутого курса, и, как $scopeникогда не упоминалось, я научился использовать только thisв контроллерах. Проблема в том, что, когда вы начинаете обрабатывать обещания в вашем контроллере, у вас возникает много проблем с ссылками, thisи вам приходится начинать делать что-то вроде var me = thisссылки на модель thisиз функции возврата обещания. Поэтому из-за этого я все еще не понимаю, какой метод мне следует использовать, $scopeили this.
Бруно Фингер
@BrunoFinger К сожалению, вам понадобится var me = thisили .bind(this)когда вы делаете обещания, или другие тяжелые вещи закрытия. Это не имеет ничего общего с Angular.
Дмитрий Лазерка
1
Важно знать, что ng-controller="MyCtrl as MC"это эквивалентно установке $scope.MC = thisсамого контроллера - он определяет экземпляр (этого) MyCtrl в области видимости для использования в шаблоне через{{ MC.foo }}
William B
3

Предыдущие версии Angular (до 1.0 RC) позволяли вам использовать это взаимозаменяемо с методом $ scope, но это уже не так. Внутри методов, определенных в области видимости, this и $ scope являются взаимозаменяемыми (angular устанавливает это в $ scope), но не иначе внутри конструктора контроллера.

Чтобы вернуть это поведение (кто-нибудь знает, почему оно было изменено?), Вы можете добавить:

return angular.extend($scope, this);

в конце вашей функции контроллера (при условии, что $ scope был введен в эту функцию контроллера).

Это имеет хороший эффект наличия доступа к родительской области через объект контроллера, который вы можете получить в child с require: '^myParentDirective'

Камил Шот
источник
7
Эта статья дает хорошее объяснение того, почему это и $ scope различаются.
Роберт Мартин
1

$ scope имеет другое «this», чем контроллер «this». Таким образом, если вы поместите console.log (this) внутри контроллера, он даст вам объект (controller), а this.addPane () добавляет метод addPane к объекту контроллера. Но $ scope имеет различную область видимости, и $ scope.methodName () должен принимать все методы в своей области видимости. this.methodName()внутри контроллера означает добавить методы внутри объекта контроллера. $scope.functionName()в HTML и внутри

$scope.functionName(){
    this.name="Name";
    //or
    $scope.myname="myname"//are same}

Вставьте этот код в ваш редактор и откройте консоль, чтобы увидеть ...

 <!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>this $sope vs controller</title>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.7/angular.min.js"></script>
    <script>
        var app=angular.module("myApp",[]);
app.controller("ctrlExample",function($scope){
          console.log("ctrl 'this'",this);
          //this(object) of controller different then $scope
          $scope.firstName="Andy";
          $scope.lastName="Bot";
          this.nickName="ABot";
          this.controllerMethod=function(){

            console.log("controllerMethod ",this);
          }
          $scope.show=function(){
              console.log("$scope 'this",this);
              //this of $scope
              $scope.message="Welcome User";
          }

        });
</script>
</head>
<body ng-app="myApp" >
<div ng-controller="ctrlExample">
       Comming From $SCOPE :{{firstName}}
       <br><br>
       Comming from $SCOPE:{{lastName}}
       <br><br>
       Should Come From Controller:{{nickName}}
       <p>
            Blank nickName is because nickName is attached to 
           'this' of controller.
       </p>

       <br><br>
       <button ng-click="controllerMethod()">Controller Method</button>

       <br><br>
       <button ng-click="show()">Show</button>
       <p>{{message}}</p>

   </div>

</body>
</html>
Аникет Джа
источник