Closed asinning closed 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 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 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) );
}
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.
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.
ok, good to know, thx
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.
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 :-)
@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 );
}
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?
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.
@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.
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"...
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.
:+1: I totally agree with that conclusion
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]".
in order to access "o" I need to do the following: