Basically, a function is a chunk of code. Generally, you first define a function, with a meaningful name, for example:
function greet() { console.log('Hello') console.log('There') }
Defining the function does not run the code! To do that, you call, or invoke, the function:
greet()
“Call” and “invoke” mean exactly the same thing.
Function definitions can have parameters, for example:
function greet(time, name) { console.log(`Good ${time}, ${name}`) }
Here time
and name
are the parameters. When you invoke the function, you supply arguments:
greet('morning', 'Alice')
The arguments are 'morning'
and 'Alice'
. Invoking the function causes Good morning, Alice
to be written to the console.
Functions can be thought of as taking in inputs and returning an output.
Example:
function average(x, y) { return (x + y) / 2 }
average(10, -4) // 3 average(2.5, 8) // 5.25
In case you are wondering, if you pass too many arguments, the extra ones are ignored. If you pass too few, the extra paremeters will have the value undefined
:
average(3, 2, 1) // 1.5, because x=3, y=2, the extra argument is ignored average(8) // NaN, because (8 + undefined) / 2 is NaN
Learn by example.
function triple(x) { return x + x + x } triple(8) // 24 triple("ho ") // "ho ho ho "
function even(x) { return x % 2 == 0 } even(-8) // true even(27) // false
// Returns whether x exactly divides y function divides(x, y) { return y % x === 0 } divides(5, 20) // true divides(2, 17) // false
// Returns whether a given year is a leap year, according to the // rules of the Gregorian calendar. A year is a leap year if it // is (1) divisible by 4 but not 100, or else (2) is divisible // by 400. function isLeapYear(y) { return divides(4, y) && !divides(100, y) || divides(400, y) } isLeapYear(1963) // false isLeapYear(1968) // true isLeapYear(2000) // true isLeapYear(2100) // false isLeapYear(2200) // false isLeapYear(2399) // false isLeapYear(2400) // true isLeapYear(920481910) // false isLeapYear(9204810000) // true
function halfOf(x) { return x / 2 } halfOf(-8) // -4 halfOf(27) // 13.5
function isLongerThan(s, n) { return s.length > n } isLongerThan("sos", 2) // true isLongerThan("sos", 3) // false
function salePrice(originalPrice, discountPercent) { return originalPrice - (originalPrice * discountPercent / 100.0) } salePrice(200, 10) // 180 salePrice(157.95, 15) // 134.2575 salePrice(157.95, 100) // 0
function circleArea(radius) { return Math.PI * radius * radius } circleArea(3) // 28.274333882308138 circleArea(1.492705) // 6.999996901568007
function sum(a) { let result = 0 for (let x of a) { result += x } return result } sum([]) // 0 sum([8]) // 8 sum([3, 9, 5, -2]) // 15
function average(a) { return sum(a) / a.length } average([3, 9, 5, -2]) // 3.75 average([8]) // 8 average([]) // NaN, but should it be???
Sometime a function cannot do what it is supposed to because something, usually the arguments it is given, just don't make any sense. In that case, you should not return anything; instead, you should fail loudly by throwing an error. Simple example: asking whether the first and last character of string are the same makes no sense for the empty string:
function sameFirstAndLast(s) { if (s === "") { throw new Error("String cannot be empty") } return s[0] === s[s.length - 1] } sameFirstAndLast("sos") // true sameFirstAndLast("hello") // false sameFirstAndLast("") // (throws an error)
Here’s a more realistic example. We’ll write a function to find a total balance of an account with principal p
invested for t
years and interest rate r
, where interest is compounded n
times per year. Note that it makes absolutely no sense to compound interst a negative number of times per year, so we should check that up front and error out if someone tries to do that:
function balanceAfter(p, n, r, t) { if (n < 0) { throw new Error("Cannot compound a negative number of times") } return p * Math.pow(1 + (r / n), n * t) }
Let’s see this function in action:
balanceAfter(1000, 12, 0.05, 10) ⟶ 1647.00949769028
balanceAfter(1, 1, 1, 20) ⟶ 1048576
balanceAfter(500, 4, -0.2, 2) ⟶ 331.7102156445312
balanceAfter(1000000, 365, 0.08, -5) ⟶ 670349.426279003
If you did not have error checking, you might get some crazy output:
balanceAfter(1000, -2, 0.05, 10) ⟶ 1659.234181850974 balanceAfter(1000, -0.06, 0.05, 10) ⟶ 2930.1560515835217 balanceAfter(1000, -0.05000002, 0.05, 10) ⟶ 1581143.8050879508 balanceAfter(1000, -0.05, 0.05, 10) ⟶ Infinity balanceAfter(1000, -0.02, 0.05, 10) ⟶ NaN
Does this give you any ideas?
Please understand the difference between a function and a function call. It is hugely important.
function dieRoll() { return Math.floor(Math.random() * 6) + 1 } dieRoll() // random value, either 1, 2, 3, 4, 5, or 6 dieRoll // the function object itself
Make sure you understand:
dieRoll
is a function
dieRoll()
is a function invocation (or call): it produces a number
This is interesting because, since functions are values, they can be assigned to variables, or even passed as arguments to other functions!
function timesFive(x) { return x * 5 } const quintupleIt = timesFive timesFive(8) // 40 quintupleIt(8) // 40
function twice(f, x) { return f(f(x)) } twice(quintupleIt, 8) // 200
How did that work?
twice(quintupleIt, 8) = quintupleIt(quintupleIt(8)) = quintupleIt(8 * 5) = quintupleIt(40) = 40 * 5 = 200
Now here is something so cool. You don't have to declare a function and use it by its name (though usually that is the right thing to do). If you like, you can write a function expression directly. You can do this in one of two ways, either as an arrow function:
// Five examples of arrow function expressions (x) => x + x + x (s, n) => s.length > n (p, n, r, t) => p * Math.pow(1 + (r / n), n * t) () => Math.random() * 100 (x, y) => { let xSquared = x * x let ySquared = y * y return Math.sqrt(xSquared + ySquared) }
or a non-arrow function:
// Five examples of non-arrow function expressions function (x) { return x + x + x } function (s, n) { return s.length > n } function (p, n, r, t) { return p * Math.pow(1 + (r / n), n * t) } function () { return Math.random() * 100 } function (x, y) { let xSquared = x * x let ySquared = y * y return Math.sqrt(xSquared + ySquared) }
Arrow functions and Non-arrow functions sometimes behave differently.
We haven’t seen examples of how they differ yet, but we will. Unless your functions are prperties of objects, or use the famous expressionthis
, you don’t have to worry about the difference.
Functions that you name in a function declaration (like we’ve been doing all along) behave like non-arrow functions.
Now we can pass functions with their expression directly:
function twice(f, x) { return f(f(x)) } twice(x => x**2, 3) // 81 twice(x => x + "e", "be") // "beee" twice(x => x / 2, 20) // 5
This is not mysterious at all; in fact it is quite natural. Just work things out:
twice(x => x**2, 3) = (x => x**2)((x => x**2)(3)) = (x => x**2)(3**2) = (x => x**2)(9) = 9**2 = 81
A function value you use without first making a named function definition is called an anonymous function. They are frequently used when processing arrays:
const a = [3, 2, 7, 8, -2] a.every(x => x > 5) // false a.some(x => x > 5) // true a.map(x => x ** 2) // [9, 4, 49, 64, 4] a.filter(x => x % 2 === 0) // [2, 8, -2] a.reduce((x, y) => x + y) // 18 // This writes some lines to the console... try it. a.forEach(x => { console.log(`Look it is ${x}`) })
Details:
a.every(f)
produces true if f(x)
is truthy for all elements x
of a
, or false otherwise. The elements of a
are tested in order, and the testing
will stop as soon as a test is falsy.
a.some(f)
produces true if f(x)
is truthy for at least one element x
of a
, or false otherwise. The elements of a
are tested in order, and the testing
will stop as soon as a test is truthy.
a.forEach(f)
calls f(x)
for each element x
of a
, in order.
a.map(f)
produces [f(a0),f(a1),f(a2),f(a3),...]
.
a.filter(f)
produces a new array containing all elements x
of a
for which f(x)
is truthy.
a.reduce(f, x)
produces f(x, f(a0, f(a1, f(a2, f(a3, ...)))))
. If the second
parameter is not supplied, the call produces f(a0, f(a1, f(a2, f(a3, ...))))
, but will
throw a TypeError
if a
is empty.
NOTE: The function passed to all of these operations can actually take more than one argument; we’ve only showed the simplest cases. For full details, see the documentation for the Array object at MDN.
One thing cool about all this is that you can chain a bunch of array functions together, so expressions look a “dataflow pipeline.”
// Return the sum of all the square roots of the numbers bigger than 100 from an array. function sumOfSquareRootsOfNumbersBiggerThan100(a) { return a .filter(x => x > 100) .map(x => Math.sqrt(x)) .reduce((x, y) => x + y) }
Note that you don’t have to use arrow functions here. If you want you can be a litte more wordy:
// Return the sum of all the square roots of the numbers bigger than 100 from an array. function sumOfSquareRootsOfNumbersBiggerThan100(a) { function biggerThan100(x) { return x > 100 } function add(x, y) { return x + y } return a.filter(biggerThan100).map(Math.sqrt).reduce(add) }
If an arugment is an object, your function can, if you choose to, mutate the argument directly! Be somewhat careful with this. It is sometimes super convenient, and exactly what you want, but if not expected it could surprise the caller. Can you find the difference between these two functions?
// Mutates point p so it moves down one unit in the y-direction function moveDown(p) { p.y -= 1 } // Returns the point that is one unit down from p in the y-direction. Leaves p unchanged. function downFrom(p) { return { x: p.x, y: p.y - 1 } }
Let’s define and invoke them in the console, and explore their behavior:
> const player = {x: 9, y: 8} > moveDown(player) > player {x: 9, y: 7} > const q = downFrom(player) > q {x: 9, y: 6} > player {x: 9, y: 7}
How about another example?
/* * Uppercases all the strings in an array. Mutates the argument. */ function uppercaseAll(a) { for (let i = 0; i < a.length; i += 1) { a[i] = a[i].toUpperCase() } }
/* * Returns a new array that is equivalent to its input array but * with its elements uppercased. Does not mutate the argument. */ function uppercased(a) { let result = [] for (let s of a) { result.push(s.toUpperCase()) } return result }
> let b = ['good', 'morning', 'los angeles']; > uppercaseAll(b) undefined > b [ 'GOOD', 'MORNING', 'LOS ANGELES' ] > b = ['good', 'morning', 'los angeles']; > uppercased(b) [ 'GOOD', 'MORNING', 'LOS ANGELES' ] > b [ 'good', 'morning', 'los angeles' ]
uppercaseAll
be rewritten to use a for-of loop? Why or why not?
Functions provide a great way to modularize code. Modularizing means to make program components focus on one thing and do that one thing well. We shouldn’t be mixing concerns together unnecessarily. For example you should not mix up computations with user interface concerns. For example, if you had a UI application where you entered a number and wanted to see if it is prime while you type, you should not write:
// // THIS CODE IS BAD. IT MIXES UP UI AND COMPUTATION CONCERNS. ALSO // IT REPEATS A LOT OF CODE. IT IS JUST MISERABLE IN SO MANY WAYS. // inputBox.addEventListener('input', () => { const n = Number(inputBox.value) if (isNaN(n) || !Number.isInteger(n)) { resultArea.textContent = 'Not an integer' } else if (n < 2 || n > Number.MAX_SAFE_INTEGER) { resultArea.textContent = 'Number too big or too small' } else if (n === 2 || n === 3) { resultArea.textContent = 'Prime' } else if (n % 2 === 0 || n % 3 === 0) { resultArea.textContent = 'Not Prime' } for (let k = 5, w = 2; k * k <= n; k += w, w = 6-w) { if (n % k === 0) { resultArea.textContent = 'Not Prime' return } } resultArea.textContent = 'Prime' })
Instead you should separate the UI from the computation. There will be more code, but it will be much cleaner.
// // THIS CODE IS BETTER. LONGER, BUT BETTER. SEPARATION OF CONCERNS FTW. // inputBox.addEventListener('input', () => { try { resultArea.textContent = isPrime(+inputBox.value) ? 'Prime' : 'Not Prime' } catch (e) { resultArea.innerHTML = `<span class="error">${e}</span>` } }) // So awesome! No input-output here! Just pure, beautiful, prime checking! Reusable!! function isPrime(n) { if (isNaN(n) || !Number.isInteger(n)) { throw 'Not an integer' } else if (n < 2 || n > Number.MAX_SAFE_INTEGER) { throw 'Number too big or too small' } else if (n === 2 || n === 3) { return true } else if (n % 2 === 0 || n % 3 === 0) { return false } for (let k = 5, w = 2; k * k <= n; k += w, w = 6-w) { if (n % k === 0) { return false } } return true }
See the Pen Trivial Prime Tester by Ray Toal (@rtoal) on CodePen.
This is really important software engineering!
Most functions should be like this one: they should only return a result or throw an exception. Period. Return and/or throw ONLY! They should not print or alert or log anything!
Prompting, alerting, or rendering results to a web page should be part of the “user interface layer” of a program. Functions should just do their business without worrying about the UI. All communication with a user is separated out into its own portion of the script and not part of these utility functions that carry out business concerns.
In addition to making the code more understandable, having a self-contained, nonalerting prime number function is great: the function is reusable.
Since JavaScript has only seven types (Undefined, Null, Boolean, Number, String, Symbol, and Object), functions must be (and are) a kind of object (just like arrays). So they can have properties.
When you create a function, it gets two properties whether you want them or not:
length
, initialized to the number of parameters it is defined with.
prototype
, initialized to an object with a single property, constructor
, which references the function itself.
So this declaration
function average(x, y) { return (x + y) / 2 }
results in this:
You don’t have to worry about this complexity just this second. But note this: remember that since you add new properties to objects at any time, and because functions are objects, you can invent your own properties:
function average(x, y) { average.calls += 1 return (x + y) / 2 } average.calls = 0 // Let’s invent a new property console.log(average(4, 8)) // Prints 6 console.log(average(10.5, 11)) // Prints 10.75 console.log(average(0, 1)) // Prints 0.5 console.log(average.calls) // Prints 3
Because functions are values, they can be values of object properties. We generally exploit this in two ways.
First we can package up related functions:
const Geometry = { circleArea: function (radius) { return Math.PI * radius * radius }, circleCircumference: function (radius) { return 2 * Math.PI * radius }, sphereSurfaceArea: function (radius) { return 4 * Math.PI * radius * radius }, boxVolume: function (length, width, depth) { return length * width * depth } }
Second, we can place functions inside of objects as a way to cleanly represent an object’s “behavior”, made possible by the this expression:
let circle = { radius: 5, area: function () {return Math.PI * this.radius * this.radius;}, circumference: function () {return 2 * Math.PI * this.radius;} } console.log(circle.area()) // Prints 78.53981633974483 circle.radius = 1.5 // Change the circle’s radius console.log(circle.circumference()) // Prints 9.42477796076938
What is the meaning of this
? The expression this
has one of several meanings depending on context. When called via an expression such as circle.area()
, it refers to the object through which the function is called, which we call the receiver. In that case we say the function is a method.
SUPER IMPORTANT FUNCTION FACT
The value ofthis
refers to the object through which the contained function is called only if the function is defined with thefunction
syntax, NOT the arrow syntax.
Writing code designed primarily around objects containing methods is a major component of object-oriented programming.
Before moving on, you need to learn a syntax shortcut. When the value of an object property is a NON-ARROW function, we can omit the word function
, like this:
const Geometry = { circleArea(radius) {return Math.PI * radius * radius;}, circleCircumference(radius) {return 2 * Math.PI * radius;}, sphereSurfaceArea(radius) {return 4 * Math.PI * radius * radius;}, boxVolume(length, width, depth) {return length * width * depth;} }
And also like this:
let circle = { radius: 5, area() {return Math.PI * this.radius * this.radius;}, circumference() {return 2 * Math.PI * this.radius;} }
Always use thefunction
form (or its shorthand) when defining methods inside objects. Never use the=>
arrow form for methods. The value ofthis
is not bound to the containing object for arrow functions.
If you are going to create objects with methods, you want to be careful that you don’t make unnecessary copies of the functions in many similar objects:
// WASTEFUL: EACH CIRCLE GETS ITS OWN REDUNDANT COPIES OF ITS METHODS. // At least we don't have to repeat them in source code, though. function Circle(r) { return { radius: r, area() { return Math.PI * this.radius * this.radius }, circumference() { return 2 * Math.PI * this.radius } } }
So what’s the problem? Well, execute let c1 = Circle(2); let c2 = Circle(10);
and
look what you get:
We have extra function objects laying around, and they take up space! So how do we fix it? Answer: Prototypes, of course! Each circle needs its own radius, but we can put the area and circle functions in a shared prototype object. There are many ways to set up code to do this:
Here is a way to implement option #2 (easy to do because every function already has a prototype property!):
/* * A circle datatype. Synopsis: * * let c = Circle(5) * c.radius => 5 * c.area() => 25&pi * c.circumference() => 10&pi */ function Circle(r) { let circle = Object.create(Circle.prototype) circle.radius = r return circle } Circle.prototype.area = function () { return Math.PI * this.radius * this.radius } Circle.prototype.circumference = function () { return 2 * Math.PI * this.radius }
Note how we used the long-form of the function value, and NOT THE ARROW FORM, because we wanted to access this
.
Now when we create circles like so:
let c1 = Circle(2) // Creates a circle with radius 2. let c2 = Circle(10) // Creates a circle with radius 10.
we get this:
Much better. There is only one copy of each method, even if we have millions of circles.
JavaScript has an operator called new, that you put before a function call to build exactly the same situation above but with fewer lines of code. Here is how we do it with operator new
:
/* * A circle datatype. Synopsis: * * let c = new Circle(5) * c.radius => 5 * c.area() => 25&pi * c.circumference() => 10&pi */ function Circle(r) { this.radius = r } Circle.prototype.area = function () { return Math.PI * this.radius * this.radius } Circle.prototype.circumference = function () { return 2 * Math.PI * this.radius } // Must call it with new to make it work: let c1 = new Circle(2) // Creates a circle with radius 2. let c2 = new Circle(10) // Creates a circle with radius 10.
So what is the meaning of new
? If you prefix a function call with new
then the following happens:
prototype
property.
The use of functions as “constructors” with methods in the prototype is so common, JavaScript has some cool syntactic sugar:
/* * A circle datatype. Synopsis: * * let c = new Circle(5) * c.radius => 5 * c.area() => 25π * c.circumference() => 10π */ class Circle { constructor(r) { this.radius = r } area() { return Math.PI * this.radius * this.radius } circumference() { return 2 * Math.PI * this.radius } } // Must call it with new to make it work: let c1 = new Circle(2) // Creates a circle with radius 2. let c2 = new Circle(10) // Creates a circle with radius 10.
Note: The class construct generates exactly the same code as the previous example. Even though you said “class Circle
,” you made a function called Circle
, and the area and circumference methods got placed in Circle.prototype
.
The special keyword this has one of four different meanings.
apply
, call
, bind
(and a few other operations), a custom value for this
can be used.
new
operator, it refers
to the object being created. (We saw this already.)
new
, it refers to the object the function was called through; this includes the global object—because all global variables, including global functions, are properties of some unnamed global object.
this
globallyHere’s an example when used in the global scope (this is rare):
this.x = 2 // Set the x property of the global object. console.log(x) // Prints 2. (Global vars are properties of global object) function f(x) { this.y = x + 1 } f(2) // f is called as a global function. console.log(y) // Prints 3.
It is more common to accidentally screw up and whack global variables by misusing constructors:
function Point(x, y) { this.x = x this.y = y } let p = new Point(4, -5) // New point created. let q = Point(3, 8) // DANGER! DANGER! Sets global x and y!
apply
, call
, and bind
The function methods apply
, call
, and bind
allow you to specifically define the object you want to use as the value of this
.
function f(a, b, c) { this.x += a + b + c } let a = { x: 1, y: 2 } f.apply(a, [10, 20, 5]) // Calls f(10, 20, 5), using 'a' as this. f.call(a, 3, 4, 15) // Calls f(3, 4, 15), using 'a' as this. f.bind(a)(10, 100, 30) // Calls f(10, 100, 30) using 'a' as this. console.log(a.x) // Logs 198.
These methods allow you to hijack constructors:
function Point(x, y) { this.x = x this.y = y } let p = { z: 3 } Point.apply(p, [2, 9]) // Now p is {x: 2, y: 9, z: 3} Point.call(p, 10, 4) // Now p is {x: 10, y: 4, z: 3} Point.bind(p)(3, 5) // Now p is {x: 3, y: 5, z: 3}
So until now, we just seen relatively simple functions. You declare them with n parameters and pass them n arguments. But there’s much more to be aware of!
What if you pass too many or too few arguments? Answers: Unmatched parameters are undefined
. Extra arguments are ignored.
function f(x, y) { return [x, y] } f() // [ undefined, undefined ] f(1) // [ 1, undefined ] f(1, 2) // [ 1, 2 ] f(1, 2, 3) // [ 1, 2 ]
If you want, you can specify default parameter values in case values are not passed (or someone passes undefined
):
function f(x = 5, y, z = 2) { return [x, y, z] } f() // [5, undefined, 2] f(8) // [8, undefined, 2] f(7, 9) // [7, 9, 2] f(2, 1, 0) // [2, 1, 0] f(3, 8, undefined) // [3, 8, 2] f(undefined, 1) // [5, 1, 2]
undefined
?”
Ever wonder how some functions, like Math.max
, can take any number of arguments whatsoever? Even a million arguments but it still works? Here’s how. You can make the last parameter be a rest parameter, which will ”roll up” or “pack” all of the final arguments into an array. Like this:
function f(x, ...y) { return y } f(8) // [] f(5, 9) // [9] f(2, 1, 0) // [1, 0] f(3, 8, 8, 3) // [8, 8, 3]
When calling functions, you are really assigning arguments to parameters, so destructuring works as you’d expect:
function f({x, y}, [a, b]) { return x + " " + y + " " + a + " " + b return `${x} ${y} ${a} ${b}` } f({y: 10, z: 5}, [1, 2, 3]) // undefined 10 1 2 f({x: 0, y: 8}, [1]) // 0 8 1 undefined let z = {x: 3, y: 5} f(z, [9, 2]) // 3 5 9 2
Spreads work as you’d expect:
function f(first, second, third) { return `${first} ${second} ${third}` } let dogs = ['spike', 'sparky', 'spot'] f(dogs) // 'spike,sparky,spot undefined undefined' f(...dogs) // 'spike sparky spot'
JavaScript does functions better than almost any other language. The real power of JavaScript is often seen when you have functions inside of functions, and especially when functions return functions.
The classic example from mathematics of an operation that creates a new function is composition:
function compose(f, g) { return x => f(g(x)) }
We can use it like this:
const square = x => x * x const addSeven = x => x + 7 const squareThenAddSeven = compose(addSeven, square)
Let’s evaluate:
squareThenAddSeven(5) = compose(addSeven, square)(5) = addSeven(square(5)) = addSeven(5 * 5) = addSeven(25) = 25 + 7 = 32
If you can follow the above example, where we continually simplify an expression by replacing function calls with their results, feel free to use this method of working out complex-looking function-heavy expressions. It gives you a pretty good insight into what is going on.
squareThenAddSeven
example in a shell. Then create and test some composition examples of your own.
Here’s another one, which is pretty simple:
function add(x) { return y => x + y }
How does this work? Example:
add(8)(5) = (y => 8 + y)(5) = 8 + 5 = 13
So add(8)
is the function that adds 8 to its argument. Note that add(x)(y)
is an expression that adds x
and y
, as if add
was some kind of two-argument function. But it’s not: it’s a one argument function that returns a function. It only looks like a two-argument function. Functions like this are called curried functions. Because of Haskell Curry.
We could also use a curried function like this:
const addTenTo = add(10)
then
addTenTo(5) = (y => 10 + y)(5) = 10 + 5 = 15
And by the way, we could have written theadd
function like this:const add = x => y => x + yOoooh, ever seen the lambda calculus? That’s just the JavaScript way to say $\lambda x. \lambda y. x + y$, isn’t it?
In practice we can manufacture functions for all kinds of things:
function delimitWith(prefix, suffix) { return s => `${prefix}${s}${suffix}` } const withParentheses = delimitWith("(", ")") const withBrackets = delimitWith("[", "]") const withBraces = delimitWith("{", "}")
Now, withParentheses
, withBrackets
, and withBraces
are functions you can use.
withBrackets("sic")
?
But now here’s an amazing use of returning functions. We can make a function so that every time we call it, it returns a different value, based on the last time we called it. For example, here’s a way to write a function such that the first time we call it we get 0, the next time 1, then 4, then 9, then 16, and so on. Each call returns the next square.
const nextSquare = (() =>{ let n = -1 return () =>{ n += 1; return n * n } })() nextSquare() // 0 nextSquare() // 1 nextSquare() // 4 nextSquare() // 9 nextSquare() // 16
The value assigned to nextSquare
is the result of calling an anonymous function. This function returns an (inner) function which is then assigned to nextSquare
. This little function has access to the variable n
. No one else can see or change n
. This function, which has access to the variables closed around it but not accessible to any place else in the code, is called a closure.
Hiding data within (usually anonymous) functions is super common. Remember the BMI script from earier in the semester? It had a bunch of global variables. Look how we can hide them:
(function () { let poundsBox = document.getElementById("weight") let inchesBox = document.getElementById("height") let bmiSpan = document.getElementById("bmi") const KILOS_PER_POUND = 0.453592 const METERS_PER_INCH = 0.0254 function computeBMI() { let kilos = +poundsBox.value * KILOS_PER_POUND let meters = +inchesBox.value * METERS_PER_INCH bmiSpan.innerHTML = kilos / (meters * meters) } poundsBox.addEventListener('input', computeBMI) inchesBox.addEventListener('input', computeBMI) }())
This code is a single statement: a function call. The function it calls set up the event handlers. The handlers manipulate private data, hidden from any other scripts the browser might have loaded. The code is unobtrusive.
It’s called something else, too. Any time you have an expression that is a function call of an anonymous function expression, we call this an immediately invoked function expression, or an IIFE.
When you call a function, any parameters are new variables distinct from any other variable. Completely new, completely distinct. Distinct from the arguments passed in. Distinct from global variables of the same name. Distinct from the same parameter in different invocations. Totally new, totally distinct from all other variables.
So if you change a parameter’s value, that will not affect the argument:
let x = 5 function f(y) { y = 10; console.log(y) } f(x) console.log(x) // Try it... This script logs 10 then 5. Changing y did NOT change x.
Nor does changing a parameter’s value change globals of the same name:
let x = 5 function f(x) { x = 10; console.log(x) } f(100) console.log(x) // Try it... This script logs 10 then 5. Changing parameter x did NOT change global x.
Nor is a change to a parameter remembered in future calls:
function f(x) { console.log(x); x = 10 } f(100) f() // Try it... This script logs 100 then undefined.
A variable’s scope is the region of the source code where it is visible. In JavaScript, a variable has either global scope, function scope, or block scope.
var
within a function body, have function scope and are visible only within their function (and any functions nested within said function).
let
or const
have block scope and are visible only within the block that they are declared in. The block may be the whole function body.
From these rules you can infer:
Now here are two other language design choices made by Eich. He didn’t have to do things this way, but he did, and you absolutely positively have to know these things:
var
or let
or const
in a declaration makes the variable a global variable, even if inside a function! See a real-life example of huge bug resulting from a missing declaration.
To really understand this stuff, you have to work through examples. These will become second nature to you at some point, but you have to pay the price in effortful study:
Example 1. Globals are visible within inner scopes.
let message = 'Hello' // variable message visible from here to end of script function greet() { console.log(message) // Refers to (global) declaration on line 1 } greet() // Prints "Hello"
Example 2. Locals are not visible in outer scopes.
function meaning() => { let answer = 42 // Local variable, in scope until the end of the function } // End of function, so answer is now out of scope meaning() console.log(answer) // Throws ReferenceError, can’t see answer out here
Example 3. Locals hide globals of the same name.
let message = 'Hello' function greet() { let message = 'Hola' // A NEW local variable, DIFFERENT than the outer one console.log(message) // Prints "Hola": inner one shadows (hides) outer one } greet() console.log(message) // Prints "Hello": global was just hidden, not changed.
Example 4. Forgetting let, const, or var trashes a global.
let message = 'Hello' function greet(); { message = "Hola" // O NOES! This is an assignment, NOT a declaration console.log(message) // We assigned to the global, so logs "Hola" } greet() console.log(message) // Prints "Hola": global got changed! Disaster!
Example 5. The scope of local variables declared with let
or const
is the whole function, but JavaScript throws an error if you try to use a variable before its declaration (the temporal dead zone).
let message = 'Hello' function greet() { console.log(message) // Throws a ReferenceError (we’re in the TDZ) let message = 'Hola' // Because the declaration is here console.log(message) } greet() // The error will be propagated out of here console.log(message) // Execution never gets here
Example 6. Local variables declared with var
are hoisted.
let message = 'Hello' function greet() { console.log(message) // Prints undefined, local variable not yet initialized var message = 'Hola' // New local variable defined here, but scope is whole body console.log(message) // Prints "Hola" now (as expected) } greet() console.log(message) // Prints "Hello" - global was just hidden, not changed.
Hoisting? What does that mean? It means that variable declarations within a function body work as if they are invisibly pulled up to the top of the body. In other words, this:
function f() { console.log(x) var x = 1 console.log(x) }
works exactly the same as this:
function f() { var x console.log(x) x = 1 console.log(x) }
Hoisting is pretty weird, so do yourself a favor and always use let
or const
, and never use var
. (Technically, let
and const
sort of do hoisting, but only hoisting in terms of shadowing outer variables of the same name; you can’t do anything in the TDZ.)
Example 7. The scope of local variables declared with let
or const
is its innermost block:
const x = 3 if (true) { let x = 5 // New variable, local to this block console.log(x) // Prints 5 } console.log(x) // Prints 3
Example 8. With let
or const
, you get a fresh variable each time through the loop. What does this mean? To find out, let’s try to create an array of functions $a = [f_0, f_1, ..., f_9]$ such that calling $f_i$ returns $i$, e.g. a[5]() === 5
. This will fail miserably:
// HORRIBLE NEWBIE MISTAKE const a = [] for (var i = 0; i < 10; i += 1) { a[i] = () => i } a[3]() // 10 a[7]() // 10
Wha?! Actually this makes sense! The scope of i is very large, going beyond the for loop. We are filling an array with functions that all return the same i. None of these functions are called until the loop is finished, and by this time, i is 10.
With let
, there is no such problem:
const a = [] for (let i = 0; i < 10; i += 1) { a[i] = () => i } a[3]() // 3 a[7]() // 7