CC-Archived / promise-as3

Promises/A+ compliant implementation in ActionScript 3.0
168 stars 55 forks source link

unexepted value of "this" within callback function #38

Closed asinning closed 10 years ago

asinning commented 10 years ago

Wow, it's really great to be able to use promises within as3. Thanks!

My comment: If I have a function inside of an object (o:SomeObject) and I create a promise with a callback function for then, I am surprised to find that the value of "this" inside of the callback function is not o:SomeObject, but rather is something displayed as "[object global]".

    private function someFunction():void {
        someFunctionThatReturnsAPromise()
        .then(function(value):void {
            trace("THIS:" + this);
        });
    }

in order to access "o" I need to do the following:

    private function someFunction():void {
        // put "this" into a scoped variable "o";
        var o:SomeObject = this;
        someFunctionThatReturnsAPromise()
        .then(function(value):void {
            trace("THIS:" + o);
        });
    }
ThomasBurleson commented 10 years ago

If you consider the promise API used with promise chaining:

  dosomething().then( dosomemore ).then( finishthework ).then( announceresult );

Each of the then resolve handlers continues or extends a promise chain. And each chain segment could have a totally different this context.

As such [and for other reasons], the promise API will not implicitly preserve your this context. This artifact is especially prevalent and known in the Javascript implementations.

But you can easily compensate:

1 Use closures (as you demonstrated). 2 Use function bind to explicitly reset the this context during the handler callback

Use Closures

Use a closure access to get a valid reference to the this context using the reference cached by the local variable self.

private function updateUser( user ):void {
  this.user = user || new User();
}

private function loadUser(userID:String):void {
  var self:SomeObject = this;

   // Load user details from the remote server  
  loadUserDetails(userID).then( function(value):void {
    trace(value.userID);
    self.updateUser(value);
  });
}

Use Function Binding

Use a technique cloned from Javascript solutions. Here is the utils class Closure.as:

/**
 * Special method to bind a handler to a specific scope
 * @return Function which when invoked will call handler with scope context
 */
static public function bind( handler:Function, scope:Object ):Function
{
  return function (...args) : * {
    var params : Array  =  ( handler.length < 1 ) ?  [ ]                :
                           ( handler.length > 1 ) ?  [ ].concat(args)   :
                           ( args.length > 0 ) ?  [ args[0] ]           :  [ null ] ;

    return handler.apply( scope, params );
  };

}

Then in the UserDelegate.as class, your code is significantly simplified:

private function updateUser( user ):void {
  trace(value.userID);
  this.user = user || new User();
}

/**
 * Load user details from the remote server  
 */
private function loadUser(userID:String):void {
  return loadUserDetails(userID).then( Closure.bind(updateUser, this) );
}
karfau commented 10 years ago

