Week 1

Lecture slides and code

  • Introduction: pdf, key
  • JavaScript in a Nutshell: pdf, key
  • Discussion section notes: here

If you have not worked with JavaScript before, take some time to familiarize yourself with the basics of the language, as most of the labs in this class will be in JavaScript.

We recommend the Mozilla Developer Network's JavaScript Guide, but there are many other tutorials on the Internet.

Dave Herman's Effective JavaScript is very good reference that covers a lot of the JavaScript intricacies. You will not need this for the class, but if you end up writing JavaScript code in the outside world, this book is must-read.

Additional resources/reading for the curious

Source code used in class

Below you'll find the source files we used during lecture. You can run these with Node.js.

Scoping

Block scoping in modern JS:

function hello(x) {
  console.log(`A: x = ${x}`); // 42
  {
    let x = 45;
    console.log(`B: x = ${x}`); // 45
  }
  {
    console.log(`C: x = ${x}`); // 42
  }
}

hello(42);

Function (but not block) scoping for vars:

function hello(x) {
  console.log(`A: x = ${x}`); // ??
  {
    var x = 45;
    console.log(`B: x = ${x}`); // ??
  }
  {
    console.log(`C: x = ${x}`); // ??
  }
}

hello(42);

Mimicking block scoping with functions:

function hello(x) {
  console.log(`A: x = ${x}`); // ??
  (function () {
    var x = 45;
    console.log(`B: x = ${x}`); // ??
  })();
  (function () {
    console.log(`C: x = ${x}`); // ??
  })();
}

hello(42);

Now, with arrow functions:

function hello(x) {
  console.log(`A: x = ${x}`); // ??
  (function () {
    var x = 45;
    console.log(`B: x = ${x}`); // ??
  })();
  (() => {
    console.log(`C: x = ${x}`); // ??
  })();
}

hello(42);

Performance

Without high-order functions, we'd perform reads and write synchronously:

const fs = require('fs');

const r1 = fs.readFileSync('./perf-sync.js', 'utf8'); // blocks until read is done
processFile('perf-sync.js', r1); // blocks until processing (write) is done
const r2 = fs.readFileSync('./perf-async.js', 'utf8'); // etc.
processFile('perf-async.js', r2);

// note that you can declare a function after the point it's used. Hoisting
// essentially moves it to the top.
function processFile(fname, str) {
  fs.writeFileSync(`/tmp/${fname}`, str);
  console.log(`DONE writing /tmp/${fname}`);
}

Passing (callback) functions as arguments allows the runtime system to call our function whenever it's ready. This allows it to perform IO concurrently and more efficiently:

const fs = require('fs');

fs.readFile('./perf-sync.js', 'utf8', cb1); // returns immediately, cb1 is queued on the event loop and called later when actual file read is done
fs.readFile('./perf-async.js', 'utf8', cb2); // returns immediately, " "

function processFile(fname, str) {
  fs.writeFileSync(`/tmp/${fname}`, str);
  console.log(`DONE writing /tmp/${fname}`);
}

function cb1(err, str) {
  // line cb1.1
  processFile('perf-sync.js', str);
}

function cb2(err, str) {
  //line cb2.1
  processFile('perf-async.js', str);
}

Can cb2 execute before cb1?
A: yes, B: no

Once we can return functions we can also express our code more compactly too:

const fs = require('fs');

fs.readFile('./perf-sync.js', 'utf8', processFile('perf-sync.js'));
fs.readFile('./perf-async.js', 'utf8', processFile('perf-async.js'));

function processFile(fname) {
  return (err, str) => {
    fs.writeFileSync(`/tmp/${fname}`, str);
    console.log(`DONE writing /tmp/${fname}`);
  };
}

And, slightly cleaner:

const fs = require('fs');

readAndProcessFile('perf-sync.js');
readAndProcessFile('perf-async.js');

function readAndProcessFile(name) {
  return fs.readFile(`./${name}`, 'utf8', processFile(name));
}

function processFile(fname) {
  return (err, str) => {
    fs.writeFileSync(`/tmp/${fname}`, str);
    console.log(`DONE writing /tmp/${fname}`);
  };
}

Expressiveness

High-order functions enables expressiveness:

const list = [1, 2, 3, 4];

console.log(filter(list, function (el) { 
  return el > 2;
})); // ??

console.log(map(list, el => { 
  return el + 42;
})); // ??


function filter(list, pred) {
  const dup = [];
  for (let i = 0; i < list.length; i++) {
    if (pred(list[i])) {
      dup.push(list[i]);
    }
  }
  return dup;
}

