Understanding Callbacks
Callbacks seem to be a sticking point for people new to programming. Put simply, callbacks are functions that are passed into another function as an argument. With the many ways one can define a function in JavaScript, it's no wonder why callbacks get confusing.
Anatomy of a Function
JavaScript has many ways of defining a function, but they all follow a similar pattern and have the same pieces, they just look a bit different. There is more technical terminology surrounding functions, but we are going to gloss over them for now. (If you are interested, feel free to look up "Function Declarations" and "Function Expressions").
Normal Functions (Named Functions)
Normal functions, probably the first way you learned about creating functions. Understanding the anatomy of these will help you understand the other types of functions as well.
function funkyFunction(music, isWhiteBoy) {
if (isWhiteBoy) {
console.log('Play: ' + music);
}
}
This is actually called a function declaration
and is broken into a few parts.
- The
function
keyword- This tells the JavaScript compiler you are making a named function
- The name
- This is the name of the function, and what you will use when you call it. It is also used in stack traces.
- The parameters
- everything between
(
and)
is a parameter, these must be separated by commas if there is more than one. There may also be nothing between the()
if the function does not take any parameters. The parenthesis are required.
- everything between
- The function body
- This is where the function actually does something. This code gets run with whatever values are passed into the parameters.
Calling a function looks similar to declaring it. When you call the function, you type the name of the function and add ()
after. (without the function
keyword and the body). Inside the ()
you may pass it the values you want the parameters you defined to represent. These arguments
are used like variables inside the body of the function.
// Calling a function
funkyFunction('that funky music', true);
// This prints "Play: that funky music" in the terminal.
Anonymous Functions
These are very similar to normal functions, with just a few differences. Anonymous functions are not 'named', and have a few different syntaxes. Even though they cannot have a name, they can be assigned to a variable. Even though when assigned to a variable they show up in stack traces, they are still considered an anonymous function. They may show up as 'anonymous function' in stack traces when passed into other functions as callbacks, however.
Anonymous functions are mostly used by passing them into other functions as a callback
. This will become more clear later.
Each of the functions below are identical to the funkyFunction above in their 'funk-tionality'
// This example is still an anonymous function even though we used the `function` keyword, as it doesn't have a name.
const funkyFunction = function(music, isWhiteBoy) {
if (isWhiteBoy) {
console.log('Play: ' + music);
}
}
// This is called an arrow function, we'll get into these soon.
const funkyFunction = (music, isWhiteBoy) => {
if (isWhiteBoy) {
console.log('Play: ' + music);
}
}
An anonymous function is just a function that does not have a name, this doesn't mean that it cannot be called. Each of the above functions can be called exactly the same way:
funkyFunction('that funky music', true);
And this is because functions are 'first class citizens' in JavaScript and can be assigned to variables. Or passed as an argument to another function.
Arrow Functions
These are just a shorter way to write a function. They do have some special rules however, and understanding the rules imposed by arrow functions will help you understand callbacks. We're going to ignore the this
binding rules for these functions for now.
- If there is only one argument, the parenthesis
()
can be omitted - if arrow functions are one line, the brackets
{}
can be omitted.- When omitting the brackets, the arrow function returns the evaluated expression without requiring the
return
keyword.
- When omitting the brackets, the arrow function returns the evaluated expression without requiring the
The functions below are variations of the rules above
const playThe = (funky) => {
return funky + " music";
}
const playThe = funky => {
return funky + " music";
}
const playThe = funky => funky + " music";
// You can call all of these functions like: `playThe('blues')`
Below are some examples of an arrow function without an argument. These functions are all identical as well. Notice the ()
in place of any named arguments. It is required because there aren't any parameters.
const playThat = () => "funky music";
const playThat = () => { return "funky music"; }
const playThat = () => {
return "funky music";
}
Key Point
Take some time and study the function examples above and note how they are similar and how the same parts exist in both, with the exception of the function
keyword.
What Callbacks Look Like
You most likely have seen, or even used, callbacks and not realized it. They are used frequently in JavaScript. Understanding JavaScript is impossible without understanding callbacks. Below is an example of something you may have run into before.
const notes = ['do', 're', 'me'];
notes.forEach((note) => console.log(note));
This is the forEach
array method. This method simply takes a callback
function as its argument. (Don't forget that forEach
is a function itself).
There are many other ways to do the same thing (as is tradition in JavaScript), below are a few more ways to write this code:
const notes = ['do', 'ray', 'me'];
notes.forEach((note) => {
console.log(note);
});
notes.forEach(function(note) {
console.log(note);
});
// This one is tricky, but will make more sense later
notes.forEach(console.log);
How Callbacks Work
To state it once more: Callbacks are just functions passed into other functions as arguments (as a parameter).
Iterator Functions
Below is what forEach
might look like under the hood, notice it calls the callback
function each time it loops over an item.
function myForEach(array, callback) {
for (let i = 0; i < array.length; i++) {
callback(array[i]); // This is when the callback function gets called, or executed
}
}
// You would call it like this:
const myArry = [2, 3, 4, 2];
myForEach(myArry, (item) => {
console.log(item + 2);
})
WHOA, hold up. Where did item
come from?
This came from the function myForEach
calling the callback with an argument. The line with callback(array[i])
is calling the callback function with an argument, which we defined inline as an anonymous function. Below are more examples of how this could be called.
const myArry = [2, 3, 4, 2];
// We do not need the `()` in this case, as we only have one argument and we are using an arrow function
myForEach(myArry, item => console.log(item + 2));
// We can pass arguments to this kind of anonymous function as well
myForEach(myArry, function(item) {
console.log(item + 2)
});
// This time we are declaring the function we want to use as a callback
// Notice we define `item` as a parameter to be passed in when it's called by the `myForEach` function.
function printItemPlusTwo(item) {
console.log(item + 2);
}
// `item` is passed into the function, we do not need to declare it here because we declared it elsewhere.
// It is the same as the 'console.log' example above except we declared our own function.
myForEach(myArry, printItemPlusTwo);
Another good example of how callbacks work might be the .map
method (read more on MDN), below is one way it might be implemented.
function myMap(array, callback) {
const myNewArray = [];
for (let i = 0; i < array.length; i++) {
const callbackResult = callback(array[i]);
myNewArray.push(callbackResult);
}
return myNewArray;
}
// This could be called like this:
const addedArray = myMap([1, 2, 3], (arrayNum) => {
return arrayNum + 2;
});
// OR
const addedArray = myMap([1, 2, 3], (arrayNum) => arrayNum + 2)
Event Listeners (DOM)
Event listeners in JavaScript seem to be confusing to people, but after understanding callbacks, these should be a lot easier to understand.
Let's review what they look like, see if you can pick out the different things going on.
const element = document.querySelector("#myId");
element.addEventListener('click', (event) => {
console.log(event.target.value);
// `event` is passed into the callback from the `.addEventListener` function when it receives a 'click' event.
});
If you notice, the second argument (value you pass into a function) to addEventListener
is a function. In this case it's an anonymous arrow function. This piece of code could have also have been written like this and it would behave identically.
const element = document.querySelector("#myId");
element.addEventListener('click', function(event) {
console.log(event.target.value);
});
Part of what confuses people is the event
object. Where does it come from? How does it get there?
This event object is passed into the callback function by the .addEventListener
function. A function is calling another function.
This is because.... Callbacks are just functions passed into another function as arguments.
That means we can declare a function outside of the argument list and just add it by its name as well. Like so:
function myEventHandler(event) {
// do something, probably with 'event'
}
const element = document.querySelector("#myId");
element.addEventListener('click', myEventHandler);
Notice how we didn't 'call' the function called myEventHandler
? If we were to call it inside the parameter list, the function we called myEventHandler
would run immediately and give the addEventListener
the result of calling that function. (in this case, it would be undefined)
Conclusion
Callbacks are an important part of JavaScript, they are vital to understand, even with the onset of promises and async/await. Callbacks get called by another function, so you don't have to call them in the arguments, ( Calling a function is using a function's name and adding ()
to the end of it, like console.log()
)
These are something that you will learn if you give yourself time, understanding how they work will make your JavaScript career a lot easier!
Top comments (21)
I understand this is content for beginners ... but there seems to be this idea that arrow functions were introduced in ES2015 for their fashionable terseness.
The reason for their inclusion is that they are different to traditional function expressions - which is related to their use as callbacks.
MDN: "Arrow functions establish
this
based on the scope the Arrow function is defined within."See also: Function Context.
In practical terms this means that when an arrow function is created inside an object method it automatically binds to the
this
of the object the method was called on, so it retains access to the original creating object even when it's passed as a callback to another function (or method of another object).Regular functions do not behave this way. Regular functions have to be explicitly bound to the object.
As a trade off arrow functions don't support apply, bind, or call and as a consequence arrow functions cannot be shared via the prototypal inheritance mechanism (given that each arrow function's
this
is statically bound it no longer can be shared via a dynamically passedthis
).That's exactly why I added
We're going to ignore the this binding rules for these functions for now.
in the article, perhaps I should have expanded on that a bit more. This comment here explains it very well though!Re-reading that paragraph again:
"Arrow functions differ from regular functions in some nuanced ways. To simplify the discussion we're steering clear of those differences."
Beginners tend to latch onto "just a shorter way to write a function" as "the truth" because it's so (convenient and) easy to remember - using it as a justification to use arrow functions exclusively even when it's inappropriate (e.g. as class methods).
I'll consider making some edits, thank you
Thank Mr Elsperger for the great article. The article is really illuminating regarding callbacks. He is well aware that there is a minor difference regarding the context between arrow functions and regular functions. It is perhaps more important than the syntax difference as anybody with a little ES6 JS experience can refactor one form to the other. This comment is great to point out the real difference between the two with this great code sample. Maybe the code sample can be simplified slightly to make it more obvious, what's going on.
The important question is weather the article should contain this difference as detailed as in this comment. In my opinion Mr Elsperger is not far from the truth that real beginners can get confused with details like this. I think he has got experience with these people as he contributes actively to The Odin Project, which is a curriculum for people, who want to become self trained web developers, like myself.
As far as I have seen this is the only real difference between arrow functions and regular function expressions, therefore it would be great to include it in the article in some less confusing form. He could use for example small print and warn beginners that they can skip this part not to get confused. This is common practice in scientific articles.
Why is this fine detail less important? JS is not a language for primarily doing OOP, but functional programming. It means listeners hardly ever use 'this'. It's a concept of OOP, where inheritance is highlighted. Of course people, who want use JS for OOP or people who use React class components (OOP syntax) must be aware of all this.
class
is merely a template for creating objects; it's not a class in the class-based object oriented sense.this
is called the function context; so it can be used to access theobject
the function was invoked on but it is also used in other ways.Please read this: Function-Oriented
You are of course right. I don't have very strong language theoretical knowledge. I am only fluent in two languages.
I think you are mistaking my position that it is important to understand
this
in reference to functions as some kind endorsement of OOP in JavaScript. Nothing could be further from the truth (as should be evident in this comment).My contention is that understanding the relevance of
this
is an important part of building JavaScript competence—completely independent of any OOD/OOP competence. In fact MDN calls it class context in the context ofclass
constructors.When you look at the documentation under
this
it's also called the execution context.this
is the execution environment that is instantiated by the JavaScript runtime before the function was invoked. Granted 99.9% of the time this is done to emulate a method call on an object but it has other uses.Here is a weird one:
JavaScript is still first and foremost an imperative language. Please see this comment and this one. For example, JavaScript isn't immutable nor does it support persistent data structures and while recursion is supported, tail call optimization (TCO) isn't standard so there is always the risk of blowing the stack.
The fact that JavaScript can support a functional style is largely due to the fact that it was initially developed as Scheme in the browser.
From a beginner's perspective I think a case could be made that arrow functions should be avoided until such time that they are fully versed and comfortable in using standard function declarations and function expressions given that those are the fundamental building blocks of the language; arrow function expressions are a mere convenience feature.
In fact prior to their introduction in ES2015 there were concerns expressed to the TC39 that developers would over-adopt arrow functions; these concerns seem to be bearing out, as now it isn't unusual for beginners to be misapplying them.
I think we perfectly understand each other, although we form our opinions on a very different basis. You seem to have a high language theoretical knowledge and the knowledge of the history of more programming languages. I only have some very practical programming knowledge mainly only in two languages. Actually the functional programming paradigm or the elements of it is only known for me by JS and completely new, as it is just missing from Pascal. That's truly imperative language without any functional programming tools apart from functions of course.
It is clear for both of us that JS is based on imperative programming and OOP, so it is not a truly functional programming language. It was clearly developed on OOP principles.
All I tried to say is when we apply JS in practice, especially on beginner level, we mainly use functions and functional programming ways of solving our practical problems, instead of OOP. So we hardly ever get bogged into problems with function context, simply because we just do not use too often the value of 'this' inside our functions.
We also agree that using arrow functions too early is itself can be confusing. In my opinion it is better to rely on constructs, which are similar and exist in other languages too. These are usually more elementary constructs. So for me it is obvious to start with regular functions, because they almost just like functions in good old Pascal. Arrow functions are a new concept compared to the good old Pascal. Where is the point, when we can't avoid using them in JS at least without applying 'noisy', not beginner friendly syntax?
My answer would be that when we are doing OOP or even using React class components we cannot avoid using 'this' inside our functions in some cases. The reason is that we need to reference to the object instances, which contain the function. We expect the value of 'this' to be bound to these object instances, because it is the case in good old Pascal and that's why we use 'this'.
In most cases JS works just like good old Pascal, regarding the value of 'this'. In certain cases unfortunately JS fails to behave like we expect. These are not very common cases, instead some marginal situations, like in your code sample. Your instance is not an instance of a class and the functions inside are not class methods either. In that case JS would behave just like good old Pascal, and everything would be fine. In your sample, on the other hand, the two functions are in a simple object and in cases like this, 'this' is only bound to the object automatically if you use an arrow function. If you use a regular function 'this' is bound to the global object or undefined. This JS behaviour is pretty unnatural and not like the good old Pascal at all. And this is the real reason, why arrow functions are unavoidable in these marginal cases, unless you use some 'noisy' explicit binding. Arrow functions of course are used much more often, just because these are simple and short at least after you got used to them.
I get to the same conclusion again, you are absolutely right, but to overwhelm a beginner with this is not wise. Mr Elsperger could edit his article a bit to explain how arrow functions are different from regular functions with binding the value of 'this', but he should separate it from the main material some way. Beginners will hardly ever meet the problem of function context in practical JS until they start using 'this' in their functions. At that point they are doing OOP in JS or programming with React class components, so they are not truly beginners any more.
The issue is that you are falling prey to your own expectations.
[Douglas Crockford, JavaScript - The Good Parts, 2008; p.2]
Do yourself a favour and do not expect JavaScript to behave like any other language. It's its own thing.
Don't let superficial familiarity get the better of you. Familiarity is a trap that is going to cost you considerable time and frustration.
This is certainly true, that the behavior of 'this' is not the same in JS as it is in the good old Pascal and perhaps other OOP languages. I think too much theory leads us too far. Let me share with you the video tutorial, where it is explained when it's happening with React class components.
The problem arises at 01:01:11 and it is explained pretty well. I also prepared a code sample according to your code and his React example. This is the same problem without React, and the code is more elementary than yours and a bit more practical, but also longer.
And the html:
It's not for truly beginners, but it is understandable for those, who finished the OOP parts of The Odin Project. I think it's natural to show it in OOP, because we use 'this' there more often.
Please use code blocks in the future for code. Images of code without a suitable alternate text description are not considered accessible.
I'm well aware of this issue and it identifies the primary use case of why arrow functions were introduced with ES2015 before it became fashionable to use arrow functions exclusively. It doesn't change that arrow functions were introduced as a special case feature, not a replacement for the original function syntax which is how many people use them.
Again you are judging
this
by your previous non-JS experience without fully investigating whatthis
actually is in JavaScript. While that is a natural thing to do, all technology is artificial and therefore is shaped by the context in which it is invented. The circumstances around the development of Object Pascal were very different to the circumstances of the development of JavaScript—and it shows.Wishing one was more like the other is a natural thing to do but ultimately is only wishful thinking destined to be disappointed.
If I had to guess
this
was likely added to JavaScript so that functions and objects can be easily composed. Note that I'm saying "objects", not "classes". In JavaScript you can actually "code" with objects; in most OOPLs you cannot—you are relegated to defining classes that can create the objects for you at runtime. Clearlythis
and the prototype chain inspired by Self made it possible to emulate OOP in JavaScript but at its corethis
is just the function's execution context (a special, invisible argument) set by the JavaScript runtime.Under 10.3 Execution Contexts
That's it.
Nothing about objects or OOP.
In JavaScript
this
can be a primitive value—it doesn't even have to be an object.And as we are talking about event handling I'd like to direct you to DOM handleEvent: a cross-platform standard since year 2000 (And the workaround to use it in React; vanilla demo, Preact demo).
By passing an object with a
handleEvent()
method, thethis
binding problem inherent to functions as event handlers goes away; React implemented it's own synthetic event system that never allowed for that possibility which is why the workaround is necessary.The point is that
this
often gets involved when functions and objects work together; the appearance ofthis
doesn't automatically imply the practice of class-based object-orientation in JavaScript.Thank you for explaining your opinion with such a detail about arrow functions in JS. I apologize for only giving screenshots of my code sample. It might be worth to play around it a bit. I created a tiny repository, you can clone to try the code, arrow-function-example. Because this example does not contain React, other solutions are possible, as you pointed out, apart from an arrow function or binding the value of 'this'. You can for example add a field, called parentComponent, to the buttonFunction object. You can use that in the constructor to add the value of 'this' at that point to the button. In the listener you can reach that with 'this.parentComponent' and the value will be the instance object. Even in React the situation has been changed by introducing function components and hooks. So the video shows the technology at around 2015, when arrow functions got into the language.
Your valuable and rich opinion helped me to change a couple of misunderstanding or misinterpretation, which I had about this question. You pointed out to me the following things:
My opinion is strongly biased by my previous experience in Object Pascal.
The analogy between JS and Object Pascal (and any other language) can be helpful but comes with limitations, simply because these technologies were developed in different times with different purposes, therefore can be and are different.
Particularly the role of 'this' are different in these two languages. In Object Pascal 'this' means strictly the object instance, which the class generates. In JS the meaning is more general and it just simply means the running environment, which a function is getting executed on. This can be even undefined or the value can be just a number or string, not necessarily an object and can explicitly bound to the function with the bind method, not like in Pascal.
In JS the function and Object are the more basic terms and the code can be fully built up from these not like in Pascal. OOP is not possible without classes in Pascal and we all know that classes just a syntactic convenience in JS. I have already known this but it is refreshing to see it from your point of view too.
Arrow functions got into the language to explicitly bind the value of 'this' without applying the bind method. This can solve some marginal problems, but you are not an advocate to apply them in unjustified cases, just because their syntax seem to be simpler or more fashionable than regular functions. So regular functions are the ones which we should generally apply apart from the marginal cases, what arrow functions actually solve. I agree with this. Even if you check my code sample you can see that the arrow function as a method becomes an instance method, but the regular function is a prototype method. So arrow functions are not recommended as methods on the favor of regular functions generally.
I still think this topic is strictly not for beginners, although Mr Elsperger could have included some more detailed explanation instead of simply avoiding it. This explanation should be based on some simple code sample for interested learners, but also should be separated from the main text. It could be beneficial for interested learners, because it helps us understand better the inner workings of the language.
Thank you again for sharing with me your view about this question.
You are phrasing this as an absolute rule.
My context is "when teaching beginners".
Ultimately a senior developer is going to do whatever they want but they are supposed to understand the difference and when they predominantly use arrow functions because that's "all they need" it will hopefully be in code that won't be reviewed by beginners to learn the ins an outs of JavaScript.
In my experience when beginners are introduced to arrow functions too early they make mistakes like using them in class definitions (think organizing related functions around a frequently used data structure—not necessarily OOP) when the shorthand syntax is more appropriate. They simply don't realize that the arrow functions are (unnecessarily) recreated on every object when a single function on the prototype chain will work for all the objects (n.b. my personal preference is to organize functions within modules).
I went through an "avoid
this
" phase myself (OOP with Functions, How to decide between classes v. closures in JavaScript) but I think it's impossible to work with JavaScript and avoid open source software entirely. And working with OSS you will invariably have to read other developer's code who use a coding style that you have absolutely no control over; at that point you better know how JavaScript works and that includesthis
.The other thing you are missing out on with arrow functions is hoisting. While variable hoisting can be confusing, I find function hoisting incredibly useful. It lets me write a "unit of work", give it a DAMP (descriptive and meaningful phrase) name and then put the function anywhere in the source order where it is out of the way because at that point in time the DAMP name tells me everything I need to know.
Also arrow functions can't be named. These days some JavaScript runtimes will grab the name of the variable they are assigned to and add it to the debugging meta data but there has been a long-standing recommendation to use named function expressions to make it easier to identify what you are looking at in the debugger (example).
Again careful with absolutes.
A JavaScript runtime can expose native functionality via a
class
-like interface which is actually a class in every sense of the word (JS classes are not “just syntactic sugar”). The best example is custom elements which can cause problems when you need to compile down to ES 5.1.I am of course well aware the function hoisting and I use it just like you do in every day development. When we compare arrow functions and regular ones, arrow functions can be compared better with regular function expressions. These behave the same regarding the hoisting.
The question of variable hoisting is something new for me as I always declare and even try to initialize variables before I use them. This obviously comes from the root of my Pascal training. I would have never thought of the possibility that it can be done any other way even in JS. I may be very wrong with it though:) This opportunity does not mean I will give up my habit regarding variables in my coding style.
The wide range of knowledge you have regarding very different programming languages and about their history and the theory behind them is amazing and unique. You remind me someone, I had conversation with before, but you might be the exact same person just on a different platform. It would be a great pleasure if you could register on my website and post the talk about simplicity there as well. I liked this talk because it underlines the importance of reasonability in your code quality. As he talks about the growing elephant and your task as drag it when it is really big. For him simplicity means the knowledge in design, which keeps this task reasonable. I really liked this talk.
The link is Fakebook. It is just a personal project with a few friends and strangers on it, but that would be a great thing to welcome you there, if you have the time for it.
Great content for beginners!
Head aching but I will grab it
Very nicely explained 😀
love this
it's what i needed the most to understand the callbacks and fucntion , becouse of this post i can understand what's funtion and whats not this is amazing , thank you so much
Thanks for this article. It's the first time I've read about callbacks without feeling confused or overwhelmed."