When discussing AngularJS in lecture this week, we saw that many of our controllers used the following trick to ensure that functions properly updated a controller's variables.
angular.module('myApp').controller('MyController', [function() {
var self = this;
self.myVar = 42;
self.updateVar = function() {
self.myVar = 23;
};
}]);
I was asked why I did this and I gave a (somewhat mysterious) single-word answer: Closures. But the answer is a bit more complicated than that and the other aspect that we have to understand is Javascript's this
variable.
I won't attempt to provide a complete explanation of the this
variable. People write entire books on that subject! But, I will attempt to show the basics.
The first important thing to know about this
is that you never have to declare it. Javascript automatically declares it for you and sets its value automatically. Indeed, Javascript won't let you assign a value to this
. If you do, it will get angry and throw a ReferenceError
at you.
this = 42; // BOOM!
The second thing to know is that this
points at the execution context for a function. The trick is understanding how that context is determined. One simple mistake is to think that the execution context is the same thing as the function's scope: that is what variables are in the function's lexical scope. But that's not correct (and see the book I referenced above for more details.) Instead, the execution context is determined by how the function is invoked.
If a function is called directly then this
points at the global execution context. (Note: I'm not going to get into a discussion about strict mode and how that changes Javascript's behavior for this example. See the book for details!)
// Example One
var a = 42;
var b = function() {
return this.a;
};
var c = function(d) {
var a = 23;
return d();
};
b(); // returns 42;
c(b); // returns 42;
In this example, when we call b
directly, it returns the a
in the global execution context. When we call b
again in c
it does not matter that c
defined its own a
variable, we are still calling b
directly (even though in c
, we refer to b
via the alias d
) and, when you call a function directly, this
binds to the global execution context.
This behavior means that if you assign a property to this
in a function, then that property is created in the global context. For instance, the following code creates a new variable in the global scope.
// Example 2
// This code assumes you are executing it using Node.
// Use window.new_var if you are running it in a browser.
console.log(global.new_var); // prints undefined
var b = function() {
this.new_var = 23;
};
console.log(global.new_var); // prints undefined
b();
console.log(global.new_var); // prints 23
A function can also be called as a method on an object. When this happens, the object is said to be implicitly bound as the execution context of the associated function. A function in this context is able to change the values of that object's properties via this
. For instance:
// Example 3
var increment = function() {
this.age = this.age + 5;
};
var person1 = {
age: 42,
makeMeOlder: increment
};
var person2 = {
age: 23,
makeMeOlder: increment
};
person1.makeMeOlder();
person2.makeMeOlder();
console.log(person1.age); // prints 47
console.log(person2.age); // prints 28
Here we define a freestanding function but store it as the value of a property on two objects. We then invoke the function on each object via the property name: person1.makeMeOlder();
. The function increment()
is invoked and its execution context is set to be (in this case) the person1 object. Thus when increment
references this.age
, it is referencing the age
property of person1
.
Functions can be invoked in two additional ways:
-
In the context of a
new
statement: here the function is used as a constructor to initialize a new object. We will not be discussing this case any further. -
In the context of an explicit binding: here the execution context for a function is selected ahead of time and then used when that function is called. There are three ways of doing this:
bind()
,call()
, andapply()
. We will look at just one of these:call()
.call()
andapply()
function the same way, they differ only in the parameters they accept.bind()
is a utility function that creates what is known as a hard binding; hard bindings are out-of-scope for this particular discussion.
Let's see an example of an explicit binding and then return to our original question about why we configured our AngularJS controllers in the way shown above.
// Example 4
var increment = function(delta) {
this.age = this.age + delta;
};
var person = {
age: 42
}
increment.call( person, 5 ); // or increment.apply( person, [5] );
console.log( person.age ); // prints 47
First, we define a function called increment()
. It accepts a delta and increments this.age
by that value. Second, we created an object called person
with a property age
set initially to 42. Second, we invoke call()
ON THE FUNCTION and we pass it the object that should explicitly be bound as the execution context when increment
is executed which is what happens next. We also supply the parameter that should be passed to increment
as the second parameter of call()
. (We show in a comment how to achieve the same effect using apply()
. apply()
simply expects any parameters for the function to be supplied in an array.) Thus, increment.call( person, 5 )
is equivalent to person.increment(5)
(assuming we defined increment as a property on person). The advantage of the former is that we can pass any object with an age
property to increment.call()
and it will update the age
property by the delta that is passed. Finally, third, we use a console.log()
statement to show that person
's age was indeed incremented by 5.
We are now ready to return to this example:
angular.module('myApp').controller('MyController', [function() {
var self = this;
self.myVar = 42;
self.updateVar = function() {
self.myVar = 23;
};
}]);
Imagine that this controller was written like this:
angular.module('myApp').controller('MyController', [function() {
this.myVar = 42;
this.updateVar = function() {
this.myVar = 23;
};
}]);
If we had a variable called ctrl
that pointed at an instance of MyController
then if we called ctrl.updateVar()
, we would expect it to change the value of ctrl.myVar
from 42 to 23. Indeed, in many cases that is exactly what would happen. But, now, imagine that we did something like this:
var action = ctrl.updateVar;
var person = {
};
action.call( person );
console.log(ctrl.myVar); // prints 42
console.log(person.myVar); // prints 23
In this situation, we have invoked updateVar()
with an explicit binding of this
that did not match the instance of MyController
and thus that call would NOT update the controller's instance of myVar
! Instead, as the example indicates, a new property called myVar
would be created on the person
object since this
was set to be pointing at person
and not the controller.
While the code above is contrived, it is all too easy to recreate it in a web application. All that has to happen is that the function is registered as a callback on code that explicitly sets the execution context by invoking call()
, apply()
, or bind()
. At that point, all bets are off... or are they?
This is where the var self = this;
trick comes to the rescue. Recall that writing the controller to use the this
variable was just an example, we actually implemented the controller like this:
angular.module('myApp').controller('MyController', [function() {
var self = this;
self.myVar = 42;
self.updateVar = function() {
self.myVar = 23;
};
}]);
In this instance, self
captures the value of this
when the controller is first created. The value of self
is then captured by updateVar()
when it is defined via the magic of closures. Then, no matter how we call updateVar()
, the controller's myVar
will be updated since updateVar()
ignores this
and instead uses self
to update the correct property. So, if our controller is implemented using the var self = this;
trick, our example code now behaves like this:
var action = ctrl.updateVar();
var person = {
};
action.call( person );
console.log(ctrl.myVar); // prints 23
console.log(person.myVar); // prints undefined
Even though person
was set to be the execution context of the call to updateVar()
, the person
object is not updated by the call since updateVar()
does not make use of this
but rather references self
via its closure.
And that's why it is recommended for all of your controllers to make use of the var self = this;
trick to ensure that controller functions are always able to update controller variables.
Since the example above is hard to reconstruct and see in action, I have created a program with a similar structure that proves that the var self = this;
trick works as advertised (i.e. prevents explicit binding from breaking the controller). Check out Example 5 to see this in action.
Let me know if you have any questions or comments!
Prof. Ken Anderson
February 6, 2015