Lecture 14 Functional JS

Javascript was a language that had…

…the functionality of Scheme
(This Lecture)
…the object orientation of Self
(This Lecture)
…and the syntax of Java
(First JS lecture)

Functions and arguments

Function Definition Syntax

Function Declaration


					function hypot (x, y) {
						return Math.sqrt(x ** 2 + y ** 2);
					}
			

Function Expression


					let hypot = function (x, y) {
						return Math.sqrt(x ** 2 + y ** 2);
					};
			

Arrow functions


				let hypot = (x, y) => Math.sqrt(x ** 2 + y ** 2);
			

				let hypot = (x, y) => {
					return Math.sqrt(x ** 2 + y ** 2);
				}
			
- [Arrow functions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions) are a more compact way to write functions, a lot can be omitted. - Even the parentheses around arguments are optional if you only have one argument. - Besides syntax, they do have some differences from other functions, which we will see later in this lecture.

Invocation is always the same


			hypot(3, 4); // 5
		

Invocation vs reference When is "Thank you 😊" logged?


			let handler = evt => console.log("Thank you 😊");
			button.addEventListener("click", handler());
		
  1. When I click the button
  2. Immediately
  3. Both immediately and when I click the button
  4. Never, there’s an error
Why does this happen? Because we invoked the function, so we actually assigned its return value (undefined) as the event listener. This is a very common mistake.

Missing arguments


			let hypot = (x, y) => Math.sqrt(x ** 2 + y ** 2);
			console.log(hypot(5)); // What does this log?
		
  1. 7.071067812
  2. NaN
  3. TypeError: Argument 'y' not provided in function 'hypot'

Variadic functions


				let sum = function (...numbers) {
					let total = 0;

					for (let n of numbers) {
						total += n;
					}

					return total;
				}
			

				let avg = function(...numbers) {
					return sum(...numbers) / numbers.length;
				}
			
- JS allows you to use any number of arguments when calling a function without erroring anyway - However, to capture a variable number of arguments as an array, you can use the [rest parameter syntax](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/rest_parameters) - Note that you can use the [spread syntax](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax) to "spread" an array's elements into positional arguments, e.g. to be used in calling another function - Note that the rest parameter syntax can be combined with regular paramters (as long as it comes after them). That is why it's called the "rest" parameter, because it captures the rest of the parameters.

Default values


			let hypot = (x, y = x) => Math.sqrt(x ** 2 + y ** 2);
			console.log(hypot(5)); // What does this log?
		
  1. 7.071067812
  2. NaN
  3. TypeError: Argument 'y' not provided in function 'hypot'

API Design Tip Avoid optional positional arguments


			let event = document.createEvent('KeyboardEvent');
			event.initKeyEvent("keypress", true, true, null, null,
			                   true, false, true, false, 9, 0);
		

API Design Tip Prefer dictionary arguments


			let event = new KeyboardEvent("keypress", {
				ctrlKey: true,
				shiftKey: true,
				keyCode: 9
			});
		
- Clear what each parameter is - No need to remember order - No need to provide superfluous parameters simply to be able to provide another parameter that comes later

First-class functions

Programming with Functions

Second-class functions

  • Functions are executable code
  • Programmers write them
  • Functions create and manipulate data
  • Arguments and return values are data
  • Functions can reduce repetitive code for managing data

First-class functions

  • Functions are data
  • Computer can manipulate them
  • Functions can create and manipulate functions
  • Arguments and return values can be functions
  • Functions can reduce repetitive code for invoking functions
