From: Rubén Beltran del Río Date: Tue, 6 Jul 2021 08:29:27 +0000 (+0000) Subject: Add let's talk about Javascript X-Git-Url: https://git.r.bdr.sh/rbdr/txt/commitdiff_plain/9739ae469545d22e1f5343c020acd843117d2bfc?hp=6bb5ca5d1badc89ac55cfd2fffa435ff57ee4a6c Add let's talk about Javascript --- diff --git a/courses/lets_talk_about_javascript.md b/courses/lets_talk_about_javascript.md new file mode 100644 index 0000000..ce47845 --- /dev/null +++ b/courses/lets_talk_about_javascript.md @@ -0,0 +1,464 @@ +# Let’s talk about Javascript. + +Now, you often hear Javascript is an “Object Oriented” language. And this is true, but not in the same sense as languages like PHP or Ruby are. It’s more accurate to say that javascript uses “Prototype Inheritance”. + +## Let’s talk about prototypes + +Let’s start with the humble object `{}`. An object is a collection of keys that point to a value. For example: `{isCool: true}`. This is what in other languages you might call a dictionary or a hash. Pretty much everything in the language is an object: + +1. Functions? Objects +2. Arrays? Objects +3. Strings? You better believe it they’re objects. +4. Then there’s object which are also objects. + +Objects in javascript have a very special and useful property called `__proto__`. This is known as the “prototype”, and to better understand what it does, let’s look at the following question: + + *What happens when you try to retrieve a property?* + + 1. Check if the property exists in the object. + 1. If it does, return that value + 2. If it doesn’t, check if the property exists in the `__proto__` + 1. If it does, return that value + 2. If it doesn’t, check if the property exists in the `__proto__` + 1. Continue this until the prototype is null + +Cool. Simple enough. *Let’s build some objects then!* + +``` +const coolPrototype = {isCool: true, expressCoolness: function () { console.log("Am I cool? You know that's " + this.isCool) }}; +const aCoolObject = {__proto__: coolPrototype}; +const aLonelyObject = {}; +const anUncoolObject = {__proto__: coolPrototype, isCool: false}; +``` + +You might have noticed the use of `this` in the `expressCoolness` . Put a pin in that, we’ll come back to it later. + +What’s the value of each of these statements: + +``` +aCoolObject.isCool; +aLonelyObject.isCool; +anUncoolObject.isCool; +``` + +What’s about these? Why? + +``` +aCoolObject.expressCoolness(); +aLonelyObject.expressCoolness(); +anUncoolObject.expressCoolness(); +``` + + +What would happen if we do the following? why? + +``` +aLonelyObject.__proto__ = coolPrototype; +aLonelyObject.isCool; +aLonelyObject.expressCoolness(); +``` + + +We could say that “aCoolObject” inherits from “coolPrototype”. An equivalent statement would be that “coolPrototype” is in “aCoolObject’s” prototype chain. + +There’s an easy way to create objects from another objects! It’s called `Object.create`. Imagine we want to create many cool objects, we could do something like this: + +``` +const aNewCoolObject = Object.create(coolPrototype); +aNewCoolObject.__proto__ === coolPrototype; // true! +aNewCoolObject.isCool; // true! +``` + +Of course, when creating objects we might want to customise their behaviour, so we might end up with something like this. +``` +const createCoolObject = function (isCool) { + const newObject = Object.create(coolPrototype); + newObject.isCool = isCool; + return newObject; +}; + +const aCustomCoolObject = createCoolObject(false); +aCustomCoolObject.expressCoolness(); +``` + +You might recognise this function as a “constructor function”: It creates a new instance of an object, and lets us customise its options on creation. + +In fact, you might be familiar with constructor functions in JS that use the `new` keyword. So what does the `new` keyword do? + +## Let’s talk about `new` + +1. Creates a new object, and sets its `__proto__` property to be the `.prototype` property of the function (ie. `Object.create`) +2. Makes that available as `this` in the body of the function +3. Returns the new object. + +So we could re-write the previous `createCoolObject` in this format as follows: + +``` +const CoolObject = function (isCool) { + this.isCool = isCool; +}; +CoolObject.prototype = {isCool: true, expressCoolness: function () { console.log("Am I cool? You know that's " + this.isCool) }}; + +const aConstructedCoolObject = new CoolObject(false); +aConstructedCoolObject.expressCoolness(); +``` + +So in a way `new` and constructor functions could be considered “syntax sugar” to make prototype inheritance easier and more similar to other object oriented languages. And recently you might have run into the `class` syntax, which is new syntax sugar on top of the constructor function form. + +``` +const CoolClass = class { + +constructor(isCool) { + this.isCool = isCool; +} + +isCool = true + +expressCoolness() { + console.log("Am I cool? You know that's " + this.isCool) +} +}; + +const aConstructedCoolObject = new CoolClass(false); +aConstructedCoolObject.expressCoolness(); +``` + +These are all equivalent. But there’s no magic going on in the background, just a chain of prototypes. + +OK. So now we know how we can create objects and link objects to other objects by creating prototype chains. + +## Let’s talk about `this`. + +This is one of the biggest sources of confusion for folks that are used to traditional object oriented program, where `this` refers to the specific instance of an object, however this is not the case for javascript, and understanding what it means is core to the language. + +So, What is `this`? + +Let’s look at these examples. What is the outcome of each of them? + +``` +node + +function aLonelyFunctionDeclaration () { + return this; +} + +const aLonelyFunctionExpression = function () { + return this; +} + +const myObject = { + aFunctionInAnObject: function() { + return this; + } +} + +const anExtractedFunction = myObject.aFunctionInAnObject; + +this; +aLonelyFunctionDeclaration(); +aLonelyFunctionExpression(); +myObject.aFunctionInAnObject(); +anExtractedFunction(); +``` + +OK. So 3 of them: it was the global object. For 1 of them it was the `myObject` object. + +Let’s look at this variation. + +``` +node --use-strict // or 'use strict'; + +function aLonelyFunctionDeclaration () { + return this; +} + +const aLonelyFunctionExpression = function () { + return this; +} + +const myObject = { + aFunctionInAnObject: function() { + return this; + } +} + +const anExtractedFunction = myObject.aFunctionInAnObject; + +this; +aLonelyFunctionDeclaration(); +aLonelyFunctionExpression(); +myObject.aFunctionInAnObject(); +anExtractedFunction(); +``` + + +OK So this gives us some hints. *What can you infer about it?* + +`this` depends on *how the function is called* and not how the function is defined. You could say that the function is the “caller”. + +In `myObject.aFunctionInAnObject()` the caller is `myObject` +In `anExtractedFunction()` the caller is not defined. In strict mode this will remain as undefined. In non strict mode it will default to the global object, which might cause some undesired effects. + +That’s pretty much it… well. Almost. + +Consider this case: + +``` +(function() { + +"use strict"; + +const iRunCallbacks = function(callback) { callback(); }; +const coolObject = { + isCool: true, + expressCoolness() { + console.log("Am I cool? You know that's " + this.isCool) + }, + callWithCallback() { + iRunCallbacks(this.expressCoolness); + } +}; + +coolObject.callWithCallback(); +})(); +``` + +Sometimes we want to pass functions around while still keeping the references to the object we’re using. We can control `this` in some different ways. + +Option 1. Wrap the call in a function. + +``` +(function() { + +"use strict"; + +const iRunCallbacks = function(callback) { callback(); }; +const coolObject = { + isCool: true, + expressCoolness() { + console.log("Am I cool? You know that's " + this.isCool) + }, + callWithCallback() { + + const myCaller = this; + iRunCallbacks(function () { + myCaller.expressCoolness() + }); + } +}; + +coolObject.callWithCallback(); +})(); +``` + +A bit verbose, but it keeps the right caller by keeping a closure to `this` inside the `myCaller` variable. + +We can do better. Javascript provides the function `bind` which creates a new function with its `this` bound to whatever value you specified. `bind` can also bind arguments, it’s a very handy function. Let’s look at how it works + +``` +(function() { + +"use strict"; + +const iRunCallbacks = function(callback) { callback(); }; +const coolObject = { + isCool: true, + expressCoolness() { + console.log("Am I cool? You know that's " + this.isCool) + }, + callWithCallback() { + + iRunCallbacks(this.expressCoolness.bind(this)); + } +}; + +coolObject.callWithCallback(); +})(); +``` + +There’s also `apply` and `call` which is pretty much the same but it also executes the function (somewhat equivalent to `myFunction.bind(…)()`). + +## Let’s talk about arrow functions +ES6 introduced some syntax sugar for this purpose in the form of `() =>` + +``` +(function() { + +"use strict"; + +const iRunCallbacks = function(callback) { callback(); }; +const coolObject = { + isCool: true, + expressCoolness() { + console.log("Am I cool? You know that's " + this.isCool) + }, + callWithCallback() { + + iRunCallbacks(() => this.expressCoolness()); + } +}; + +coolObject.callWithCallback(); +})(); +``` + + +Arrow functions are special functions that are not well suited to be methods, as they *don’t bind their own this*, don’t have arguments, can’t be used with `bind`, can’t be constructors. + +Arrow functions get the `this` based on what was the `this` in the scope they were defined. + +## Let’s talk about prototypes again + +The fact that it works like this, means that javascript is extremely powerful! + +For example, have you ever encountered a statement like this? + +`Array.prototype.slice.call(arguments)` + +We’re using a function of the `Array.prototype` object in a non-array-like object. This works + +There’s nothing *special* about objects in javascript other than the properties and methods they have. If your function quacks like an array, then any object in the `Array.prototype` can be used. Consider the following example: + +``` +const notAString = { + called: 0, + toString() { + this.called += 1; + return `[${this.called}]`; + } +}; + +String.prototype.repeat.call(notAString, 10) +String.prototype.repeat.call(notAString, 10) +``` + +Now you know everything you need to know* + +There’s other interesting things you can do in objects with getters and setters (eg. read-only objects). But that is left as an exercise to the reader: [Working with objects - JavaScript | MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Working_with_Objects#defining_getters_and_setters) + + +### Q&A I + + +## Let’s talk about the event loop. + +Another important part of javascript is knowing when things will run. While javascript is asynchronous, it’s not parallel. Tasks run one at a time in a loop known as the “Event Loop” + +Every function call that is the root of the stack (ie. it’s not running from inside another function), creates a task. + +Every function call from inside another function, adds to the stack. + +Each message will be run *to completion*. That is, no other message will be processed until the previous one is complete. + +Consider these two functions +``` +let someGlobalState = 1; + +function printAndIncreaseGlobalStateByTen() { + console.log("Running some prints with global state", someGlobalState); + for (let i = 0; i < 10; ++i) { + printANumber(someGlobalState); + ++someGlobalState; + } + console.log("Prints run, new global state is", someGlobalState); +} + +function printANumber(number) { + + console.log("here's a number: ", number); +} + +function duplicateGlobalState() { + someGlobalState = someGlobalState * 2; + console.log("Duplicated global state", someGlobalState); +} + +printAndIncreaseGlobalStateByTen(); +duplicateGlobalState(); +``` + +As you can see, `duplicateGlobalState` will not run until all the other calls have finished. We can remove an element from the stack and enqueue it as a message by using `setTimeout`. + +``` +let someGlobalState = 1; + +function printAndIncreaseGlobalStateByTen() { + console.log("Running some prints with global state", someGlobalState); + for (let i = 0; i < 10; ++i) { + setTimeout(function () { + printANumber(someGlobalState) + ++someGlobalState; + }, 0); + } + console.log("Prints run, new global state is", someGlobalState); +} + +function printANumber(number) { + + console.log("here's a number: ", number); +} + +function duplicateGlobalState() { + someGlobalState = someGlobalState * 2; + console.log("Duplicated global state", someGlobalState); +} + +printAndIncreaseGlobalStateByTen(); +duplicateGlobalState(); +``` + +What will the outcome of this be? + +You can compare the two as follows: + +`[t1 [t2] [t2] [t2] [t2] [t2] [t2] [t2] [t2] [t2] [t2]] [t3]` +`[t1] [t2] [t2] [t2] [t2] [t2] [t2] [t2] [t2] [t2] [t2] [t3]` + +Note that `setTimeout` tells you the *minimum* time before the function is executed, and not the actual time. + +Why is this important? Well, the event loop has other tasks to run while your code is running: + +* Calculating Styles +* Calculating Layout +* Rendering +* React to user interaction events + +On every iteration the event loop will run all the pending tasks, and then these operations. Tasks enqueued as a result of this work will not be run until the next iteration of the loop. + +If you’re blocking the event loop by having long-winded tasks, you will not be able to do any of these steps and the UI will be unresponsive :( + +## Let’s talk about `requestAnimationFrame` + +For tasks that are tied to updating rendered state, you have another option that is better than `setTimeout`. `requestAnimationFrame`. + +`requestAnimationFrame` tasks will only run when the browser is about to render a new frame. This can avoid unnecessary work that will not be rendered. + +If non-blocking processing raw speed is important: `setTimeout` +If non-blocking operations should be minimised only to when we want to update the screen: `requestAnimationFrame` + +## Let’s talk about Microtasks + +There’s a different queue for “Microtasks”. Microtasks are promise callbacks and callbacks from the mutation observer. + +Unlike tasks, *all* microtasks are executed every time the queue is empty. This includes microtasks queued as a result of this. + +This means that loops of microtasks *can block render*. Be careful when looping promise callbacks. + +https://codepen.io/rbdr/pen/gOWPwzv + +I really recommend watching this video for a better idea: [Before you continue to YouTube](https://www.youtube.com/watch?v=cCOL7MC4Pl0)., it explains the event loop better than I could ever. + +### Q&A II + +## Let’s talk about MDN +The resource I use the most when building this is MDN. You should get familiar with it because it’ll answer all your questions. + +For example, learn about microtasks here: [Using microtasks in JavaScript with queueMicrotask() - Web APIs | MDN](https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide) +Or learn about the event loop here: [Concurrency model and the event loop - JavaScript | MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop) + +Or learn about arrays here: [Array - JavaScript | MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array) + +One thing to note is that methods can be either `Array.` or `Array.prototype.`. As you might know from what we discussed, the former are methods attached directly to the Array constructor (eg. `Array.from()`) or in the prototype, which means it’s available for any instance (eg. `const array= []; array.filter() // Array.prototype.filter` ) + +## Let’s talk about the Spec +If you want to get deeper into the behaviour, you can read the spec. This is the definition of how JS works, and can give you some specifics, and it’s surprisingly straightforward to follow. For example, how does Array.prototype.slice work? [ECMAScript® 2022 Language Specification](https://tc39.es/ecma262/multipage/indexed-collections.html#sec-array.prototype.slice)