When JS runs code, it:
Code we save ('define') functions & can use (call/invoke/execute/run) later with the function's name & ().
Created to run the code of a function - has 2 parts:
There is a global execution context, while other functions have an execution context.
Threads of execution - you only have one that performs work.
The bottom of the call stack is always the global execution context.
As functions are invoked, they are put onto the call stack and evaluated.
The example used is a function copyArrayAndDivideBy2:
// not flexible function copyArrayAndDivideBy2(array) { const output = []; for (let i = 0; i < array.length; i++) { output.push(array[i] / 2); } return output; } // the example used function copyArrayAndManipulate(array, instructions) { const output = []; for (let i = 0; i < array.length; i++) { output.push(instructions(i)); } return output; } function multiplyBy2(input) { return input * 2; } const result = copyArrayAndManipulate([1, 2, 3], multipleBy2); // my personal curried preference function copyArrayAndManipulate(instructions) { return function(array) { const output = []; for (let i = 0; i < array.length; i++) { output.push(instructions(i)); } return output; }; }
Note that in relation to memory, the local memory generated from
copyArrayAndManipulatehas a reference to the value ofinstructions(n this case,multiplyBy2).
In the above example, copyArrayAndManipulate is the higher-order function. multiplyBy2 is the callback function.
syntactic sugar - we'll see their full effects later."Readability comes before insignificant, marginal performance gains."
Here is a link
"If you understand closures in JS, you understand JS."
Functions with memories:
The above all starts with returns a function from another function.
function createFunction() { function multiplyBy2(num) { return num * 2 } return multiplyBy2 } const genFunc = createFunction() const result = generatedFunc)(3)
It is imperative that you understand the data store and execution contexts that come from the function. Remember: after a function runs, all the local memory is erased except for the returned value from that function's execution context.
With closures, the local context that it was called in comes back with it when the closure is returned. A "backpack" of live data. This data can be updated and given back to the global context after the local context of the closure is run every time.
The other benefit is the "privisation" of variables in the closure scope that you cannot access directly.
During the Q&A, there is a short answer on decoration on using closures with functions that have access to the local data context.
So what is the name of this "backpack"? The variable environment. Another name is COVE - closed over variable environment. The last acronym used in this talk is PLSRD: Persistent lexically or static scope reference data. We can also simply call it the closure.
There is a link to a great artcile on closures
JavaScript is a lexically (or static) scoped langauge.
Closures give a new toolkit for writing professional code.
The closure exercises are here
The core JS engine has 3 parts:
But we need more:
Understanding the browser features are imperative to understanding the asynchronous web queues and execution context.
The behaviours that happen due to this asynchronousity requires interaction with a world outside of JS now.
The example here questions what happens on setTimeout(() => console.log('hello'), 0).
// what happems? function printHello() { console.log('hello'); } function blockForOneSec() { // block for one sec logic } setTimeout(printHello, 0); blockerForOneSec(); // blocks thread console.log('me first'); // this still runs before printHello due to the rules of the callback queue
It explains how after the timeout has run, the callback function is first put onto the "callback queue". The event loop after blockForOneSec still runs the console.log function as the callback queue priority is put to the back of the event queue.
You could have 1m
console.logcalls, and they would all run before anything runs from the callback queue. Until ES6, this was everything for async JS.
Note the
event loopruns the entire time while the thread is still blocked to check and know if the blocking timer as finished.
Some of the problems with the callback queue:
There is an immediate consqueunce when the background work is one.
The ES6 solution to priority: Promises. Using two-pronged "facade" functions that both:
function display(data) { console.log(data); } // one consequence: immediately pulls out special Promise object with a value and an onFufilled property (empty array). // the second: Web browser API createds a network request const futureData = fetch(url); futureData.then(display); console.log('me first!');
When the data comes back from the response, it will go into the Promise object value property.
The empty array can have an functions triggered to run when the data comes back in to the value property. The data back is put in as the parameter for that function being run from the onFulfillment array that is filled from the then keyword.
There is also a
onRejectionfunction array to handle errors.
Here is the example given for the call order once the function comes back:
function display(data) { console.log(data); } function printHello() { console.log('Hello'); } function blockForOneSec() { // block for one sec logic } setTimeout(printHello, 0); // runs last as the call comes from the callback queue const futureData = fetch(url); futureData.then(display); // display takes priorty over callbacks due to the microtask queue blockForOneSec(); console.log('me first!'); // still runs first
Once the sychronous code runs in an execution context, the event loop checks the
microtaskqueue and call those tasks before thecallback queue.
Another interesting concept was "starving the callback queue" - if you keep filling the microtask queue, you can endlessly delay the callback queue from running.
Hold promise-deferred functions in the microtask queue and callback function in the task queue (callback queue). When the Web Browser Feature (API) finishes, add the function to the call stack (ie run the func) when the call stack is empty & all global code run (have the event loop check this condition).
Prioritize functons in the microtask queue over the Callback queue.
__proto__ and prototype.new and class keywords such as tools to automate our object & method creation.This is the principle of encapsulation.
const user1 = { name: 'Will', score: 3, increment: function() { user1.score++; }, }; user1.increment();
You could also use Object.create(null) then assign properties to the object if it is assigned to a variable.
Factory functions are used to build out objects.
function userFactory(name, score) { const newUser = {}; newUser.name = name; newUser.score = score; newUser.increment = function() { newUser.score++; }; return newUser; } const user1 = userFactory('Jill', 0); const user2 = userFactory('Jack', 2); user1.increment();
The factory follows the previous knowledge that we understand about closures, the variable environment and returns from the local execution context.
Is the better way than putting a copy of functions onto every single object?
We can use the Prototype chain. We have the interpreter - if it doesn't find the function on the object (in this case, the user) - look up that protoype object to see if it is there.
function userFactory(name, score) { const newUser = Object.create(userFunctionStore); newUser.name = name; newUser.score = score; return newUser; } const userFunctionStore = { increment: function() { newUser.score++; }, login: function() { console.log('Logged in'); }, }; const user1 = userFactory('Jill', 0); const user2 = userFactory('Jack', 2); user1.increment();
Under the hood, there is a hidden property on the objects we've been creating __proto__ and it has a link to userFunctionStore.
The argument for
Object.createis always the__proto__property.
We can use hasOwnProperty which comes from the head honcho, the Object prototype that all objects link to at the top of the prototype chain.
If we use an exammple of user1.hasOwnProperty('store'), it will move up the prototype chain looking for that value.
The parent Object type at the top of the chain has property
__proto__which will have anullvalue.
The default value for this is from the global namespace.
There is the historical reference for the
that = thisassignment that objects used to use to keep context.
If you use .call (or .apply) with this as the argument, it would keep the scope as well.
This can now be avoided with the this assignment being lexically scope by ES6's arrow functions.
Lexically scoped = it is from the scope of where this was defined.
The new keyword can be used to automate the factory function.
userFactory.prototype.increment = function() { this.score++; }; const user1 = new userFactory('Jack', 2);
This automatically sets the __proto__ value and an empty object at this as well. The __proto__ points to a prototype object of userFactory in the above example.
This section introduced the idiomatic way to define when the new keyword needs to be used to instantiate an object. In addition, the syntactic sugar of the class keyword is discussed, and how it really is no different under the hood than what was demonstrated in previous sections.
Developers use the convention of the capital letter to help developers know that the new keyword should be used for a factory function.
class UserCreator { // Syntactic sugar for the factory // function. constructor(name, score) { this.name = name; this.score = score; } // These methods are syntactic sugar for the // prototype ie this transpiles to // UserCreator.prototype.increment. increment() { this.score++; } login() { console.log('login'); } } const user1 = new UserCreator('Eva', 0); user1.increment();