Lock in $30 Savings on PRO—Offer Ends Soon! ⏳

Javascript - Best Practices & Mistakes to Avoid

Javascript - Best Practices & Mistakes to Avoid

Doesn’t matter if you just started or you use javascript everyday.
there will always be things which might surprise you.

Matteo Bertozzi

July 09, 2023
Tweet

More Decks by Matteo Bertozzi

Other Decks in Programming

Transcript

  1. var x = 1; if (x === 1) { var

    x = 2; console.log(x); // 2 } console.log(x); // 2 function call() { let x = 1 return 10 * x } console.log(call()) // undefined Javascript const arr = ['1', '11', '111']; const res = arr.map(parseInt); console.log(res); // [1, NaN, 7] const nums = [30, 3, 10, 1, 2, 20]; nums.sort(); console.log(nums); // [1, 10, 2, 20, 3, 30] const x = [1, 2, 3] + [4, 5, 6]; console.log(x); // “1,2,34,5,6” [] == {} // false {} == [] // Uncaught SyntaxError: Unexpected token == '10' == 10 // true 'true' == true // false '11' + 1 // 111 '11' - 1 // 10 "2" + "2" - "2" // 20 null == 0; // false null > 0; // false null >= 0; // true const map = new Map() map.set('key', 10); console.log(map); // Map(1) { "key" => 10 } console.log(JSON.stringify(map)); // {}
  2. // let is a variable that can be reassigned let

    value = 10; value += 10; // this is ok value = 100; // this is ok let refItems = [1, 2, 3]; refItems.push(4); // this is ok refItems = [5, 6, 7]; // this is ok const obj = {a: 10}; obj.b = 20; // this is ok obj = {c: "ok"}; // this is ok // const means that the variable cannot be reassigned const value = 10; value += 10; // this is NOT-OK value = 100; // this is NOT-OK const refItems = [1, 2, 3]; refItems.push(4); // this is ok refItems = [5, 6, 7]; // this is NOT-OK const obj = {a: 10}; obj.b = 20; // this is ok obj = {c: “no”}; // this is NOT-OK for (var i = 0; i < 10; ++i) { var xyz = 123 * i; } console.log(i, xyz); // 10 1107 for (let i = 0; i < 10; ++i) { const xyz = 123 * i; } console.log(i, xyz); // ReferenceError: i and xyz are not defined var do not use! let can be reassigned const cannot be reassigned Block-scoped variables It’s time to wave goodbye to var globally-scoped variables Use let and const
  3. Declare Variables near where they are used let a =

    {}; let b = 10; const c = [1, 2, 3, 4]; for (…) { b *= items[i] } if (b == 0) return; for (let i = 0; i < b; ++i) { sum += b * c[i % c.length]; } if (sum < 1000) return; a = {b: 1 + b, sum: 10 * sum}; let b = 10; for (…) { b *= items[i] } if (b == 0) return; const c = [1, 2, 3, 4]; for (let i = 0; i < b; ++i) { sum += b * c[i % c.length]; } if (sum < 1000) return; const a = {b: 1 + b, sum: 10 * sum}; a is allocated here but not even used until the last line. The object allocated here was not even used and thrown away then we assigned a new one. DO NOT allocate objects if not necessary. The machine does not have infinite memory And allocating object is not cheap Our method is over, and a and c were not used Our method is over, and a was not used Keeping variables near were they are used allows to save memory and time in case of early exit And it should improve code readability
  4. Don’t Spend time for the same lookups for (let i

    = 0; i < items.length; ++i) { const name = body.items[i].data['user'].info.name; const surname = body.items[i].data['user'].info.surname; const email = body.items[i].data['user'].info.email; const photo = body.items[i].blobs['user'].profile; users.push({name, surname, email, photo}); } const items = body.items; for (let i = 0; i < items.length; ++i) { const item = items[i]; const userInfo = item.data['user'].info; const photo = item.blobs['user'].profile; const name = user.name; const surname = user.surname; const email = user.email; users.push({name, surname, email, photo}); } Rewrite As Accessing to properties of Objects/Maps is not cheap Especially when there are lots of nested objects Use variables to store the nearest object you have to use. Do lookup the the same item once for (let i = 0; i < items.length; ++i) { const x = i * body.asJson().value; } const value = body.asJson().value; for (let i = 0; i < items.length; ++i) { const x = i * value; } Always assume that functions are slow and have computations Rewrite As
  5. Choose the right data structure const expected = ['foo', 'bar',

    'car', …]; for (let i = 0; i < items.length; ++i) { if (expected.indexOf(items[i].type) >= 0) { // the item is what we expect } } const expected = ['foo', 'bar', 'car', …]; for (let i = 0; i < items.length; ++i) { if (expected.findIndex(v => v == items[i].type) >= 0) { // the item is what we expect } } const expected = ['foo', 'bar', 'car', …]; for (let i = 0; i < items.length; ++i) { const results = expected.filter(v => v == items[i].type); if (results.length == 1) { // the item is what we expect } } const expected = new Set(['foo', 'bar', 'car', …]); for (let i = 0; i < items.length; ++i) { if (expected.has(items[i].type) >= 0) { // the item is what we expect } } The .findIndex() calls a function for each item. Calling a function is expensive The .filter() scans every item, and return a list. If we just need to find the first one matching we are wasting time looking at other items even if we have already the answer we wanted Searching into an Array is an O(N) operation In the worst cast “item not present” We must scan and compare every item If the Array is Sorted you can use Binary Search and you get down to O(log n) In general prefer the use of Set for “contains” operations. Set.has() operation is O(1) Even Worse Even Worse Rewrite as const processed = new Set(); for (const item of items) { if (!processed.has(item.type)) { // process 'type' item once processed.add(item.type); } } Example 1 Example 2
  6. .map() is not a .forEach() const r = [1, 2,

    3].map(v => v * 2); const r = []; for (const v of [1, 2, 3]) { r.push(v * 2); } It creates a new array with the results of calling the provided function on every element Use it only to “map” (transform) the input .map(v => {…}) Writing Is the same as for (let i = 0; i < items.length; i++) { processItem(items[i); } for (const item of items) { processItem(item); } items.forEach(item => processItem(item)); iterate through each items [1, 2, 3].map(v => v * 10); // [10, 20, 30 [1, 2, 3].map(v => { v * 10 }); // [undefined, undefined, undefined] Watch out for Curly braces Is not the same as With curly braces you must use return [1, 2, 3].map(v => { return v * 10; }); // [10, 20, 30]
  7. callbacks and optional params const arr = ['1', '11', '111'];

    const res = arr.map(parseInt); const arr = ['1', '11', '111']; const res = arr.map(v => parseInt(v)); // res = [1, 11, 111] It is NOT Equivalent to const arr = ['1', '11', '111']; const res = arr.map(parseInt); It is Equivalent to const arr = ['1', '11', '111']; const res = arr.map(
 (v,index,array) => parseInt(v,index,array) ); callbackFn(element: T, index: number, array: T[]) .map() .filter() .group() .every() .find() .findIndex() .findLast() .findLastIndex() parseInt(value: string, radix?: number) parseInt('1', 0) // 1 (base inferred) parseInt('11', 1) // Nan (base 1 not supported) parseInt(‘111', 2) // 7 (111 in base 2) function testCallback(a: string, b?: number) { // ... } // always right foo(a => testCallback(a)) // beaware of callback parameters foo(testCallback); [1, NaN, 7] Whaaat?!
  8. Don’t Lose your “this” The bind() method creates a new

    function that, when called, has its this keyword set to the provided value let foo = new Foo(1); const func = foo.bar.bind(foo); setTimeout(func, 100); class Foo { value: number; constructor(value: number) { this.value = value; } bar() { console.log(this.value); } } function test(callback) { // You probably expect to print 1 // But, You get undefined! callback(); } const foo = new Foo(1); test(foo.bar); Mistake #1 function test(callback) { // You probably expect to print 1 callback(); } const foo = new Foo(1); // Not working! //test(foo.bar); // Working alternative test(() => foo.bar()); Solutions #1 let foo = new Foo(1); setTimeout(() => foo.bar(), 100); foo = new Foo(2); // You probably expect to print 1 // But, You get 2 Mistake #2 let foo = new Foo(1); // Not working! //setTimeout(() => foo.bar(), 100); // Working alternative const func = foo.bar.bind(foo); setTimeout(func, 100); foo = new Foo(2); Solutions #2
  9. Asyncronous code vs Sequential function doSomething(callback) { setTimeout(() => {

    for (let i = 0; i < 3; ++i) { // notify the value change callback(i); } }, 1000); } const results = []; doSomething(value => results.push(value)); console.log(results); // [] doSomething(value => { console.log('received a value from the callback:’, value) }); // received a value from the callback: 0 // received a value from the callback: 1 // received a value from the callback: 2 The results of the callbacks will not be available on the next line Handle Events When they arrive Using the callback
  10. Async-Await vs Callbacks const httpResult = await fetch(SERVICE_URL); const result

    = await httpResult.json(); console.log(result); fetch(SERVICE_URL) .then(response => response.json()) .then(data => { const result = data; console.log(result); // the fetch is completed, // execute everything else here }); // any async operation above // are still in progress... let result; fetch(SERVICE_URL) .then(response => response.json()) .then(data => result = data); console.log(result); // undefined function doSomething() { return new Promise((resolve, reject) => { setTimeout(() => { const results = []; for (let i = 0; i < 3; ++i) { results.push(i); } resolve(results); }, 1000); }); } doSomething().then(results => console.log('callback', results)); console.log(‘after doSomething() using callbacks’); // after doSomething() using callbacks // callback [ 0, 1, 2 ] Remember that With Async Code the Execution is non-linear const results = await doSomething(); console.log('after doSomething, already with the result', results); // after doSomething, with the result [ 0, 1, 2 ]
  11. Generator Functions for (const item of getItems()) { console.log(item); }

    function getItems() { const items: Item[] = []; for (let i = 0; i < 10; ++i) { items.push({ id: i, price: i * 10 }); } return items; } function *getItems(): Generator<Item> { for (let i = 0; i < 10; ++i) { yield { id: i, price: i * 10 }; } } Using Yield (Generator Functions) Using a list (Without Generator) async function *getItems(): AsyncGenerator<Item> { const URL = 'https://dummyjson.com/products/1'; for (let i = 0; i < 10; ++i) { // demo async "generation/computation" const r = await fetch(URL); yield await r.json(); } } for await (const item of getItems()) { console.log(item); } For await Async function * async generators Generator functions allow you to declare a function that behaves like an iterator The Yield Keyword
  12. Compare things The strict equality operator '10' == 10 //

    true '10' != 10 // false 'true' == true // false 'true' != true // true null == undefined // true null != undefined // false The “loose equality” operator '10' === 10 // false '10' !== 10 // true 'true' === true // false 'true' !== true // true null === undefined // false null !== undefined // true The “Strict equality” operator Type And Value Check
  13. Sorting Numbers Elements are converted into strings and sorted lexicographically

    const nums = [30, 3, 10, 1, 2, 20]; nums.sort((a, b) => a - b); console.log(nums); [1, 2, 3, 10, 20, 30] Sort comparator returns < 0 if (a < b) > 0 if (a > b) = 0 if (a == b) const nums = [30, 3, 10, 1, 2, 20]; nums.sort(); console.log(nums); [1, 10, 2, 20, 3, 30] Using a Custom Comparator function compareByAgeSurnameName(a, b) { let cmp = a.age - b.age; if (cmp != 0) return cmp; cmp = a.surname.localeCompare(b.surname); if (cmp != 0) return cmp; return a.name.localeCompare(b.name); } Composite Key Comparator const pepole = [{name: 'foo', surname: 'bar', 'age': 30}]; pepole.sort(compareByAgeSurnameName);
  14. JSON.stringify() function jsonReplacer(_: string, val: unknown): unknown { if (val

    instanceof Map) { return Object.fromEntries(val.entries()); } else if (val instanceof Set) { return Array.from(val.keys()); } else if (val instanceof Uint8Array) { return base64(val); } // else if ... return val; } const map = new Map([['k1', 'v1'], ['k2', 'v2']]); console.log(JSON.stringify(map)); // {} const set = new Set(['a', 'b', 'c']); console.log(JSON.stringify(set)); // {} const data = new Uint8Array([1, 2, 3, 4]); console.log(JSON.stringify(data)); // {"0":1,"1":2,"2":3,"3":4} console.log(JSON.stringify(map, jsonReplacer)); // {"k1":"v1","k2":"v2"} console.log(JSON.stringify(set, jsonReplacer)); // ["a","b","c"] console.log(JSON.stringify(data, jsonReplacer)); // "AQIDBA==" Replacer Function Use the replacer
  15. UUID & Secure Random const id = crypto.randomUUID() // id

    = ‘36b8f84d-df4e-4d49-b662-bcde71a8764f’ const buf = new Uint8Array(16); crypto.getRandomValues(buf); function bad_uuidv4() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx' .replace(/[xy]/g, c => { const r = (Math.random() * 16) | 0; const v = c === 'x' ? r : (r & 0x3) | 0x8; return v.toString(16); }); } Pseudo-Random number generators have limited randomness and can generate collisions There is already a method to generate an UUID v4 …and a strong random value generator Use the Crypto API
  16. The “Crypto” Module Hashes & Encryption const data = new

    TextEncoder().encode('hello world'); const hash = await crypto.subtle.digest('SHA-512', data); const key = await crypto.subtle.generateKey({name: "AES-GCM", length: 256}, true, ["encrypt", "decrypt"]); const iv = crypto.getRandomValues(new Uint8Array(12)); const enc_data = await crypto.subtle.encrypt({ name: "AES-GCM", iv: iv }, key, data); const dec_data = await crypto.subtle.decrypt({ name: "AES-GCM", iv: iv }, key, enc_data); // new TextDecoder().decode(data) === data API for Encryption/Decryption API for Hash Digest
  17. Use let and const Declare Variables near where they are

    used Don’t Spend time for the same lookups, and use variables Choose the right data structure and learn about time complexity .map() is not a .forEach() be aware of callbacks and optional parameters use bind() or arrow functions when passing methods as callback beaware of asynchronous code, even if Looks sequential beaware of comparisons, use the triple equals as much as you can sort() converts elements into string, Use Custom comparators JSON stringify() may not work as expected on all the types avoid Math.random() for IDs, use the crypto module remember the standard library for digests and encryption Best Practices & Mistakes to Avoid Javascript