A programming language is said to have first-class functions if it treats functions as [first-class citizens](https://en.wikipedia.org/wiki/First-class_citizen). This means it supports passing functions as arguments to other functions, returning them as the values from other functions, and assigning them to variables or storing them in data structures.

Functions can be function arguments


					<button id=button>Click me</button>
			

				button.addEventListener("click", function (event) {
					event.target.textContent = "Thank you 😊";
					});
			
- In JS functions are objects and can be passed around in arguments! - Note that this function has no name. Functions in JS can be anonymous, naming them is optional.

Functions can be variable values


				let hypot = (x, y) => Math.sqrt(x ** 2 + y ** 2);
			

One function, multiple references What happens?


			let f = x => x + 1;
			let g = f;
			f = 42;
			g(1);
		
  1. 42
  2. 2
  3. TypeError: g is not a function
What happens here? 1. `f` is first a pointer to the value `x => x + 1`, a function 2. Now `g` also points to the same function 3. Now `f` points to the value `42` instead of the function it previously held. `g` has not changed. 4. `g` invokes the function `x => x + 1` and binds `x` to `1`, thus it returns `2`.

Functions can be object properties


			let greet = function() {
				console.log(`Hi, I’m ${this.name}`);
			};
			let instructors = [
				{name: "Lea", greet: greet},
				{name: "David", greet: greet}
			];

			for (let instructor of instructors) {
				instructor.greet();
			}
		
- When functions are properties of objects they are called *methods* - There is nothing special about these properties, they can be overwritten, reassigned, added to objects we don’t own etc. - When a function is also a method, its special `this` parameter points to the object itself. This is called the *function context* and we will explore it in more detail soon. - Note that `console.log()` is also a method of the `console` object.

Methods = Functions + Object properties

" Hello!   ".trim() // "Hello!"
- This is a good heuristic about whether we need a method: if we don't actually need access to an object, then there's no point making the function a method.

Functions can even be function return values


				function interpolate(min, max) {
					return function (p) {
						return min + (max - min) * p;
					}
				}
			

				let range = interpolate(1, 10);
				console.log(range(.1), range(.5), range(.9));
			

Why first-class functions?

Higher Order Functions

Functions that:

  • Take functions as parameters, and/or
  • Return functions

Higher Order Functions Example: Currying


				function curry(f, ...fixedArgs) {
					return function(...args) {
						return f(...fixedArgs, ...args);
					}
				}
			

Or, more succinctly (but less clearly):


					let curry = (f, ...fixedArgs)
					          => (...args)
					          => f(...fixedArgs, ...args);
				

			function interpolate(min, max, p) {
				return min + (max - min) * p;
			}

			let range = curry(interpolate, 1, 10);
			console.log(range(.1), range(.5), range(.9));
		

Curry?

[Haskell Brooks Curry]() was

Map, Reduce, Filter

Map, Reduce, Filter

Common patterns for transforming a collection of items en masse

In JS these are all methods on any Array object. Each takes a callback: a function that will be applied to each item.

Map: Transform Each Item

Without map


					let numbers = [1, 2, 3, 4];
					let squares = [];

					for (let n of numbers) {
						squares.push(n ** 2);
					}
			

With map


					let numbers = [1, 2, 3, 4];
					let squares = numbers.map(n => n ** 2);
			
Map transforms an array using a transformation function and returns the result as a new array

Filter: Select some Items

Without filter


					let numbers = [1, 2, 3, 4, 5];
					let odd = [];

					for (let n of numbers) {
						if (n % 2) {
							odd.push(n);
						}
					}
			

With filter


					let numbers = [1, 2, 3, 4, 5];
					let odd = numbers.filter(n => n % 2);
			
Filter returns a new array with only the items for which the filtering function returned a truthy value

Reduce: Combine all items

Without reduce


					let numbers = [1, 2, 3, 4, 5];
					let sum = 0;

					for (let n of numbers) {
						sum += n;
					}
			

With reduce


					let numbers = [1, 2, 3, 4, 5];
					let sum = numbers.reduce(
						(acc, current) => acc + current,
						0 // initial value
					);
			

Reduce code arguably longer or more complicated, but still more informative

These can be chained

Sum the squares of odd numbers from 1 to 5


			let numbers = [1, 2, 3, 4, 5];
			let result = numbers
				.filter(n => n % 2)
				.map(n => n ** 2)
				.reduce((acc, cur) => acc + cur, 0);
		
Because each array method returns an array as well, we can continue calling array methods on the result. This pattern is called *chaining*, and is very popular in the JS world. Do note the performance implications: this loops 3 times, whereas a carefully crafted loop-based piece of code would only need to loop once. However, both of these are O(N).

More iterative functions

Closures

Closures


				function counter(start = 0) {
					let i = start;
					return () => ++i;
				}
			

				function counter(i = 0) {
					return () => ++i;
				}
			

				let a = counter();
				console.log(a(), a()); // 1, 2
			
  • Sometimes a function you create at runtime has to remember something from its time of creation that will be gone by the time of execution
  • Javascript is lexically scoped
  • Code in a block has access to the local variables in that block
  • …and the block that contains it, and that one's container, etc.
  • Access persists even after execution leaves the block

What is happening here?

  • Invoking counter defines local variable i
  • The inner function has access to the i variable
  • …even when it’s called after counter returns

What will happen when the button is clicked?


			let i;
			for (i = 1; i <= 3; i++) {
				button.addEventListener("click", evt => {
					console.log(i);
				});
			}
		
  1. The console logs 1, 2, 3
  2. The console logs 3, 3, 3

What will happen when the button is clicked?


			for (let i = 1; i <= 3; i++) {
				button.addEventListener("click", evt => {
					console.log(i);
				});
			}
		
  1. The console logs 1, 2, 3
  2. The console logs 3, 3, 3

What about this?

this is a special parameter that is always defined. It is called the function context.

Always? What if we’re not in a method?

Global context What is logged?


			<script type="module">
				console.log(this);
			</script>
		
  1. undefined
  2. Window
  3. ReferenceError: this is not defined
In modules, `this` on the top level is defined, but set to `undefined`. If you forget `type="module"` on your `<script>` element, you will get different behavior, since the global object will then be the `window` object.

Top-level functions What is logged?


			<script type="module">
				let logContext = function() {
					console.log(this);
				}

				logContext();
			</script>
		
  1. undefined
  2. Window
  3. ReferenceError: this is not defined
Nothing surprising here, the context of top level functions is the same as the top-level context.

The global object


			<script type="module">
				console.log(globalThis); // Window
				console.log(globalThis.HTMLElement === HTMLElement); // true
				console.log(globalThis.globalThis); // Window
				console.log(this); // undefined
			</script>

		

Creating our own globals


			<script type="module">
				globalThis.logContext = function() {
					console.log(this);
				}
			</script>
			<script type="module">
				logContext(); // undefined
			</script>
		

Context is a dynamic binding


				<script type="module">
					globalThis.logContext = function() {
						console.log(this);
					}
				</script>
				<script type="module">
					logContext(); // undefined
					globalThis.logContext(); // Window
				</script>
			
We could call the same function on different objects, and get different values for `this`. Just like regular arguments get dynamic bindings!

Context in arrow functions


						let person = {
							name: "David",
							hello: () => console.log(this)
						};
						person.hello();
					
				
  1. {name: "David", hello: f}
  2. undefined

Arrow functions don't have their own context, so this just points to the context of whatever scope they're defined in

Context in event listeners


				button.addEventListener("click", function(event) {
					console.log(this);
				});
			
  1. An HTMLButtonElement
  2. undefined
  3. Window
Basically the same as `event.currentTarget` that we saw in the previous lecture

Modifying context temporarily

  • func.call(context, ...args)
  • func.apply(context, args)

					function logContext() {
						console.log(this);
					}

					logContext(); // logs undefined
					logContext.call(document); // logs document
				
> let fakeArray = {0: "a", 1: "b", length: 2}
> fakeArray.push("c")
Uncaught TypeError: fakeArray.push is not a function
> [].push
< ƒ push() { [native code] }
> [].push.call(fakeArray, "c")
< 3
> fakeArray
< {0: 'a', 1: 'b', 2: 'c', 3: 'c', length: 4}
- Note that since functions are first-class objects, they can have methods too, just like every other object! - So why would we want to do that? Let's look at a more realistic example…

Modifying context permanently: func.bind(context, ...args)

Returns a new function whose context is always bound to the first argument


				function logContext() {
					console.log(this);
				}
				let logContext2 = logContext.bind({foo: 1});

				logContext2(); // logs {foo: 1}
				logContext2.call(document); // logs {foo: 1}
			
> let $$ = document.querySelectorAll
> $$("div")
Uncaught TypeError: Illegal invocation
> $$ = document.querySelectorAll.bind(document)
> $$("div")
< NodeList(43) [div, div, div, …]
Why would you want to do that? When you want to pass a function around (e.g. use it as a listener, or as an argument to other functions) and you want to be sure its context will reliably be the one you expect or simply to be able to use it without `.call()` or `.apply()` and still have its context be useful, What type of function is `.bind()`?

Classes

Classes

Classes


				class Person {
					constructor (name, birthday) {
						this.name = name;
						this.born = new Date(birthday);
					}
					getAge () {
						const ms = 365 * 24 * 60 * 60 * 1000;
						return (new Date() - this.born) / ms;
					}
				}
			

				let david = new Person(
					"David Karger",
					"1967-05-01T01:00"
				);
				let lea = new Person(
					"Lea Verou",
					"1986-06-13T13:00"
				);
				console.log(lea.getAge(),
					david.getAge());
			
  • Define properties and methods in class declaration
  • Construct objects using new
  • All created objects inherit from class

Subclassing with Extends


				class PowerArray extends Array {
					constructor(...args) {
						super(...args);
					}

					isEmpty() {
						return this.length === 0;
					}
				}
			

				let arr = new PowerArray(1, 2, 5, 10, 50);
				console.log(arr.isEmpty()); // false
			

				let arr2 = PowerArray.from([1, 2, 3]);
				console.log(arr2.isEmpty()); // false
			
  • Often want to add properties and methods to an existing class
  • Create a new class that extends the old
  • Inherits superclass' methods and properties
  • Including constructor!
  • super is bound to the class you inherit from if you want to access its properties or methods.
  • Note that even static methods are inherited

Encapsulation: Private class features


				class Clicker {
				  #clicks = 0;

				  click () {
				    this.#clicks++
				  }
				  getClicks () {
				    return #clicks;
				  }
				}
			
Private properties and methods start with a pound sign (`#`). Unlike regular (public) properties that can be created at any time, private properties need to be declared in advance. JS only supports two types of property visibility: public and private. This means that subclasses cannot actually read private properties of their superclass.

Custom HTML Elements by extending HTMLElement


				<click-counter></click-counter>
			

				class ClickCounter extends HTMLElement {
					#clicks = 0;

					constructor() {
						super();
						this.#render();
						this.addEventListener('click', this.#clicked)
					}
					#render () {
						this.innerHTML = `Clicked ${this.#clicks}x`;
					}
					#clicked () {
						this.#clicks++;
						this.#render();
					}
				}

				customElements.define('click-counter', ClickCounter);
			
