Lecture 01 - JavaScript
Introduction to JavaScript
This is not a comprehensive introduction to JavaScript (JS). Instead I am aiming to highlight some of the common “gotchas” when coming to JS from other languages like Java or Python as well as some of the features you will encounter in JavaScript that you will not have seen before.
For a more comprehensive introduction to JavaScript, I suggest the 3rd edition of Eloquent JavaScript.
Why do we care about JavaScript? It is the language embedded in the browser that allows us to programmatically manipulate the page (our topic for the next session). Increasingly JavaScript is also used on the server (that is not just in the browser).
History and standardization
A little context. Java is to JavaScript as Ham is to Hamburger. JavaScript was created in 1995 at Netscape (in 10 days!) and was named as a marketing ploy to capitalize on the growing popularity of Java (although Sun, then Oracle, owned the trademark).
JavaScript was standardized into ECMAScript, and thus JavaScript is a dialect of ECMAScript. After a period of divergence in the browser wars era, the various vendors (namely the browser creators) are now more faithfully implementing the standard.
There are numerous implementations:
- V8 (Chrome and Node)
- Spidermonkey (Firefox)
- Nitro (WebKit, Safari)
- Chakra (IE Edge)
- …
Not all engines support all features. We will be using ECMAScript 2020, an update to the standard that adds numerous helpful features. Fortunately, at this point almost all modern browsers support the 2020 specification. That is not necessarily true for newer features (newer than 2020) or older browsers.
We will also be using tools, such as transpilers and polyfills, which mitigate compatibility problem enabling us to write to a single modern standard. These will happen behind the scenes for the most part, so you won’t even think about it.
JavaScript notes
JavaScript is a very pragmatic language that has evolved to meet user needs as opposed to being formally “designed” (recall the first version created in 10 days…). As a result there is more than one way to do something, and not all are good. There was (is) a quite famous book “JavaScript: The Good Parts”, promoted as:
Most programming languages contain good and bad parts, but JavaScript has more than its share of the bad, having been developed and released in a hurry before it could be refined. This authoritative book scrapes away these bad features to reveal a subset of JavaScript that’s more reliable, readable, and maintainable than the language as a whole—a subset you can use to create truly extensible and efficient code.
Over the last few years, JavaScript has really grown up to be a decent language. Perhaps not a great one, but a decent one. However, be careful reading online suggestions/tutorials. Some are good, some are (very) outdated, some are opinionated in good ways, some are opinionated in bad ways, and some are just wrong.
We will make use of established style guides, e.g. from AirBnB and tools likeESLint, which automatically identify potentially problematic code, to help us avoid the “bad parts”.
Some examples of those gotchas mentioned earlier…
Type coercions
JavaScript does automatic type coercion, which can catch you out at odd moments when it isn’t clear which value will be coerced.
const x = '42';
console.log(x + 1);This will print out the string '421'. The + operator is acting as string concatenation, so the 1 is converted to a string and the then string '42' and '1' are concatenated together.
const x = '42';
console.log(x - 1);This will print out the value 41. The - operator isn’t valid for strings, so the '42' is converted to an integer and we subtract 1 from 42.
const x = '42';
console.log(+x + 1);This will print out the value 43. The first + is actually the unary plus, which you have probably never used before (it is much less useful than the unary minus). It is only applicable to numbers, so the '42' becomes the number 42, which is then added to the 1 to get 43.
Why does this matter (and why does JavaScript work this way)? In JavaScript we get values from outside of our code from HTML forms or through data files that are read in. By default all values arrive in JavaScript as strings. The type coercion facility is designed to ease that burden by allowing you to just treat the values as numbers anyway. This largely works… unless you try to add one of these values to another number. The unary plus trick is a common one for forcing values to either be numbers or throw an error before you make a mistake like this.
Equality (and truthiness)
Use === instead of == (ESLint).
$ node
> 5 == "5"
true
> 5 === "5"
false
This is related to type coercion. The == does its best to coerce the two sides to have the same type, and sometimes in hard to reason about ways. The === is type safe – it doesn’t so any conversions. On the face of it, it seems like you would want to use == for the auto-conversion, but the reality is that more often than not it just pushes type issues deeper into your code. Just use ===.
Variable declarations
JavaScript is dynamically typed like Python and can define variables like Python, e.g.
x = 42;but doing so makes a global variables and pollutes the global namespace (ESLint). Instead we should declare all variables as block scoped with const, if possible, or let (ESLint). const specifies that a variable will not be reassigned. However, those are ES6 features and so you will also see var declarations, e.g.
var x = 42;Prior to ES6, var was the only form of declaration. var has function-level scope (even if you re-declare a variable), instead of the more familiar block-level scope of const and let. That is, all vars are “hoisted” to the top of the function (or globally). As a result the latter is preferred to avoid tricky bugs like the following. You should use const or let, but be aware you will likely see examples with var.
As an example compare the two following functions (adapted from MDN):
function varTest() {
var x = 1;
if (true) {
var x = 2;
console.log(x);
}
console.log(x); // What will print here?
}function letTest() {
let x = 1;
if (true) {
let x = 2;
console.log(x);
}
console.log(x); // What will print here?
}All of my code and examples use const and let exclusively. I expect your code to do that same. There is a lot of example code out there that still uses var. A good way to communicate to me “I found this online and didn’t take the time to understand what I copied and pasted” would be to submit code that uses var.
Declaring functions
Functions in JavaScript are first class citizens, which is to say they can be passed around as values. This happens so often, that there is a special abbreviated syntax called “fat arrow syntax” for writing short anonymous functions in place.
That means that we can we have five different ways to declare functions.
Function declaration
This looks the closest to what you have seen in other languages.
function double(x){
return x * 2
}Function expression
We can create anonymous (unnamed) functions and use them as expressions. Here we are assigning it to the variable double so that it largely works the same as the function declaration, but we could also pass a function expression into a function as an argument (this goes for all of the function expressions below – the assignment statement is just for completeness; it is only the right hand side that is the function expression).
const double = function(x){
return x * 2;
}Named function expression
This is the same as above, but we can add a name that could be used internally (perhaps for recursion) and will show up in stack traces.
const double = function f(x){
return x * 2;
}Function expression (fat arrow syntax)
In this shorter form we get rid of the function keyword and use => to indicate that it is a function. Note that you need the leading parentheses even if there aren’t any arguments (just like we would in conventional function declarations).
const double = (x) => {
return x * 2;
};Arrow functions and anonymous functions created with function have subtle differences that won’t matter much for us.
Details Arrow functions don’t have their own this and close over the this of the enclosing scope when they are defined. Functions defined with the function keyword have their own this and that this is set differently based on how they are called. This probably doesn’t make any sense to you now, but at some point it will be important to you…
Function expression (fat arrow syntax with implicit return)
Because programmers are frequently lazy typists and our little anonymous function expressions frequently don’t have any functionality beyond returning a value, we have a version with an implicit return.
If we leave off the curly braces, this tells JavaScript that we just have a return value and we can also leave of the return keyword.
const double = (x) => 2 * x;Note that if your function returns an object, the curly braces will make it look like you want the explicit return and you will get a syntax error. To return an object from this version, surround it in parentheses.
Higher-order functions
Anonymous functions are a common concept in JavaScript. JavaScript borrows from the functional programming paradigm, and the use of higher-order functions (functions that take functions as arguments) is common.
Consider this simple for loop
const m = [4, 6, 2, 7];
for (let i = 0; i < m.length; i++) {
console.log(m[i]);
}We might rewrite this loop using the built in forEach loop on Arrays as:
m.forEach(function (i) {
console.log(i);
});or using fat arrow syntax:
m.forEach((i) => console.log(i));In general arrow functions are preferred for their conciseness. For example, instead of
const f = function moreDescriptiveNameForF() {};we could write
const f = () => {};Some common methods (operations) that use this pattern are map, filter, reduce, and sort. In each of these examples we are using higher-order functions to abstract over actions (e.g. filtering an array to keep just those elements that satisfy a predicate) not just values. What do we mean by abstracting over actions? Instead of a writing a function that filters data with specific (and fixed) predicate and applying that function to arbitrary data, we are writing a generic filter function that can be applied to arbitrary data and implement arbitrary predicates (by supplying a different predicate function value). For example:
const filterPos = (array) => {
let result = [];
for (let i = 0; i < array.length; i++) {
if (array[i] >= 0) {
result.push(array[i]);
}
}
return result;
};
const filterNeg = (array) => {
let result = [];
for (let i = 0; i < array.length; i++) {
if (array[i] < 0) {
result.push(array[i]);
}
}
return result;
};
filterPos([0,1,2,3,4]);
filterNeg([0,1,2,3,4]);Can be written as:
[0,1,2,3,4].filter((item) => item >= 0);
[0,1,2,3,4].filter((item) => item < 0);What is the difference between forEach and map? The latter returns a new array of the same length with the values produced by invoking the function argument on the input array. Knowing that, how could we implement map with forEach, i.e. how would you implement function map(a, f) such that
const a = [4, 6, 7, 9];
map(a, (item) => item + 1); // Equivalent to map(m, (item) => { return item + 1; });produces [5, 7, 8, 10]. As a hint, check out the Array methods and note that an empty array can be created with [].
const map = (a, f) => {
let result = [];
a.forEach((item) => {
result.push(f(item));
});
return result;
};Closures
Consider the following example. What will get printed?
const wrapValue = (n) => {
let local = n;
return () => local;
};
let wrap1 = wrapValue(1);
let wrap2 = wrapValue(2);
console.log(wrap1()); // What will print here?
console.log(wrap2()); // What will print here?In JavaScript, “inner” functions have access to variables defined in containing lexical scopes. That is the anonymous function created inside wrapValue can use the local local variable (similar to many other programming languages).
More than just have “access” to variables in enclosing scopes, defining a function references a variable defined in an enclosing scope creates a closure, i.e. the combination of the function and the lexical environment in which that function was declared. That environment includes any local variables that were in scope when the function was defined. Thus this code will print
1
2
as the wrap1 function value is a closure over the local variable initialize to be 1, while the wrap2 function value is a closure over the local variable initialize to be 2.
We will use closures extensively. Much of the JavaScript code we write (both “front-end” and “back-end”) is “event-based”. That is, we want to connect some particular actions to an event, such as a click, triggered by the user. We do so by attaching a “callback” to the event. That callback is typically a function that formed a closure over the necessary data for that action.
Alternately you could think about closures as being similar to objects (in a OO sense) with only one method.
Closures and var
What will be printed by the following loops (source)?
var funcs = [];
for (var i = 0; i < 3; i++) {
// Create 3 functions and
funcs[i] = () => {
// store them in the funcs array,
console.log('My value: ' + i); // each should log its value.
};
}
for (var j = 0; j < 3; j++) {
funcs[j](); // Run each function to print values
}Surprisingly:
My value: 3
My value: 3
My value: 3
What you are observing is the interaction between var scoping rules and closures. Recall that the scope of the var is the entire function (or in this the case global environment). Thus each function is closing over the same var. If we replace for (var i = 0; i < 3; i++) with for (let i = 0; i < 3; i++) the loop will work as intended, because each let variable is scoped to the loop body and there are thus three different “i” variables (slightly different than C/C++).
This would be one of those reasons to avoid just treating var as a drop in replacement for let and copying legacy code into your your work without really understanding what is going on.
Objects
Like Java and Python, JavaScript is object oriented. Everything is an object.
Objects have properties and methods, which we can access with dot notation or via the indexing operation, i.e. obj.name and obj['name'] are equivalent.
We can create object literals just like they were Python dictionaries, and work with them in similar ways:
let rectangle = {
x: 20,
y: 20,
width: 10,
height: 25,
aspectRatio: () => {
this.width / this.height;
},
};> rectangle.x
20
> rectangle['y']
20
> rectangle.color = 'red';
'red'
> rectangle
{ x: 20,
y: 20,
width: 10,
height: 25,
aspectRatio: [Function: aspectRatio],
color: 'red' }In our above example, aspectRatio is a method (a property that is a function), but it is only available on the rectangle object. To share properties between objects that are instances of a class we can use prototypes.
JavaScript is a “prototype-based language”, that is each object has a prototype. You can think of the prototype as a “fallback”. From Eloquent Javascript, a helpful introduction to this topic and the source for the following quote and description:
When an object gets a request for a property that it does not have, its prototype will be searched for the property, then the prototype’s prototype, and so on.
These prototypes (accessible via Object.getPrototypeOf(obj)) forms a tree with Object.prototype at the root.
To create a new instance of a class we need to create an object with the appropriate prototype and all the properties that instance must have. Doing so is the constructor’s job. An example JavaScript constructor:
function Hello(name) {
this.name = name;
}If you invoke the new operator on a function, that function is treated as a constructor. When you invoke new Hello, an object with the correct prototype is created (the Hello.prototype property), that object is bound to this in the constructor function, and ultimately returned by new.
All constructors (all functions) have a prototype property. There is an important distinction between the constructor’s prototype and its prototype property. The former is Function.prototype, since the constructor is a function, and the latter holds the prototype for objects created via that constructor. Properties that should be shared by all instances of a class are added to the constructor’s prototype property, e.g. Hello.prototype.
This may seem foreign to you. ES6 introduced class declarations (using the class keyword) implemented on top of JavaScript’s much more flexible prototypal inheritance features. These classes will likely seem more familiar to you and we will use them this semester.
Consider the following example (source):
class Hello {
constructor(name) {
this.name = name;
}
hello() {
return 'Hello ' + this.name + '!';
}
static sayHelloAll() {
return 'Hello everyone!';
}
}
class HelloWorld extends Hello {
constructor() {
super('World');
}
echo() {
console.log(super.hello());
}
}
const hw = new HelloWorld();
hw.echo();
hw.hello();
console.log(Hello.sayHelloAll());The equivalent ES5 code would approximately be (alternatively a more faithful translation generated by the Babel transpiler):
function Hello(name) {
this.name = name;
}
Hello.prototype.hello = function hello() {
return 'Hello ' + this.name + '!';
};
Hello.sayHelloAll = function () {
return 'Hello everyone!';
};
function HelloWorld() {
Hello.call(this, 'World');
}
HelloWorld.prototype = Object.create(Hello.prototype);
HelloWorld.prototype.constructor = HelloWorld;
HelloWorld.sayHelloAll = Hello.sayHelloAll;
HelloWorld.prototype.echo = function echo() {
console.log(Hello.prototype.hello.call(this));
};
var hw = new HelloWorld();
hw.echo();
hw.hello();
console.log(Hello.sayHelloAll());I don’t want to downplay the flexibility and power of JavaScript’s prototypal model. For example, it enables “concatenative inheritance” (often termed mixins). See this post for more examples. And I do want to note that many people are not a fan of the class keyword. If you are interested I encourage you to learn more. But I also don’t want us to get hung up on the way to our higher-level goals in the course. Thus the extensive use of the class keyword and its more familiar structure.
Closures as “Classes”
The combination of closures and “everything as an object”, allows us to use closures in ways we might use classes in other languages. Consider the following implementation of a counter.
const counter = function CounterClosure() {
let count = 0;
return () => count++;
};In action:
> let cn = counter();
undefined
> cn();
0
> cn();
1Why does the first cn() call return 0 (when it should be incrementing)? Postfix increment, i.e. ++ after the variable, returns the value of the variable before the increment operation, where as prefix increment returns the value after the increment, e.g.
// Postfix
var x = 3;
y = x++; // y = 3, x = 4
// Prefix
var a = 2;
b = ++a; // a = 3, b = 3Here, count is effectively a private member that can be manipulated by the returned closure but not accessed outside it. How could we use “everything as an object” to obtain access to the count field without incrementing? That is how could you implement a value method that would return the private count? As a hint, because functions are objects, they have properties…
const richCounter = () => {
let count = 0; // "Private" count variable
const increment = () => count++;
increment.value = () => count;
return increment;
};In action:
> let rc = richCounter();
undefined
> rc.value()
0
> rc()
0
> rc.value()
1Spreading
JavaScript has some syntax for working with objects and arrays that will seem very strange at first, and then (if you are like me) you will start trying (unsuccessfully) to use in other languages… Both of these are about rapid access to the elements of the underlying data structure.
For arrays, we have the spread operator, which is .... This treats all of the elements of the array as individuals. We use this when we have a list of values stored in an array, and we want to apply it to something that expects a comma separated list of values, not a single array.
For example, calling functions:
const sum = (x, y) => x + y;
let args = [4, 5];
console.log(sum(4, 5)); // entered manually
console.log(sum(args[0], args[1])); // reading values from the array
console.log(sum(...args)); // using spreadingWe also use it to add all of the elements of one array to another array:
const list1 = [1, 2, 3];
const list2 = [list1, 4]; // [[1,2,3],4]
const list3 = [...list1, 4]; // [1,2,3,4]This also works for objects, and can be used to clone objects or create new objects with additions fields.
const obj1 = {
a: 1,
b: 2,
};
const obj2 = { ...obj1, c: 3 }; // {a:1, b:2, c:3}With objects, latter property definitions override earlier ones, which also be useful.
const obj1 = {
a: 1,
b: 2
};
const obj2 = {...obj1, , b:4, c:3}; // {a:1, b:4, c:3}If we use ... in the function definition, it reverses the process, condensing a collection of disparate arguments into an array. These are called rest parameters.
Destructuring
Desctructuring is a related concept that allows us to extract portions of arrays and objects out during assignment statements.
With arrays, the destructuring happens based on position.
const l = [1, 2, 3];
const [a, b, c] = l; // a = 1, b= 2, c = 3We can combine it with spreading:
const l = [1, 2, 3, 4, 5, 6];
const [a, b, ...rest] = l; // a=1, b=2, rest=[4,5,6]With objects, we can name the new variables with the names of properties in the object:
const obj = {
name: 'James Robert McCrimmon',
age: 20,
nickname: 'Jamie',
};
const { name } = obj; // name = "James Robert McCrimmon"
// can do multiples as well
const { age, nickname } = obj; // age=20, nickname="Jamie"This can also be used in function arguments, which we will see a great deal. On the face of it, this seems strange, if we know we only want two fields, why write the function to accept an object that we have to take apart? This only makes sense in the context of other expectations for the functions. For example, it may be that we did not write the code that will call the function. Or we want it to work with a variety of different objects that have some fields in common.
const printName = ({ name }) => console.log(name);
// assuming our earlier object,
// but this would work on any object that has a name field
printName(obj);