Skip to main content
Version: 2020fa

Lecture 4

Lecture Video

Lecture Slides

Assignment 3 due 10/20 03:59pm

var, let, and const

Before we had ES6, there was only one way to declare variable: var. (Well, there is actually another way, but it's very bad and you should never use it.)

For example, if you want to define a variable a initialized to 21 * 2, you write

var a = 21 * 2;

However, var has a big problem: it is function scoped instead of block scoped. Therefore, you might accidentally use a local variable that should not be used anymore!

function someComplexFunction() {
var foo = 42;
if (someCondition()) {
var abc = 41 * 2;
foo += abc;
}
// You can still use abc here!
}

In ES6, we finally have let and const that are block scoped. Using block scoped variables outside of the block will be a runtime error. You should use const whenever possible since it defines an immutable variable, only use let if the variable has to be mutable. As mentioned above, you should never use var.

Here is a rewrite of the someComplexFunction above using only let and const:

function someComplexFunction() {
let foo = 42; // foo might be re-assigned. We need to use `let`.
if (someCondition()) {
const abc = 41 * 2; // abc is never mutated. We can use `const`.
foo += abc;
}
// You can NOT use abc here! This is nice!
}

Sometimes you have to use let even if the variable is only assigned one. This usually happens when then they are separately assigned inside two branches. For example,

let myMood;
if (hasPrelim()) {
myMood = 'bad';
} else {
myMood = 'good';
}

To prevent this, people use the ternary operator.

const myMood = hasPrelim() ? 'bad' : 'good';

Arrow Functions

Before ES6 we wrote functions as such:

function myFunction(x) {
return x + 1;
}

or

const myFunction = function (x) {
return x + 1;
};

Now we have this shorthand:

const myFunction = (x) => {
return x + 1;
};

The syntax is

const functionName = (parameter1, parameter2, parameter3) => {
// function body
};

For this simple function, we have even shorter shorthand:

const myFunction = (x) => x + 1;

This shorthand works if the function body is just one line that will be immediately returned. In this case, we are simply incrementing x and returning the result.

Anonymous Arrow Functions: just don't include [function name]

(parameter1, parameter2, parameter3) => {
// function body
};

Functional Programming

map

array.map(function) runs function on each element of arrayΒ and returns an array containing the results.

Example: [1, 4, 9].map(x => Math.sqrt(x)) will return [1, 2, 3].

filter

array.filter(function) runs function on each element of arrayΒ and return an array containing all elements that satisfy the function requirements.

Example: [1, 4, 9].filter(x => x > 3) will return [4, 9]

forEach

array.forEach(function) runs function on each element of array. The difference between map and forEach is that map returns a value, whereas forEach just applies the function to each element of the array.

Example: [1, 4, 9].forEach(x => console.log(x)) will print out each element to the console.

every

array.every(function) runs function on each element of array and returns whether every element of the array satisfies the function requirements.

Example: [1, 4, 9].every(x => x > 0) will return true. However, [1, 4, 9].every(x => x > 1) will return false.

some

array.some(function) runs function on each element of array and returns whether any element of the array satisfies the function requirements.

Example: [1, 4, 9].some(x => x == 1) will return true. However, [1, 4, 9].some(x => x == 2) will return false.

reduce

array.reduce(function) runs function on each element of arrayΒ and returns a single value.

Example: [1, 4, 9].reduce((sum, curr) => sum + curr) will return 14.

Spreading and Destructuring

Say we have a function:

const add3 = (a: number, b: number, c: number) => a + b + c;

Now if we had an array:

const arr = [1, 2, 3];

We can use the spread operator ... to destructure each element of the array as one of the arguments:

add3(...arr); // same as add3(arr[0], arr[1], arr[2]) output 6

Now if we had an object:

const add3Object = {
a: 3,
b: 4,
c: 7,
};

add3(...add3Object) is illegal, since the order of fields in the object is not guaranteed. However, you can refactor the add3 function to be:

type ABC = { a: number; b: number; c: number };

const add3 = ({ a, b, c }: ABC) => a + b + c;

// equivalent to:
const add3Uglier = (abc: ABC) => abc.a + abc.b + abc.c;

Then we can do

add3(add3Object);

The spread operator and destructuring is especially useful in destructuring assignment.

const [a, b] = [1, 2];

console.log(a); // 1
console.log(b); // 2

const arr1 = [1, 2, 3, 4, 5];
const [c, d, ...rest] = arr1;

console.log(c); // 1
console.log(d); // 2
console.log(rest); // [3, 4, 5]

Ugly Pieces of JavaScript

Truthy, falsy

JavaScript values can be classified into 'truthy' and 'falsy'. Of course, true is truthy and false is falsy. Most values are truthy, except:

  • false
  • ''
  • 0
  • {}
  • null
  • undefined
  • NaN

The if guard in JavaScript checks whether a value is truthy rather than whether the value is true. Similar mechanism applies to && and ||. Therefore, we have

  • '' && 'haha' evaluates to ''
  • 'haha' || '' evaluates to 'haha'.

Global variables

You were told because that there are only one way to define a variable before ES6: var. This is a white lie. You can actually define a variable without var, let, and const:

foo = 3;

If you do this, then you just define a global variable. It means you can use the variable foo everywhere. If you have a local variable, then you might accidentally use or override it with the wrong value.

Lessons learned: never use or define global variables.

Type coercion

Like most languages, JavaScript coerces types to better suit the operations that are being applied.

Example 1

If we execute true + false we get 1. This is because there is an addition operator, and true gets coerced to 1 while falseΒ gets coerced to 0.

Example 2

{} + [] + {} + [1] returns 0[object Object]1 because {} + [] gets evaluated to 0, {} gets evaluated to [object Object], and they both get coerced to strings. Then, adding a list to a string simply adds the contents of the list to the string, so 1 gets appended to the end.

Example 3

const zero = +[]; // + coerce [] into 0
const one = +!![]; // ! coerce [] into false, got inverted, then coerce to 1
const two = +!![] + +!![]; // 2 = 1 + 1

const fib2 = (__) =>
__ === zero || __ === one ? __ : fib2(__ - one) + fib2(__ - two);

This is the Fibonacci sequence implemented using type coercion.

Why Linters Are Necessary

You have just learned some darkest aspects of JavaScript. We must use it because it's the only language that can be understood by browser. To mitigate the problem, people wrote linters that try to automatically find these problems. They are well integrated with editors so you can directly see the warnings. If you follow the 'Setup your editor' section, you are good to go. You will see warnings when you accidentally write some wrong code.

Resources

I recommend the You Don't Know JS series by Kyle Simpson. The ebooks are available for free on GitHub. The series is comprehensive and will teach you everything you want to know.

Demo Code

Need more examples? This code from the lecture's live coding demo rewrites some functions from the preassessment first using loops then with map, filter, and reduce.

index.ts
// getSqrts: takes in an array and returns an array with all the square roots
// of those numbers
// example: [1, 4, 9] => [1, 2, 3]
const getSqrts = (arr: number[]): number[] => {
const result: number[] = [];
for (const num of arr) {
result.push(Math.sqrt(num));
}
return result;
};
const getSqrtsMap = (arr: number[]): number[] => {
return arr.map(Math.sqrt);
};
// perfectSquares: takes in an array and returns an array with only the
// elements that are perfect squares
// example: [1, 2, 3] => [1]
const perfectSquares = (arr: number[]): number[] => {
const result: number[] = [];
for (const num of arr) {
if (Math.sqrt(num) % 1 === 0) {
result.push(num);
}
}
return result;
};
const isPerfectSquare = (num: number) => Math.sqrt(num) % 1 === 0;
const perfectSquaresFilter = (arr: number[]): number[] => {
return arr.filter(isPerfectSquare);
};
// mySum: takes in an array and returns the sum of the elements
// example: [1, 2, 3] => 6
const mySum = (arr: number[]): number => {
let sum = 0;
for (const num of arr) {
sum += num;
}
return sum;
};
const mySumReduce = (arr: number[]): number => {
return arr.reduce((acc: number, curr: number) => acc + curr);
};
// testing!
const input = [1, 2, 3];
console.log(getSqrts(input));
console.log(getSqrtsMap(input));
console.log(perfectSquares(input));
console.log(perfectSquaresFilter(input));
console.log(mySum(input));
console.log(mySumReduce(input));