Here we have created a rudimentary custom element that shows how many times it has been clicked. We create custom elements by extending the `HTMLElement` class, and then calling `customElements.define()` to associate the class with a tag name. We have defined methods for updating the element's rendering and handling clicks, but these are private, so the actual API of the component to other developers appears like a regular HTML element.

Accessors

So far we’ve seen…

JS and its various APIs have a number of things that look like properties, but actually execute code when they are read or set. We have also seen plenty of examples in the DOM, e.g. `element.textContent`, `element.innerHTML` etc.

Accessors:
Properties on the outside,
functions on the inside

Accessors in the DOM


				console.log(document.body.innerHTML);
				// 👆🏼 serializes <body>’s contents as an HTML string

				document.body.innerHTML = "<em>Hi</em>";
				// 👆🏼 parses the string provided
				//    and replaces <body>’s subtree with it
			

Defining getters


					let lea = {
						name: "Lea",
						birthday: new Date("1986-06-13T13:00"),
						get age () {
							const ms = 365 * 24 * 60 * 60 * 1000;
							return (new Date() - this.birthday) / ms;
						}
					}

					console.log(lea.age); // 35.81898413454465
			

What happens if we try to set a getter?


					let lea = {
						birthday: new Date("1986-06-13T13:00"),
						get age () {
							const ms = 365 * 24 * 60 * 60 * 1000;
							return (new Date() - this.birthday) / ms;
						}
					}
					lea.age = 30;
					console.log(lea.age);
			
  1. 35.81898413454465
  2. 30
  3. TypeError: Cannot set property age of #<Object> which has only a getter
