// Your code here console.log(new Point(1, 2).plus(new Point(2, 1))) // → Point{x: 3, y: 3}
Write a class called Point
, which represents a
point in two-dimensional space. A point has x
and y
properties, given as arguments to its
constructor.
It also has a single method plus
, which takes
another point and returns the sum of the two points, that is, a
new point whose x
is the sum of the x
properties of the two original points, and whose y
is
the sum of their y
properties.
function Speaker(name, verb) { this.name = name this.verb = verb || "says" } Speaker.prototype.speak = function(text) { console.log(this.name + " " + this.verb + " '" + text + "'") } function Shouter(name) { Speaker.call(this, name, "shouts") } Shouter.prototype = Object.create(Speaker.prototype) Shouter.prototype.speak = function(text) { Speaker.prototype.speak.call(this, text.toUpperCase()) } new Shouter("Dr. Loudmouth").speak("hello there")
Rewrite these two object types to use the class
keyword, instead of direct prototype
manipulation. Speaker
is a simple type that exposes
a speak
method which, when called, logs the given
text along with the speaker's name. Shouter
is a
subtype of Speaker
which shouts its text and makes it
uppercase.
class Speaker { constructor(name, verb) { this.name = name this.verb = verb || "says" } speak(text) { console.log(this.name + " " + this.verb + " '" + text + "'") } } class Shouter extends Speaker { constructor(name) { super(name, "shouts") } speak(text) { super.speak(text.toUpperCase()) } } new Shouter("Dr. Loudmouth").speak("hello there")
This is the way the solution to the previous exercise might look.
The way the verb
property is set per
instance rather than per class is kind of awkward. Refactor the
code to use a getter (get verb() { ...
}
) instead of an instance property.
class Point { constructor(x, y) { this.x = x, this.y = y } add(other) { return new Point(this.x + other.x, this.y + other.y) } } var fakePoint = YOUR_CODE_HERE console.log(fakePoint instanceof Point)
Use a single object literal to create an object that is
indistinguishable from a Point
instance, without
calling the Point
constructor.
function startNode(type, value, options) { return YOUR_CODE_HERE } console.log(startNode("Identifier", "foo", { sourceProperty: "src", sourceValue: "bar.js" })) // → {type: "Identifier", // value: "foo", // src: "bar.js"}
Fill in the startNode
function using a single
object literal. The function should return an object
with type
and value
properties
containing the value of the arguments by those names, and a third
property, named by the sourceProperty
option, set to
the value of the sourceValue
option.
var ids = { next: 0 } console.log(ids.get()) // → 0 console.log(ids.get()) // → 1
Add a get
method to the ids
object,
which returns the next id and increments its next
counter. Use the short method syntax.
var callbacks = [] for (var i = 0; i < 10; i++) { callbacks.push(function() { console.log(i) }) } callbacks[2]()
This is a typical mistake to make in JavaScript. We create a
number of functions in a loop, and refer to an outside variable
from these functions. All of them will end up referring to the
same variable, which ends up being incremented to 10
.
Thus, callbacks[2]
does not log 2
. It
logs 10
, as do all functions in the array.
Do you know the solution for such situations in ES5? Does ES6 provide a cleaner solution?
const account = { username: "marijn", password: "xyzzy" } account.password = "s3cret" // (*much* more secure) console.log(account.password)
Does the fact that account
is constant mean that
we can't update password
? Why, or why not? And if
not, how could we make it so that we can't?
let s = new Something() class Something {}
This code produces an error. So apparently, unlike functions, classes are not hoisted to the top of their scope. (They are block-scoped.)
What could be the reason for this?
const inventory = [ {type: "machine", value: 5000}, {type: "machine", value: 650}, {type: "duck", value: 10}, {type: "furniture", value: 1200}, {type: "machine", value: 77} ] let totalMachineValue = YOUR_CODE_HERE console.log(totalMachineValue)
Write an expression using higher-order array methods
(say, filter
and reduce
) to compute the
total value of the machines in the inventory
array.
function SortedArray(compare) { this.compare = compare this.content = [] } SortedArray.prototype.findPos = function(elt) { for (var i = 0; i < this.content.length; i++) { if (this.compare(elt, this.content[i]) < 0) break } return i } SortedArray.prototype.insert = function(elt) { this.content.splice(this.findPos(elt), 0, elt) } var sorted = new SortedArray(function(a, b) { return a - b }) sorted.insert(5) sorted.insert(1) sorted.insert(2) sorted.insert(4) sorted.insert(3) console.log("array:", sorted.content)
The code for this exercise implements a wrapper for working
with sorted arrays. Its constructor takes a comparison function
that compares two elements and returns a number, negative if the
first is less than the second, zero when they are equal, and
positive otherwise (similar to what the sort
method
on arrays expects).
Rewrite the code to use an ES6 class. Then, rewrite the loop to
use the ES6 array method findIndex
, which is
like indexOf
, but takes a function instead of an
element as argument, and returns the index of the first element
for which that function returns true (or returns -1
if no such element was found). For example [1, 2,
3].findIndex(x => x > 1)
yields 1
. Use arrow
functions for all function expressions.
console.log([1, 2, 3].reduce((x, y) => x + y, 0))
Where does an arrow function end? At a closing bracket or
semicolon, of course. But does a comma denote the end? Is the body
of the function in this example x + y, 0
, or
just x + y
?
Is there anything else that will end an arrow function body? Experiment.
function go(options) { let {speed = 4, enable: {hyperdrive = false, frobnifier = true}} = options console.log("speed=", speed, "hyperdrive:", hyperdrive, "frobnifier:", frobnifier) } go({speed: 5})
This function uses destructuring for argument parsing. But it
has a problem: it crashes when the caller passes an option object
without an enable
property. Since all options have
defaults, we'd like to not crash in this case. Can you think of a
clean way to fix this problem?
If you also want to allow not passing in an option object at all, how would you solve that?
function lastIndexOf(arr, elt, start) { for (let i = start - 1; i >= 0; i--) if (arr[i] === elt) return i return -1 } console.log(lastIndexOf([1, 2, 1, 2], 2))
It would be nice to be able to call our `lastIndexOf` function without providing the third argument, and have it default to starting at the end of the array. It would be even nicer if we could express this with an ES6 default argument value. Can we?
In which scope are default arguments evaluated? (Experiment to find out.)
function detectCollision(objects, point) { for (let i = 0; i < objects.length; i++) { let object = objects[i] if (point.x >= object.x && point.x <= object.x + object.width && point.y >= object.y && point.y <= object.y + object.height) return object } } const myObjects = [ {x: 10, y: 20, width: 30, height: 30}, {x: -40, y: 20, width: 30, height: 30}, {x: 0, y: 0, width: 10, height: 5} ] console.log(detectCollision(myObjects, {x: 4, y: 2}))
The detectCollision
function searches through an
array of rectangles and returns the first rectangle that the given
point is inside of.
Use destructuring and a higher-order function to make this code
cleaner. You might want to use the new array
method find
, which takes a function as argument, and
returns the first element in the array (the element, not its
index) for which the function returns true.
function replace(array, from, to, elements) { array.splice.apply(array, [from, to - from].concat(elements)) } let testArray = [1, 2, 100, 100, 6] replace(testArray, 2, 4, [3, 4, 5]) console.log(testArray) function copyReplace(array, from, to, elements) { return array.slice(0, from).concat(elements).concat(array.slice(to)) } console.log(copyReplace([1, 2, 100, 200, 6], 2, 4, [3, 4, 5])) let birdsSeen = [] function recordBirds(time) { birdsSeen.push({time, birds: Array.prototype.slice.call(arguments, 1)}) } recordBirds(new Date, "sparrow", "robin", "pterodactyl") console.log(birdsSeen)
Simplify these three functions by using the spread syntax.
The first one, replace
, replaces part of an array
with elements from another array.
The second one, copyReplace
, does the same, but
creates a new array rather than modifying its argument.
The third one is used to record a birdwatcher's sightings. It
takes first a timestamp (say, a Date
), and then any
number of strings naming birds. It then stores these in
the birdsSeen
array.
const teamName = "tooling" const people = [{name: "Jennie", role: "senior"}, {name: "Ronald", role: "junior"}, {name: "Martin", role: "senior"}, {name: "Anneli", role: "junior"}] let message = YOUR_CODE_HERE console.log(message)
Given the data in the starting code, use a template string to produce the following string. Make sure the numbers, names, and team name actually come from the data.
There are 4 people on the tooling team.
Their names are Jennie, Ronald, Martin, Anneli.
2 of them have a senior role.
function html(...) { // Your code here } const mustEscape = '<>&"' console.log(html`You should escape the ${mustEscape.length} characters “${mustEscape}” in HTML
`) function escapeHTML(string) { return String(string).replace(/"/g, """).replace(/</g, "<") .replace(/>/g, ">").replace(/&/g, "&") }
Write a function html
that can be used as a
template string tag, and produces a string in which all the
interpolated pieces are escaped as HTML. Use the
supplied escapeHTML
function to do the escaping.
Remember that a tag function gets an array of in-between strings as its first argument, and then the interpolated values as further arguments.
// Generate a random graph const graph = [] for (let i = 0; i < 50; i++) graph.push({value: Math.random(), edges: []}) for (let i = 0; i < 100; i++) { let from = graph[Math.floor(Math.random() * graph.length)] let to = graph[Math.floor(Math.random() * graph.length)] if (from.edges.indexOf(to) != -1) continue from.edges.push(to) } function connectedValue(node) { // Your code here } console.log(connectedValue(graph[0])) console.log(connectedValue(graph[24])) console.log(connectedValue(graph[49]))
The code for this exercise generates a random graph—an array of nodes, each of which has a value and an array of edges to other nodes.
Your task is to implement the
function connectedValue
which, starting at a node,
traces through the graph by following edges, summing up the total
value of all nodes connected to the start node (including the
start node itself).
The recursive exploring is the tricky part. You can use either
a worklist (an array of nodes that have to be explored) or a
recursive function. Use a Set
object to track the
nodes that you've already visited (to avoid going around in
circles endlessly when the graph contains a cycle).
The Set
methods you'll need for that
are .add(value)
and .has(value)
.
class MyMap { // Your code here } const names = new MyMap names.set(Array, "the array constructor") names.set(Math, "the math object") console.log(names.get(Array)) // → "the array constructor" console.log(names.size) // → 2 names.delete(Array) console.log(names.get(Array)) // → undefined names.clear() console.log(names.get(Math)) // → undefined
Implement your own version of Map
, without
actually using Map
. I know I said this did not exist,
but the catch is that you don't have to worry about algorithmic
complexity—looking up a value in a big map can be as slow as it
needs to be.
Implement at least these methods/getters:
set(key, value)
get(key)
delete(key)
get size()
clear()
function get(url) { return new Promise((succeed, fail) => { var req = new XMLHttpRequest() req.open("GET", url, true) req.addEventListener("load", () => { if (req.status < 400) succeed(req.responseText) else fail(new Error("Request failed: " + req.statusText)) }) req.addEventListener("error", () => { fail(new Error("Network error")) }) req.send(null) }) }
There is one page on the domain http://marijnhaverbeke.nl that contains the word “Piranha”. It is linked from the domain's top page.
Given this Promise-returning implementation of an HTTP get request, write a simple promise-based “crawler” that first fetches the top page, then uses some crude regexp technique to find linked URLs in that page, and fetches all linked local pages, in parallel. It scans the content of those pages for the word “Piranha”, and logs the address of any matching pages to the console.
function all(promises) { return new Promise(function(success, fail) { // Your code here. }); } // Test code. all([]).then(function(array) { console.log("This should be []:", array); }); function soon(val) { return new Promise(function(success) { setTimeout(function() { success(val); }, Math.random() * 500); }); } all([soon(1), soon(2), soon(3)]).then(function(array) { console.log("This should be [1, 2, 3]:", array); }); function fail() { return new Promise(function(success, fail) { fail(new Error("boom")); }); } all([soon(1), fail(), soon(3)]).then(function(array) { console.log("We should not get here"); }, function(error) { if (error.message != "boom") console.log("Unexpected failure:", error); });
The Promise
constructor has a
method all
that, given an array of promises, returns
a promise that waits for all of the promises in the array to
finish. It then succeeds, yielding an array of result values. If
any of the promises in the array fail, the promise returned by
all
fails too (with the failure value from the
failing promise).
Implement something like this yourself as a regular function
called all
.
Note that after a promise is resolved (has succeeded or failed), it can't succeed or fail again, and further calls to the functions that resolve it are ignored. This can simplify the way you handle failure of your promise.
// Your code here let roundedAbs = Math.round[compose](Math.abs) console.log(roundedAbs(-5.5)) // → 6
Define a symbol compose
, and add it
to Function.prototype
, with a method that composes
the this
function with the method's argument,
returning a new function that applies both composed functions, one
after the other, to its argument, and returns the result.
class Queue { constructor() { this._content = [] } put(elt) { return this._content.push(elt) } take() { return this._content.shift() } } let q = new Queue q.put(1) q.put(2) console.log(q.take()) console.log(q.take())
Change the Queue
class to use a symbol instead of
an underscore-prefixed property name for the private content
property.
class List { constructor(head, tail) { this.head = head this.tail = tail } map(f) { return new List(f(this.head), this.tail && this.tail.map(f)) } } let list = new List("x", new List("y", new List("z", null))) for (let elt of list) console.log(elt) // → x // y // z
Make the List
class (a linked list implementation)
iterable by adding a [Symbol.iterator]
method.
(Remember: an iterator is an object with a next
method that returns {value, done}
objects.)
function integers() { let i = 0 return {next() { return {value: i++} }, [Symbol.iterator]() { return this }} } function take(n, iter) { return { next() { // Your code here }, [Symbol.iterator]() { return this } } } for (let elt of take(3, integers())) console.log(elt) // → 0 // 1 // 2
The integers
function creates an infinite
iterator, that keeps on producing integers forever.
Implement the take
function, which wraps an
iterator in another iterator, but cutting it off
after n
elements.
class List { constructor(head, tail) { this.head = head this.tail = tail } map(f) { return new List(f(this.head), this.tail && this.tail.map(f)) } } console.log(List.from([3, 2, 1])) // → List{head: 3, tail: List{head: 2, tail: List{head: 1, tail: null}}}
Here is our List
class again. In
ES6, Array
has a static method from
,
which does the thing that you'd now
use Array.prototype.slice.call
for—turn an array-like
object (like arguments
or node.childNodes
) into a real array. If its
argument it iterable, it'll take the values from the iterator, and
create an array from that. If not, it'll look for
a length
property, and use integer indexes to fetch
element values.
Add a static from
method to the List
class, which behaves the same way: It takes a single argument. If
that argument is iterable (has a Symbol.iterator
property), it iterates of it, and creates a list from its content.
If not, it loops over the object's indices
(using .length
), and builds a list from its
content.
Note that lists are easiest to build from back to front. You might want to first convert an iterable to an array to make building your list easier. Alternatively, use mutation to build the list front-to-back.
class List { constructor(head, tail) { this.head = head this.tail = tail } map(f) { return new List(f(this.head), this.tail && this.tail.map(f)) } } let list = new List("x", new List("y", new List("z", null))) for (let elt of list) console.log(elt) // → x // y // z
Solve exercise Iterators.1 again, making
the List
class iterable by adding
a [Symbol.iterator]
method. But this time, use a
generator function.
(Remember: the syntax for declaring
a Symbol.iterator
generator method in a class
is *[Symbol.iterator]()
.)
function* integers() { for (let i = 0;; i++) yield i } function* take(n, iter) { // Your code here } for (let elt of take(3, integers())) console.log(elt) // → 0 // 1 // 2
Redo exercise Iterators.2 using a generator.
drive(function*() { // Your code here }) function drive(generator) { let iterator = generator() function resume(result) { if (result.done) return result.value.then( value => resume(iterator.next(value)), error => resume(iterator.throw(error))) } resume(iterator.next()) } function get(url) { return new Promise((succeed, fail) => { var req = new XMLHttpRequest() req.open("GET", url, true) req.addEventListener("load", () => { if (req.status < 400) succeed(req.responseText) else fail(new Error("Request failed: " + req.statusText)) }) req.addEventListener("error", () => { fail(new Error("Network error")) }) req.send(null) }) }
In exercise Promises.1, we wrote a simple web crawler
using promises (get the content of http://marijnhaverbeke.nl,
fetch the content of all local links, search for the word
“Piranha”). Do this again, using the drive
function
from the slides and a generator function. Don't worry about making
the requests in parallel this time.