From what I know in ActionScript (and I can imagine it's the same for javascript), the scope in which the closure was executed is preserved. you can access properties and methods of those contextes (which one should be this...) by just referring to them without the this. in front of them.

    private var prop:String = "from this";
    private function someFunction(parameter:String):void {
        var variable:String = "from inside the method";
        someFunctionThatReturnsAPromise()
        .then(function(value):void {
            trace("prop:" + prop);
            trace("parameter:" + parameter);
            trace("variable:" + variable);
        });
    }

But you are right, if you really only need access to what has been this in the outer scope and not to one of its methods or properties you need to create a reference to it that is named differently.

And it can get pretty tricky with duplicate names if you have a lot of those handlers in the scope of one method.

ThomasBurleson commented 10 years ago

The scope is not preserved in a function fn callback if fn.apply(null,[...]) is used; which is the case when deferreds are resolved or rejected and the registered handers are invoked.

With closures, you must be careful and never assume the this context is implicitly preserved. Again, I highly recommend Closure.bind() if you want to preserve the this context.

karfau commented 10 years ago

ok, good to know, thx

fwienber commented 10 years ago

I guess you are both right. Yes, you cannot rely on this being the same as the "outer" this in a callback function, like Thomas said, and yes, in ActionScript (unlike JavaScript!), all properties of the outer this are in scope, like karfau said. I find the best way to deal with this pitfall to take advantage of the fact that in ActionScript, in contrast to JavaScript, a method is automatically bound to the object from which it is referenced (the object before the dot or the implicit this), even if the method is not called immediately. Thus there is no need to bind methods in ActionScript - actually, it has no effect, so it only makes sense for functions! Your sample code can be simplified to look like so:

private function updateUser( user ):void {
  this.user = user || new User();
}

/**
 * Load user details from the remote server  
 */
private function loadUser(userID:String):void {
  return loadUserDetails(userID).then( this.updateUser );
}

You can even leave out the last this.. Note that all this would not work in JavaScript. Since in JavaScript, you may not leave out the explicit this., you cannot access this properties through the scope, and also "methods" (which do not really exist in JavaScript, they are just members that happen to be functions) are not automatically bound to their object unless called immediately: o.doSomething() binds this to o when doSomething is invoked, but var fn = o.doSomething; fn() does not. So the techniques proposed by Thomas are valid and needed for JavaScript, but not for ActionScript.

karfau commented 10 years ago

Thanks for the clarification! I think @asinning opened the issue because regarding your example, The this in updateUser will not work. And what I tried to say was: It is true that it doesn't work with this but it works without it.

It's always good to have a simple example :-)

ThomasBurleson commented 10 years ago

@All, The this in function updateUser() will not work because the example did not use .then( Closure.bind(updateUser, this) );.

Either use Closure.bind() (or similar wrapper) or use a closure access variable.

To be specific why the wrapper is needed with Promise AS3 resolve/reject, see line 314 of Consequence.as

/**
 * Execute this callback.
 */
public function execute():void
{
    closure.apply( null, parameters );
}
fwienber commented 10 years ago

And what I am trying to say is: The this in updateUser works perfectly, because updateUser is a method. It just does not work if you use a function, as in asinning's original example. Has anyone actually tried it?

fwienber commented 10 years ago

Try this code:

package {
import flash.display.Sprite;

public class BoundThisTest extends Sprite {

  public function BoundThisTest() {
    function testFunction() {
      return this;
    }
    trace(testFunction.apply("foo"));
    trace(testMethod.apply("foo"));
  }

  private function testMethod() {
    return this;
  }

}
}

It will print

[trace] foo
[trace] [object BoundThisTest]

Believe me, you cannot bind this differently in a method using apply or call, only in a function.

ThomasBurleson commented 10 years ago

@fwienber - Perhaps we are arguing two different points. I think, however, that you have fallen into the same trap that most AS3 developers encounter: unaware of the power of apply(scope,args) and call(scope, ...)

Please refer to Function::apply( ) asdocs.

fwienber commented 10 years ago

I tried to explain that in ActionScript 3, unlike JavaScript, methods are not just functions. If you had read the documentation you refer to thoughtfully, it states exactly this fact:

Methods of a class are slightly different than Function objects. Unlike an ordinary function object, a method is tightly linked to its associated class object. Therefore, a method or property has a definition that is shared among all instances of the same class. Methods can be extracted from an instance and treated as "bound" methods (retaining the link to the original instance). For a bound method, the this keyword points to the original object that implemented the method. For a function, this points to the associated object at the time the function is invoked.

You should think twice next time before calling someone else "unaware"...

fwienber commented 10 years ago

Because of the occasionally hard-to-follow discussion, I'd like to summarize the solution regarding the original problem. You should refactor callback functions that need access to their "outer" this to (private) methods, where this is always bound to the original instance of the class containing the method and can be used as such without restrictions. There is no need to explicitly bind methods to this in ActionScript 3, because they are already ultimately bound like so. Only for non-method (JavaScript-like) functions, a method like Closure.bind() makes sense to set this as desired. As @karfau pointed out, even inside such non-method functions, class members are in scope, i.e. can be accessed solely by their name, omitting explicit this. access.

karfau commented 10 years ago

:+1: I totally agree with that conclusion