By default, accessors we define with this syntax

What do you expect the console to log?


					let lea = {
						birthday: new Date("1986-06-13T13:00"),
						get age () {
							const ms = 365 * 24 * 60 * 60 * 1000;
							return (new Date() - this.birthday) / ms;
						}
					}
					lea.birthday = new Date("1992-04-01T13:00");
					console.log(lea.age);
			
  1. 35.81898413454465
  2. 30.01626640588534

Defining setters


					let lea = {
						birthday: new Date("1986-06-13T13:00"),
						set age (a) {
							const ms = 365 * 24 * 60 * 60 * 1000;
							this.birthday = new Date((Date.now() - ms * a));
						},
					}
					lea.birthday = new Date("1990-04-01T13:00");
					lea.age=3;
					console.log(lea.birthday); // 2017
			

API Design Tip When to use accessors?

Developers tend to think of things that look like properties as "free" when eyeballing the performance of their code, while they take functions into account as something that could be more intensive.
Modularity with Modules

Modularity

Why

  • Major idea from computer science
  • Break complicated system into many small components
  • Each gathers what is needed for a particular functionality
  • Minimize interaction between components
  • Lets you focus working memory on relevant component
  • Ignore details of rest of system — abstraction

How

  • Create contexts with distinct namespaces
  • Use same word/name in distinct contexts without confusion
  • Directories in filesystems
  • Scoped local variables
  • Objects
  • Now, modules

JS Modules

syntax

  • <script type="module">
  • export const obj = 'square';
  • export {obj, draw ...}
  • import { obj, draw} from './modules/square.js';
  • import { obj as square} from './modules/square.js';

effect

  • import file as module
  • make obj visible for import
  • export names defined elsewhere in file
  • bring these names into my namespace
  • import obj, but call it square in my namespace

Notes