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