function map(list, f) {
  const dup = [];
  for (let i = list.length-1; i >= 0; i--) {
    dup.unshift(f(list[i]));
  }
  return dup;
}

It also can enable more efficient code:

const list = [1, 2, 3, 4];

const add42 = (el) => {
  return el + 42;
};

function mul1337 (el) {
  return el * 1337;
}

console.log(map(map(list, add42), mul1337));
console.log(map(list, compose(mul1337, add42)));

function compose (f, g) {
  return (x) => { 
    return f(g(x));
  }
}

function map(list, f) {
  const dup = [];
  for (let i = list.length-1; i >= 0; i--) {
    dup.unshift(f(list[i]));
  }
  return dup;
}

Abstraction

We can also use functions to implement module systems.

Consider a simple module in Node.js:

const secret = "cse130 is fun!"; // scoped to this function, hidden to outside world
exports.myVar = 42;
exports.myFunc = function (x) {
  if (x === secret) {
    console.log('yes!');
  } else {
    console.log('guess again!');
  }
};

This module can be loaded with require, which is (very) roughly implemented as follows:

// using node's requie:
{
  const mod = require('./module-node.js');

  console.log(mod.myVar); // ??
  mod.myFunc("what?"); // ??
  mod.myFunc("cse130 is fun!"); // ??
}

// using our fake require:
{
  const mod = requireMyModule();

  console.log(mod.myVar); // ??
  mod.myFunc("what?"); // ??
  mod.myFunc("cse130 is fun!"); // ??
}

function myModule(exports) {
  // same code as module-node.js:
  const secret = "cse130 is fun!"; // scoped to this function, hidden to outside world
  exports.myVar = 42;
  exports.myFunc = function (x) {
    if (x === secret) {
      console.log('yes!');
    } else {
      console.log('guess again!');
    }
  };
}

function requireMyModule() {
  // create new object that will be populated by the module
  const exports = {};
  myModule(exports);
  return exports;
}

Objects

We'll be looking at objects later in the class. Objects can be expressed ad-hoc, using object literal notation:

const obj = {
  "x-w00t": 10,
  x: 1337,
  f: function (y) {
    this.x++;
    return this.x + y;
  }
};

console.log(obj.x); // ??
console.log(obj.f(3)); // ??
console.log(obj["x"]); // ??
console.log(obj["x-w00t"]) // ??

But we can (again) use functions to construct objects:

function Car(make, model) {
  this.make = make;
  this.model = model;
  this.toString = function () {
    return `${this.make}@${this.model}`;
  };
}
Car.mySweetProp = 42;

const f = new Car("Ford", "Focus");
console.log(f.toString());
const t = new Car("Toyota", "Corola");
console.log(t.toString());

// Car.prototype is shared by all objects created by calling new Car(...)
// That's right you can treat functions like objects!

console.log(f.__proto__ === Car.prototype); // ??

// Let's define property common to all cars:
Car.prototype.color = "black";

console.log(f.color); // ??
// getProperty "color" of f
//     if it has it, return it
//     else getProperty "color" of f.__proto__
console.log(t.color); // ??

// Can override the default color that is defined on the prototype:

t.color = "red";

console.log(t.color); // ??
console.log(f.color); // ??

// We can define a method on the prototype:

Car.prototype.toColorString = function () {
  return `${this.make}, ${this.model}, ${this.color}`;
};

console.log(f.toColorString()); // ??
console.log(t.toColorString()); // ??

More recently, however, JavaScript adopted classes. You can think of them as being syntactic sugar for the above:

class Car {
  constructor(make, model) {
    this.make = make;
    this.model = model;
  }
  toString() {
    return `${this.make}@${this.model}`;
  }
  static get mySweetProp() {
    return 42;
  }
}

const f = new Car("Ford", "Focus");
console.log(f.toString());
const t = new Car("Toyota", "Corola");
console.log(t.toString());

// Car.prototype is shared by all objects created by calling new Car(...)
// That's right you can treat functions like objects!

console.log(f.__proto__ === Car.prototype); // ??

// We can define property common to all cars as before:
Car.prototype.color = "black";

console.log(f.color); // ??
// getProperty "color" of f
//     if it has it, return it
//     else getProperty "color" of f.__proto__
console.log(t.color); // ??

// Can override the default color that is defined on the prototype:

t.color = "red";

console.log(t.color); // ??
console.log(f.color); // ??

// We can define a method on the prototype as before:

Car.prototype.toColorString = function () {
  return `${this.make}, ${this.model}, ${this.color}`;
};

console.log(f.toColorString()); // ??
console.log(t.toColorString()); // ??