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(); }); }