A Little this, a Little _that:
Understanding Scope and Context in JavaScript
David Aragon
@davidmaragon
Been a JS and Ruby developer for 4 years. I work at..
I work at Quick Left in Boulder.
Slides at:
davidql.github.io/scope_talk
Code sample:
function() {
alert("code will be this big");
}
Road Map
(Is This Talk for Me?)
Scope from the ground up
The magic variable `this`
What is context?
The future with EcmaScript 6
Let's look at scope from the ground up
What is scope?
Scope: the lifetime of a variable
function add(n1, n2) {
var sum = n1 + n2;
if (true) {
var sum = 42;
}
return sum;
};
A scope maps a variable name to a value.
What does the var sum mean when we go to return it?
Scope in Pseudocode
JavaScript does NOT use block scope
Unlike many other langauges, JS does not use blocks to define a scope.
JavaScript uses functional scope:
function add(number1, number2) {
// anything inside is local
var sum = number1 + number2;
};
JavaScript does NOT define a scope with a pair of curly braces.
JS uses functions to define a scope, and only functions.
For a variable to be local, it must:
Be declared with `var`
Be declared inside a function
function add(number1, number2) {
var sum = number1 + number2;
};
Scope trick: Self-Invoking functions
(function() {
// This function runs the second
// it's encountered.
// Gives me a private scope on demand
})();
Now that we know only a function can define a scope, here is a useful way write a self-executing function.
You can treat it like a portable scope creator.
Self-Invoking functions
// Step 1: normal function
function() {};
// Step 2: wrap the whole thing in parentheses
(function() {});
// Step 3: call your function!
(function() {
// private scope
})();
Now that we know only a function can define a scope, here is a useful way write a self-executing function.
You can treat it like a portable scope creator.
Let's play GUESS THAT SCOPE
This 3-question mini pop quiz will quickly explain some of the details of scope resolution.
What gets printed?
var episode_1 = "Winter Is Coming";
if (true) {
var episode_1 = "The Kingsroad";
}
console.log(episode_1);
"The Kingsroad"
var episode_1 = "Winter Is Coming";
if (true) {
var episode_1 = "The Kingsroad";
}
console.log(episode_1);
// "The Kingsroad"
What gets printed?
var episode_1 = "Winter Is Coming";
(function() {
var episode_2 = "The Kingsroad";
})();
console.log(episode_2);
Error: episode_2 is not defined
var episode_1 = "Winter Is Coming";
(function() {
var episode_2 = "The Kingsroad";
})();
console.log(episode_2);
// Error: episode_2 is not defined
What gets printed?
var episode_3 = "Lord Snow";
function printName(episode_3) {
console.log(episode_3);
}
printName("A Golden Crown");
"A Golden Crown"
var episode_3 = "Lord Snow";
function printName(episode_3) {
console.log(episode_3);
}
printName("A Golden Crown");
// prints: "A Golden Crown"
This shows us that the innermost scope always win. The closest variable definition wins, including argument names (they're closer than the var definition above the function).
If only it were that easy
Up until this point I've carefully avoided mentioning an important pitfall in JS: the global scope.
A variable is global if it is not:
Declared with `var`
Declared inside a function
var episode_1 = "Winter Is Coming";
(function() {
// declaring without var
episode_2 = "The Kingsroad";
})();
console.log(episode_2);
// "The Kingsroad"
Functions give you a private scope, but ONLY if you declare a variable with the keyword var.
Global scope is window in a browser. Accessible to everyone.
Why is global scope so bad?
"Browsers are the most hostile software development environment imaginable". -Douglas Crockford
Everyone has access to the global scope
3rd party code can change your values at any time
Global variables are one reason that's the case.
Along with the JS we write, a page is executing tons of 3rd party JavaScript. You don't want this unknown code to be able to affect or overwrite your own, but if you use globals, you're giving the keys of your application to a faceless 3rd party developer. If you use globals, any variable you rely on *can be overwritten at any time without notice*.
The Dangers of Global Scope
http://blog.safeshepherd.com/23/how-one-missing-var-ruined-our-launch/
Node.JS
Stored the current user's request in a global
Whenever there was more than 1 user, their requests overwrote each other and so the responses were garbage
Detecting Globals
Object.keys(window);
// ["top", "window", "location",
// "external", "chrome", "document", ...
The magic variable `this`
So far, we've determined what value a variable holds by looking at the program text before it. But there are ways to define variables *not* based on what comes before them in a program. This is dynamic scoping.
`this` is a magic variable whose definition depends on how you call the function its in.
What is context?
If a function is a sentence, the context is the subject of that sentence. It's who the function is about.
We access the context through the variable `this`.
If a function is a sentence, the context is the subject of that sentence. It's who the function is about.
We access the context through the variable `this`.
Why is `this` so hard?
We're used to lexical scoping (where we can see the value of a variable in the visual scope).
`this` uses dynamic scoping, which we're less used to thinking about.
But we all kinda sorta know what `this` should be.
Before we get into the mechanics of it all, it's useful to think about why `this` is so hard for us.
(slide fragments answer the question)
Let's look at some examples in the wild, ones we use all the time.
`this` in a jQuery event callback
$('button').on('click', function() {
$(this).addClass('was_clicked');
// $(this) refers to the button the was clicked
});
`this` in a jQuery element loop
$('a.disabled').each(function() {
$(this).remove();
// $(this) refers to the current anchor
// element in our loop of the matched set
});
4 ways to set the value of `this` in a function
Let's quickly show an example for each of these. They're super easy.
4 ways to set the value of `this` in a function
Call a method *on* an object
Pass `this` in, with .call() and .apply()
Use `new` to make `this` start out empty
.bind() `this` with 3rd party methods
1. Call a method *on* an object
Here we have an object called user. Whenever we call a method on that object, meaning we call user.anyFunction(), `this` will be user. So in user.alertName(), `this` is user, and this.name is user.handle, which we've defined here as "@davidmaragon."
So that's the first way to know `this`. Whenever we call obj.function(), `this` is obj.
2. Call a function and pass `this` in, with .call();
To explicitly set the value of `this` in a function, don't call it the normal way, with parenthesis. Treat it like an object, and then access the object's .call() function.
This is super cool, because even though we've defined user.handle to be "@davidmaragon", we can override it and make `this` whatever we want it to be with .call(). Just pass in an object, and `this` will be that object.
Note that all the arguments to .call() after the `this` context are the function's arguments.
2. (part b) Call a function and pass `this` in, with .apply();
3. Use `new` to create a brand new function context
function User(handle) {
this.handle = handle;
}
var andrew = new User("@AndrewWK");
// andrew === {handle: "AndrewWK"}
If you call a function with the `new` keyword, `this` will start out in the function as an empty object. After the function runs and `this` gets built up, it is returned.
But! Note that if I called this without `new`, it would be a normal old function call, and `this` would be window.
4. .bind() methods ($.proxy, _.bind, native .bind())
var alertName = function() {
alert(this.handle);
}
$.proxy(alertName, {handle: "@AndrewWK"})();
// alerts "@AndrewWK"
Native .bind() available from IE 8, FF 4, Chrome 7, iOS 6 (so not great)
Utility libraries like Underscore understand that context is an important and difficult part of JavaScript, so they've given us methods to help us pass around `this` easily.
Performance
on jsPerf: http://jsperf.com/bind-vs-jquery-proxy/5
4 ways to set the value of `this` in a function
Call a method *on* an object
Pass `this` in, with .call() and .apply()
Use `new` to make `this` start out empty
.bind() `this` with 3rd party methods
If none of the above are used, `this` is global object
Let's play GUESS THAT CONTEXT
3 questions / solidify your understanding of what `this` is in a given sitution
What is `this`?
(function() {
console.log(this);
})();
It's global object
(function() {
console.log(this);
// Window {top: Window, window: Window, location: ...
})();
What is `this`?
var background_colors = ["green", "blue", "red"];
(function() {
console.log(this);
}).call(background_colors);
It's `background_colors`
var background_colors = ["green", "blue", "red"];
(function() {
console.log(this);
// ["green", "blue", "red"];
}).call(background_colors);
What is `this`?
var user = {
print: function() {
console.log(this);
}
};
user.print();
It's `user`
var user = {
print: function() {
console.log(this);
}
};
user.print();
// Object {print: function}
`this` in jQuery source
$("div.disabled").each(function(index) {
console.log(this + " is element " + index);
});
From this we learn that when you $.each over an array of elements, jQuery uses .call() to pass each of the elements into your callback as the context.
We also learn that $.each() passes additional arguments into your callback, like the index i.
Underscore.js does this exact same for-loop-plus-.call() with its .each method.
Annotated $().each()
Beware of Callbacks
$('button').on('click', function() {
setTimeout(function() {
// Uh oh! `this` is the global object!
$(this).addClass('clicked');
}, 1000);
});
Dealing With Callbacks
Using `_this` and `_that`
$('button').on('click', function() {
var _this = this;
setTimeout(function() {
$(_this).addClass('clicked'); // All better
}, 1000);
});
This is a closure. "a function or reference to a function together with a referencing environment"
Our Future With EcmaScript 6
`let` allows block scope (!)
Arrow functions pull context from scope
`let` gives a variable block scope
// EcmaScript 6
if (true) {
let local_var = "I'm local to the if block!";
}
console.log(local_var); // Error: undefined local_var
Browser support for `let`:
Chrome
19+ (expiremental flag enabled)
Firefox
24+
Safari
No Support
IE
11+
Node (0.10.26)
No Support
Arrow Functions
// current (ES5) declaration
var myFunc = function(params) { }
// arrow function
var myFunc = (params) => { }
Without Arrow Functions (ES 5)
$('button').click(function() {
var _this = this;
setTimeout(function() {
// lookup _this from parent scope
$(_this).addClass('clicked');
}, 1000);
});
With Arrow Functions
$('button').click(function() {
setTimeout(() => {
$(this).addClass('clicked');
}, 1000);
});
Browser support for arrow functions:
Chrome
No support
Firefox
24+
Safari
No Support
IE
No support
Node (0.10.26)
No Support
Where to go from here
You know all the rules.
These two concepts underlie important developments in client-side coding:
Modules that mix private vars with public exports (like RequireJS)
Full fledged MVCs in the browser, like Backbone
Functional programming toolbelts like Underscore.js
For MVC point: At Quickleft we run full day trainings in Backbone.JS, so if you want to level up and explore these concepts in much more detail, be sure to check that out.
Thank you!
@davidmaragon