5

Relevant fiddle: https://jsfiddle.net/tqf4zea7/1/

I'm using $q in an angular controller. To test some scenarios, I created an array on the scope to push messages to:

$scope.messages = [];

I have set up a function that returns a $q function as so:

function returnAPromise(valToReturn){
    return $q(function(resolve, reject){
        $timeout(function(){
            resolve(valToReturn);
        }, 500);
    });
}

I then have a .then() call on the result that looks like this:

returnAPromise('third').then($scope.messages.push);

Since I only want to push the value that the promise was resolved with to the array, I figured I could just pass in the push method of the messages array, but when I do that, I get the following error:

VM289 angular.js:12520 TypeError: Array.prototype.push called on null or undefined
    at processQueue (VM289 angular.js:14792)
    at VM289 angular.js:14808
    at Scope.$eval (VM289 angular.js:16052)
    at Scope.$digest (VM289 angular.js:15870)
    at Scope.$apply (VM289 angular.js:16160)
    at VM289 angular.js:17927
    at completeOutstandingRequest (VM289 angular.js:5552)
    at VM289 angular.js:5829

If I enclose push in a function, it works fine:

returnAPromise('third').then(function(message){ 
     $scope.messages.push(message) 
});

Is this a closure issue I don't understand?

sonicblis
  • 2,926
  • 7
  • 30
  • 48

2 Answers2

6

You need to bind push since it uses this

returnAPromise('third').then($scope.messages.push.bind($scope.messages));
Yury Tarabanko
  • 44,270
  • 9
  • 84
  • 98
  • 1
    https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind Examples section describes the issue and why .bind() resolves it. Unfortunately, it includes the line "A common mistake for new JavaScript programmers". =[ I've been demoted. – sonicblis Aug 16 '16 at 19:49
1

I know there is an accepted answer, but i will explain this more clearly here

Example

lets start with an example

var scope = {
  scopeFn: function() {
    console.log('this', this)
  }
}

function callFn(fn) {
  fn();
}

callFn(obj.scopeFn) // will log window object 
callFn(function() {
  obj.scopeFn();
});// will log scope object

as you can see, wrapping the function will give the called object the value of this, but calling it directly without wrapping it will call the window object.

why?

this will bind to the object its called from.

In the first example callFn(obj.scopeFn) you are passing the function as a parameter, hence when the function is called, its called directly not from the scope object. (scope object is lost, only the function reference is sent).

in the second example you are calling the function scopeFn from the object, hence the this will bind to its object. (scope object is not lost, as the whole thing is there when its called)

Solution

To solve this issue you need to bind the function you are passing as a parameter for the resolve, so it will always be called as if it is called from its parent object.

var scopeMessage = $scope.messages;
returnAPromise('third').then(scopeMessage.push.bind(scopeMessage));
Community
  • 1
  • 1
Bamieh
  • 10,358
  • 4
  • 31
  • 52