Skip to content

Week 1

JavaScript fundamentals revisited by Purag Moumdjian

Below are the notes from the discussion section.

The global object

In Node.js, the global object contains functions (and lots of other information) exposed by the runtime environment.

By default, this in the top-level scope of a node module refers to the module itself. When not in strict mode, this inside a function is automatically assigned the global object.

// global object

console.log(this === global); // ??

{
  console.log(this === global); // ??
}

function f () {
  console.log(this === global);
}

f(); // ??

new f(); // ??

(function () {
  "use strict";
  console.log(this === global); // ??
})();

Receivers/the this keyword

We can dynamically change the value of this when executing a function which uses it. All functions in JavaScript have three methods which let us do this: call, apply, and bind.

call and apply immediately invoke the function with the specified receiver (target object) and arguments. bind creates a new function with the receiver (and any provided arguments) bound to the specified values. This lets us create clones of functions where the this keyword always refers to a specific object (see swapObj2 below).

bind also lets us accomplish partial application of functions, meaning we can create copies of functions with some (or all) of the arguments already fixed. We'll revisit this in detail throughout the quarter.

// objects + this

let obj = {
  x: 1,
  y: 2,
  swap: function () {
    let temp = this.x;
    this.x = this.y;
    this.y = temp;
  }
};

console.log(obj.x, obj.y); // ??

obj.swap();

console.log(obj.x, obj.y); // ??

let obj2 = {
  x: 3,
  y: 4
}

console.log(obj2.x, obj2.y); // ??

obj.swap.call(obj2);

console.log(obj2.x, obj2.y); // ??
console.log(obj.x, obj.y); // ??

swapObj2 = obj.swap.bind(obj2);

swapObj2();
console.log(obj2.x, obj2.y); // ??

swapObj2();
console.log(obj2.x, obj2.y); // ??

// Also works with regular functions (not belonging to an object)...
function f () {
  console.log(this.x, this.y);
}

f.call(obj2);
console.log(obj2.x, obj2.y); // ??

// And also works with arguments...
function g (arg1) {
  console.log(this.x, this.y, arg1);
}

g.call(obj2, "argument 1"); // ?

Classes/Function constructors

Before classes were native to JavaScript, we would use plain old functions to construct objects. These are called function constructors. When we invoke any function with the new keyword (i.e. new Car()), this is automatically bound to a newly created object (whose prototype __proto__ is Car.prototype), allowing us to specify instance data attached to this inside a function constructor.

Since functions create a new scope in JavaScript, and since JavaScript functions are closures, we can create "hidden"/private fields and methods just by declaring local variables and functions. These are only visible in the scope of the function, and because functions are closures and capture the environment, these "fields" and "methods" can be accessed even after the object is created.

One caveat is that each instance of the object will have its own copy of every private method, and if you want a public method that uses a private method, each object needs a copy of that, too -- the public method will have to share the scope of the private method.

This is not the way you want to manage private field, it's just an illustration. See this blog post for a discussion.

// classes + function constructors

let Car = function (make, model) {
  this.make = make;
  this.model = model;
};

let car1 = new Car("toyota", "camry");

console.log(car1.make, car1.model); // ??

// We can even make private fields!
Car = function (make, model, year) {
  let private_year = year;

  this.make = make;
  this.model = model;
}

let car2 = new Car("honda", "civic", 2018)

console.log(car2.make, car2.model); // ??
console.log(car2.private_year); // ??
console.log(car2.year); // ??

// Private methods?
Car = function (make, model, year) {
  let private_year = year;

  this.make = make;
  this.model = model;

  function private_getInternalLabel () {
    return `${make}@${model}`;
  }

  this.toString = function () {
    return `${private_getInternalLabel()}@${private_year}`
  };
}

let car3 = new Car("ford", "edge", 2018);

console.log(car3.private_getInternalLabel()); // ??
console.log(car3.toString()); // ??

Hoisting

JavaScript hoists var declaration and function declarations and definitions to the top of the current scope. This behavior may or not be desired, but if you want to avoid it, just use let and const to declare variables, as they are not hoisted -- in other words, they create a new scope only once they executed, and these scopes end as soon as the surrounding block ends.

// hoisting

{
  console.log(undeclared); // ??
}

{
  console.log(let_declared_later); // ??
  let let_declared_later;
}

{
  console.log(var_declared_later); // ??
  var var_declared_later;
}

{
  console.log(defined_later()); // ??
  function defined_later () {
    return "yay!";
  }
}

{
  console.log(let_defined_later()); // ??
  let let_defined_later = function () {
    return "yay!";
  };
}

Functions/Callbacks/Arrow Notation

Below, we have a function constructor for Car, and in the constructor we create methods getMake and getModel which access the arguments of the function. We return the new object to the caller and the function's scope ends, but subsequent calls to getMake and getModel will still have access to the arguments from the function's scope due to closure.

For another example, see Closure

// functions + closures

function f () {
  return 0;
}

console.log(typeof f); // ??
console.log(f instanceof Object); // ??

console.log(f.toString()); // ??

// Back to function constructors
let Car = function (make, model) {
  this.getMake = function () {
    return make;
  };

  this.getModel = function () {
    return model;
  };
}

let car1 = new Car("toyota", "camry");

console.log(car1.getMake(), car1.getModel()); // ??

// Arrow notation: more concise, especially with callbacks
const g = () => {
  return 0;
};

console.log(g()); // ??

setTimeout(() => {
  console.log("done");
}, 10);

// More examples of this in 06.functional.js...

List Manipulation a la Functional Programming in JavaScript

map, reduce (or fold), and filter are three examples of list manipulation functions. Recursion is the bread and butter of functional programming, and these functions are prime examples. JavaScript arrays natively support some of these behaviors, as seen below.

// functional stuff!

const array = [1, 2, 3];

let newArray = array.map((x) => x + 1);
console.log(newArray); // ??

newArray = array.reduce((accum, x) => accum + x, 0);
console.log(newArray); // ??

newArray = array.filter((x) => x <= 2);
console.log(newArray); // ??

newArray = array.filter((x) => x > 2);
console.log(newArray); // ??

Closure

Below are some classical examples of closures.

// Closure example

// #1
{
  var funcs = [];
  for (var i = 0; i < 10; i++) {
    funcs.push(function () {
      console.log(i);
    });
  }
  funcs.forEach(function (func) {
    func();
  });
}

// #2
{
  let i = 5;

  const f = () => console.log(i);

  f(); // ??

  i = 10;

  f(); // ??
}

// #3
// How do we fix the problem in #1?
{
  let funcs = [];
  for (let i = 0; i < 10; i++) {
    funcs.push(() => console.log(i));
  }
  funcs.forEach((func) => func());
}

// #4
// How can we fix it without using let? (Also, how did we fix this before let?)
{
  var funcs = [];
  for (var i = 0; i < 10; i++) {
    funcs.push((function (j) {
      return function () {
        console.log(j);
      }
    })(i));
  }
  funcs.forEach(function (func) {
    func();
  });

  // or...
  funcs = [];
  for (var i = 0; i < 10; i++) {
    funcs.push(function (j) {
      console.log(j);
    }.bind(null, i));
  }
  funcs.forEach(function (func) {
    func();
  });
}