]> git.r.bdr.sh - rbdr/txt/blame_incremental - courses/lets_talk_about_javascript.md
Merge branch 'main' of git.sr.ht:~rbdr/txt
[rbdr/txt] / courses / lets_talk_about_javascript.md
... / ...
CommitLineData
1# Let’s talk about Javascript.
2
3Now, 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”.
4
5## Let’s talk about prototypes
6
7Let’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:
8
91. Functions? Objects
102. Arrays? Objects
113. Strings? You better believe it they’re objects.
124. Then there’s object which are also objects.
13
14Objects 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:
15
16 *What happens when you try to retrieve a property?*
17
18 1. Check if the property exists in the object.
19 1. If it does, return that value
20 2. If it doesn’t, check if the property exists in the `__proto__`
21 1. If it does, return that value
22 2. If it doesn’t, check if the property exists in the `__proto__`
23 1. Continue this until the prototype is null
24
25Cool. Simple enough. *Let’s build some objects then!*
26
27```
28const coolPrototype = {isCool: true, expressCoolness: function () { console.log("Am I cool? You know that's " + this.isCool) }};
29const aCoolObject = {__proto__: coolPrototype};
30const aLonelyObject = {};
31const anUncoolObject = {__proto__: coolPrototype, isCool: false};
32```
33
34You might have noticed the use of `this` in the `expressCoolness` . Put a pin in that, we’ll come back to it later.
35
36What’s the value of each of these statements:
37
38```
39aCoolObject.isCool;
40aLonelyObject.isCool;
41anUncoolObject.isCool;
42```
43
44What’s about these? Why?
45
46```
47aCoolObject.expressCoolness();
48aLonelyObject.expressCoolness();
49anUncoolObject.expressCoolness();
50```
51
52
53What would happen if we do the following? why?
54
55```
56aLonelyObject.__proto__ = coolPrototype;
57aLonelyObject.isCool;
58aLonelyObject.expressCoolness();
59```
60
61
62We could say that “aCoolObject” inherits from “coolPrototype”. An equivalent statement would be that “coolPrototype” is in “aCoolObject’s” prototype chain.
63
64There’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:
65
66```
67const aNewCoolObject = Object.create(coolPrototype);
68aNewCoolObject.__proto__ === coolPrototype; // true!
69aNewCoolObject.isCool; // true!
70```
71
72Of course, when creating objects we might want to customise their behaviour, so we might end up with something like this.
73```
74const createCoolObject = function (isCool) {
75 const newObject = Object.create(coolPrototype);
76 newObject.isCool = isCool;
77 return newObject;
78};
79
80const aCustomCoolObject = createCoolObject(false);
81aCustomCoolObject.expressCoolness();
82```
83
84You might recognise this function as a “constructor function”: It creates a new instance of an object, and lets us customise its options on creation.
85
86In fact, you might be familiar with constructor functions in JS that use the `new` keyword. So what does the `new` keyword do?
87
88## Let’s talk about `new`
89
901. Creates a new object, and sets its `__proto__` property to be the `.prototype` property of the function (ie. `Object.create`)
912. Makes that available as `this` in the body of the function
923. Returns the new object.
93
94So we could re-write the previous `createCoolObject` in this format as follows:
95
96```
97const CoolObject = function (isCool) {
98 this.isCool = isCool;
99};
100CoolObject.prototype = {isCool: true, expressCoolness: function () { console.log("Am I cool? You know that's " + this.isCool) }};
101
102const aConstructedCoolObject = new CoolObject(false);
103aConstructedCoolObject.expressCoolness();
104```
105
106So 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.
107
108```
109const CoolClass = class {
110
111constructor(isCool) {
112 this.isCool = isCool;
113}
114
115isCool = true
116
117expressCoolness() {
118 console.log("Am I cool? You know that's " + this.isCool)
119}
120};
121
122const aConstructedCoolObject = new CoolClass(false);
123aConstructedCoolObject.expressCoolness();
124```
125
126These are all equivalent. But there’s no magic going on in the background, just a chain of prototypes.
127
128OK. So now we know how we can create objects and link objects to other objects by creating prototype chains.
129
130## Let’s talk about `this`.
131
132This 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.
133
134So, What is `this`?
135
136Let’s look at these examples. What is the outcome of each of them?
137
138```
139node
140
141function aLonelyFunctionDeclaration () {
142 return this;
143}
144
145const aLonelyFunctionExpression = function () {
146 return this;
147}
148
149const myObject = {
150 aFunctionInAnObject: function() {
151 return this;
152 }
153}
154
155const anExtractedFunction = myObject.aFunctionInAnObject;
156
157this;
158aLonelyFunctionDeclaration();
159aLonelyFunctionExpression();
160myObject.aFunctionInAnObject();
161anExtractedFunction();
162```
163
164OK. So 3 of them: it was the global object. For 1 of them it was the `myObject` object.
165
166Let’s look at this variation.
167
168```
169node --use-strict // or 'use strict';
170
171function aLonelyFunctionDeclaration () {
172 return this;
173}
174
175const aLonelyFunctionExpression = function () {
176 return this;
177}
178
179const myObject = {
180 aFunctionInAnObject: function() {
181 return this;
182 }
183}
184
185const anExtractedFunction = myObject.aFunctionInAnObject;
186
187this;
188aLonelyFunctionDeclaration();
189aLonelyFunctionExpression();
190myObject.aFunctionInAnObject();
191anExtractedFunction();
192```
193
194
195OK So this gives us some hints. *What can you infer about it?*
196
197`this` depends on *how the function is called* and not how the function is defined. You could say that the function is the “caller”.
198
199In `myObject.aFunctionInAnObject()` the caller is `myObject`
200In `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.
201
202That’s pretty much it… well. Almost.
203
204Consider this case:
205
206```
207(function() {
208
209"use strict";
210
211const iRunCallbacks = function(callback) { callback(); };
212const coolObject = {
213 isCool: true,
214 expressCoolness() {
215 console.log("Am I cool? You know that's " + this.isCool)
216 },
217 callWithCallback() {
218 iRunCallbacks(this.expressCoolness);
219 }
220};
221
222coolObject.callWithCallback();
223})();
224```
225
226Sometimes 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.
227
228Option 1. Wrap the call in a function.
229
230```
231(function() {
232
233"use strict";
234
235const iRunCallbacks = function(callback) { callback(); };
236const coolObject = {
237 isCool: true,
238 expressCoolness() {
239 console.log("Am I cool? You know that's " + this.isCool)
240 },
241 callWithCallback() {
242
243 const myCaller = this;
244 iRunCallbacks(function () {
245 myCaller.expressCoolness()
246 });
247 }
248};
249
250coolObject.callWithCallback();
251})();
252```
253
254A bit verbose, but it keeps the right caller by keeping a closure to `this` inside the `myCaller` variable.
255
256We 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
257
258```
259(function() {
260
261"use strict";
262
263const iRunCallbacks = function(callback) { callback(); };
264const coolObject = {
265 isCool: true,
266 expressCoolness() {
267 console.log("Am I cool? You know that's " + this.isCool)
268 },
269 callWithCallback() {
270
271 iRunCallbacks(this.expressCoolness.bind(this));
272 }
273};
274
275coolObject.callWithCallback();
276})();
277```
278
279There’s also `apply` and `call` which is pretty much the same but it also executes the function (somewhat equivalent to `myFunction.bind(…)()`).
280
281## Let’s talk about arrow functions
282ES6 introduced some syntax sugar for this purpose in the form of `() =>`
283
284```
285(function() {
286
287"use strict";
288
289const iRunCallbacks = function(callback) { callback(); };
290const coolObject = {
291 isCool: true,
292 expressCoolness() {
293 console.log("Am I cool? You know that's " + this.isCool)
294 },
295 callWithCallback() {
296
297 iRunCallbacks(() => this.expressCoolness());
298 }
299};
300
301coolObject.callWithCallback();
302})();
303```
304
305
306Arrow 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.
307
308Arrow functions get the `this` based on what was the `this` in the scope they were defined.
309
310## Let’s talk about prototypes again
311
312The fact that it works like this, means that javascript is extremely powerful!
313
314For example, have you ever encountered a statement like this?
315
316`Array.prototype.slice.call(arguments)`
317
318We’re using a function of the `Array.prototype` object in a non-array-like object. This works
319
320There’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:
321
322```
323const notAString = {
324 called: 0,
325 toString() {
326 this.called += 1;
327 return `[${this.called}]`;
328 }
329};
330
331String.prototype.repeat.call(notAString, 10)
332String.prototype.repeat.call(notAString, 10)
333```
334
335Now you know everything you need to know*
336
337There’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)
338
339
340### Q&A I
341
342
343## Let’s talk about the event loop.
344
345Another 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”
346
347Every function call that is the root of the stack (ie. it’s not running from inside another function), creates a task.
348
349Every function call from inside another function, adds to the stack.
350
351Each message will be run *to completion*. That is, no other message will be processed until the previous one is complete.
352
353Consider these two functions
354```
355let someGlobalState = 1;
356
357function printAndIncreaseGlobalStateByTen() {
358 console.log("Running some prints with global state", someGlobalState);
359 for (let i = 0; i < 10; ++i) {
360 printANumber(someGlobalState);
361 ++someGlobalState;
362 }
363 console.log("Prints run, new global state is", someGlobalState);
364}
365
366function printANumber(number) {
367
368 console.log("here's a number: ", number);
369}
370
371function duplicateGlobalState() {
372 someGlobalState = someGlobalState * 2;
373 console.log("Duplicated global state", someGlobalState);
374}
375
376printAndIncreaseGlobalStateByTen();
377duplicateGlobalState();
378```
379
380As 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`.
381
382```
383let someGlobalState = 1;
384
385function printAndIncreaseGlobalStateByTen() {
386 console.log("Running some prints with global state", someGlobalState);
387 for (let i = 0; i < 10; ++i) {
388 setTimeout(function () {
389 printANumber(someGlobalState)
390 ++someGlobalState;
391 }, 0);
392 }
393 console.log("Prints run, new global state is", someGlobalState);
394}
395
396function printANumber(number) {
397
398 console.log("here's a number: ", number);
399}
400
401function duplicateGlobalState() {
402 someGlobalState = someGlobalState * 2;
403 console.log("Duplicated global state", someGlobalState);
404}
405
406printAndIncreaseGlobalStateByTen();
407duplicateGlobalState();
408```
409
410What will the outcome of this be?
411
412You can compare the two as follows:
413
414`[t1 [t2] [t2] [t2] [t2] [t2] [t2] [t2] [t2] [t2] [t2]] [t3]`
415`[t1] [t2] [t2] [t2] [t2] [t2] [t2] [t2] [t2] [t2] [t2] [t3]`
416
417Note that `setTimeout` tells you the *minimum* time before the function is executed, and not the actual time.
418
419Why is this important? Well, the event loop has other tasks to run while your code is running:
420
421* Calculating Styles
422* Calculating Layout
423* Rendering
424* React to user interaction events
425
426On 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.
427
428If 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 :(
429
430## Let’s talk about `requestAnimationFrame`
431
432For tasks that are tied to updating rendered state, you have another option that is better than `setTimeout`. `requestAnimationFrame`.
433
434`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.
435
436If non-blocking processing raw speed is important: `setTimeout`
437If non-blocking operations should be minimised only to when we want to update the screen: `requestAnimationFrame`
438
439## Let’s talk about Microtasks
440
441There’s a different queue for “Microtasks”. Microtasks are promise callbacks and callbacks from the mutation observer.
442
443Unlike tasks, *all* microtasks are executed every time the queue is empty. This includes microtasks queued as a result of this.
444
445This means that loops of microtasks *can block render*. Be careful when looping promise callbacks.
446
447https://codepen.io/rbdr/pen/gOWPwzv
448
449I 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.
450
451### Q&A II
452
453## Let’s talk about MDN
454The resource I use the most when building this is MDN. You should get familiar with it because it’ll answer all your questions.
455
456For 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)
457Or learn about the event loop here: [Concurrency model and the event loop - JavaScript | MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop)
458
459Or learn about arrays here: [Array - JavaScript | MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array)
460
461One thing to note is that methods can be either `Array.<methodName>` or `Array.prototype.<methodName>`. 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` )
462
463## Let’s talk about the Spec
464If 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)