Javascript for everyone

Translated into:
UA
EN

Олександр Данильченко

Full-stack розробник у компанії Binary Studio. Працює над розробкою нового SPA фреймворку, який генерує складні візуально насичені сайти лише за простими декларативними описами у кількох json'ах. Любить відкривати та експериментувати з новими функціями веб-платформи. І не любить Safari через несумісності та застарілість.

Hello, world! 👋
Розповім про всі радості сучасного JavaScript, починаючи від базового синтаксису і закінчуючи (обережно, жаргон!) модулями, бандлерами і транспайлерами. Перед тим як розпочати — декілька дісклеймерів про зміст лекції:
  1. Деякі терміни та технології розглядаються поверхнево;
  2. "— А клікати на всі лінки на документацію і читати всі-всі статті?" Ну-у-у-у-у, up to you;
  3. Все мало би бути цікаво та по максимуму інформативно, але тут вже як вийде... Have fun! 🙃

І ще один heads up — вам знадобиться улюблена IDE, Node.js, і стартер проекту (складатиму його по ходу лекції). Готові? Побігли!.

Студент все ще проходить випробувальний термін у своїй новій компанії і все ще використовує Slack для співпраці з іншими розробниками та комунікації з клієнтами. Одного теплого весняного вечора в чат пише Ryu — колишній клієнт (returning customer, ya-a-a-ay!), для якого раніше було зроблене онлайн-резюме. Він повністю задоволений якістю нашої роботи і хоче довірити нам ще один проект —
Ryu06:40
(stares intently at you, his headband fluttering in the wind)

To live is to fight, and to fight is to live.

(clenches his fist)

I’ve spent all my life defeating my enemies, perfecting my martial skills, and learning the true essence of what it means to be an honorable warrior. But now my greatest challenge lies before me.

(raising his hand into the air, Ryu’s hand is engulfed in flame, and he brings it cr-r-r-r-rashing down to the ground, splitting the Earth in two as a hot knife would cut through butter)

I need my own browser game.

(in the distance, a Japanese flute plays a gentle melody)

My master tells me that it’s necessary to promote my “brand image”. I have no idea what he means by that, but then again, I am not one to question his wisdom. I am sure, in time, the “fanbase” I will acquire through this game will bring me great strength in the battles to come.

(Ryu once again turns his gaze upon you)

Please go forth and complete this task for me. In return, I promise to avenge any relatives who may have been killed by your arch nemesis, or restore your family honor, or… whatever. We’ll work out contract details later.

(Ryu turns his back and begins to walk away)

For now I will go meditate. Let me know if you have any questions ― WiFi is a bit spotty up at the Mountain Temple, but I’ll check my email whenever I get the chance.

Oleksandr KovalovAndrii KarunOleg ChulanovskyiAlexandr TovmachArtem Manukian
+8
14 repliesLast reply today at 06:54
Отже, жодної специфікації немає, але схоже на те, що цього разу мова йде про симулятор "вуличної бійки" — гру, в якій гравець вибирає двох бійців, які в свою чергу проводять поєдинок, луплячи один одного руками, ногами, своєю головою. У кого скорше "закінчиться життя", той і програв. Насправді ж вони не вмирають, бо вони віртуальні.
Ryu06:40
Who isn't real? Me not real?! Your not real, 🤬!
You
+5
7 repliesLast reply today at 06:54
🤭Ой... 🤔_Або вмирають..._ Замовник не надав жодного конкретного технічного завдання, тому складемо його собі самі. Так само і дизайн. Нехай на початках це буде просто браузерна гра, в якій один персонаж систематично б'є іншого і навпаки, а помірної складності алгоритм визначає, якої сили був нанесений удар і скільки життя залишилось в того повільного невдахи, що не встиг ухилитись від "з локтя у носа". А там далі вже подивимось, який новий функціонал чи дизайн можна встигнути закодити до дедлайну. Що ж, запевнити клієнта, що все під контролем і до роботи?
You06:40
Yeah, sure, I'm on it, let's go! I'm gonna target modern browsers and will start with the plain JavaScript using new features where necessary. Sometime in the future, when the project evolves, we'll use libraries like React or Vue.js, but for now, vanilla JavaScript will do the job just fine.
RyuAndrii KarunOleksandr KovalovAlyona Rapova
+10
15 repliesLast reply today at 06:54
Level 1 back to top

Basic JavaScript

Difficulty: Nothing special 🤷‍♂️. Objectives: Get a general idea about JavaScript.
  1. Ready to try JavaScript?

    To start with, here are the main points you should remember about JS:

    Brendan EichEcmaScriptscript languagedynamic languageweakly typedfirst-class functionsobject-orientedprototype-basedsingle-threadedasynchronous

  2. Putting comments in code

    The syntax of comments is the same as in C++ and in many other languages. Comments behave like white space and are discarded during script execution.

    const style = {
      display: 'flex',
      flexWrap: 'wrap'
      // dominantBaseline: central; not supported by IE 9 :(
    }
    /*
     * I'm a large and bulky
     * comment
     */
    function foo() {
      console.log('bar');
    }
    
    /**
     * returns the name of the lecture
     * @param {string} course 'js' | 'net'
     * @returns {string | undefined}
     */
    function getLectureName(course) {
      switch (course) {
        case "js":
          return "Introduction to JavaScript";
        case "net":
          return "C# and .NET framework";
      }
    }
  3. Declaring block:

    A block statement (or compound statement in other languages) is used to group zero or more statements. The block is delimited by a pair of braces:

    {
      // I'm a block and I have my own scope here
    }
  4. Declaring variables: var, let and const

    There are three kinds of declarations in JavaScript:

    • Declaration will be hoisted to the global scope or a function block scope. Variable defined inside of the inner scope can override another variable from the outer scope. That's why it mustn't be used in the modern JS code!
      var message;
      message = 'Hello World';
      
      var name = 'John',
          age = 24;
      
      var count = 10;
      if (count > 5) {
        var count = 5; // Not declares a new variable, but using existing hoisted count
      }
      // count is equal to 5 😰
    • Declaration won't be hoisted but will create a new locally scoped variable.
      let message;
      message = 'Hello World';
      
      let name = 'John',
          age = 24;
      
      let count = 10;
      if (count > 5) {
        let count = 5; // Declares a new local variable, which doesn't interfere with the outer scope
        // count is equal to 5 here 👌
      }
      // count is still equal to 10 👌
    • Variable cannot be reassigned, but objects defined with const can be changed
      const PRIMARY_COLOR = '#ccc';
      PRIMARY_COLOR = '#fff'; // ❌
      
      const user = {
        name: 'John',
        age: 20
      };
      user.name = 'Oleksandr'; // 👌
      user.newProp = 'newProp'; // 👌
      
      user = 'newValue'; // ❌
  5. Data Types

    According to the ECMAScript, there are seven primitive types and everything else is an Object type. Let’s run over those one by one:

    • The boolean type has two values: true and false.

      let isValid = true;
      isValid = false;
    • An integer or floating point number.

      const age = 23;
      const coef = 12.345;
      const hex = 0x00111; // hexadecimal, base 16 starts with 0x
      const octal = 0o01; // octal, base 8 starts with 0o
      const binary = 0b0011; // binary, base 2 starts with 0b
      The number type has three symbolic values: `+Infinity`, `-Infinity`, and `NaN` (not-a-number).
      const result = 2 / "text"; // NaN
      isNaN(result); // true
    • Represents who represent whole numbers larger than 253 - 1 which is larger than maximum value of Number type.

      const bigInt = 9007199256474096991n;
      const evenLargerInt = BigInt("99007199256474096991");
    • A sequence of characters that represent a text value. JavaScript strings are immutable.

      let str = "Hello world";
      str = 'new string';
      
      let age = 23;
      let newStrOld = 'age - ' + age;
      let newStr = `age - ${age}`;
      console.log(`first line
      second line`);
      str.length // 10
      str.charAt(2) // w
      str.toLowerCase() // "new string"
      str.toUpperCase() // "NEW STRING"
      str.indexOf('str') // 4
      newStr.substring(11, 17) // second
      newStr.substr(11, 6) // second
    • A Symbol is a unique and immutable primitive value and may be used as the key of an object property. So we can “covertly” hide something into objects that we need, but others should not see, using symbolic properties.

      const isAdmin = Symbol('isAdmin');
      
      const user = {
        name: 'John',
        isAdmin: false,
        [isAdmin]: true
      }
      
      console.log(user.isAdmin); // false
      console.log(user[isAdmin]); // true
      
    • undefined is a property of the global object. The initial value is the primitive value undefined

      let x; // undefined
      
      const noReturnValue = foo(); // undefined
      function foo() {
        return;
      }
    • null is a keyword denoting a special empty (null) value.

      let x = null;
  6. Type coercion

    JavaScript variables can be converted to a new variable and another data type either by the use of a JavaScript function or automatically by JavaScript itself:

    Converting to string:

    String(null) // "null"
    false.toString();  // "false"
    12 + 'px' // "12px"

    Converting to number:

    Number(true) // 1
    +true // 1
    parseInt('3.14', 10) // 3
    parseFloat('3.14') // 3.14
    12 - '4' // 8
    12 - 'text' // NaN
    12 + +'4' // 16

    Converting to boolean:

    0, '', null, undefined, NaN -> false
    Boolean(null); // false
    !!null; // false
    if (true || 12) { // true
    if (true && 0) { // false
    
    if (12) { // will be executed }
  7. Standard built-in objects

    Objects, which are created in the global scope and can be accessed wherever in the code. They can be created by the user script or provided by the host environment (browser, node). Let's look through the common ones:

    • Represents complex Javascript data type. It is used to store various keyed collections and more complex nested entities.
      const obj = {
        foo: 'bar',
        nested: {
          bar: 'foo'
        }
      }
      
      Object.keys(obj); // ['foo', 'nested']
      Object.values(obj); // ['bar', { bar: 'foo' }]
      Object.entries(obj); // [ ['foo', 'bar'], ['nested', { bar: 'foo' }] ]
    • List-like object, which includes methods to perform traversal and mutation operations. Each element can be accessed only by its index. Neither the length of a JavaScript array nor the types of its elements are fixed.
      let names;
      names = new Array('Anna', 'Alex', 'Alicia');
      names = ['Anna', 'Alex', 'Alicia'];
      
      names[0]; // 'Anna'. Indices range [0..n-1]
      names.includes('Alex') // true
      
      arr.push(element); // add to the end
      arr.unshift(element); // add to the front
      arr.splice(index, 1, element); // insert element on index
      arr.splice(index, 1); // remove element on index
      arr.pop(); // remove from the end
      arr.shift(); // remove from the front
    • Object, which holds key-value pairs and remembers the original insertion order of the keys. Any value (both objects and primitive values) may be used as either a key or a value.
      const binarians = new Map();
      
      binarians.set('JsExpert', 'Alicia');
      binarians.set('PhpExpert', 'Volodymyr');
      binarians.set('JsExpert', 'Alex');
      
      binarians.get('JsExpert'); // 'Alex'
      binarians.delete('PhpExpert');
    • Object allows storing unique values of any type, whether primitive values or object references. Plays a role as an array of unique values.
      const uniqueNames = new Set();
      
      uniqueNames.add("Alex");
      uniqueNames.add("Alicia");
      uniqueNames.add("Alex"); // ['Alex', 'Alicia']
      
      uniqueNames.has('Alex'); // true
      uniqueNames.delete('Alex');
    • Object which holds pattern used to match character combinations in strings.
      const regexp = /ab+c/; // compiling literal, preferred
      const regexp = new RegExp('ab+c' + variable);
      regexp.test('abc'); // true
    • Object represents a single moment in time in a platform-independent format. Internally contains a Number that represents milliseconds since 1 January 1970 UTC.
      const dateCurrent = new Date();
      const dateMarch = new Date('March 13, 2000, 08:04:20');
      dateMarch.getFullYear(); // 2000
      dateMarch.getHours(); // 4
    • Object that has properties and methods for mathematical constants and functions. Works with Number type, but doesn't support BigInt type
      Math.PI; // 3.141592653589793
      Math.E; // 2.718281828459045
      ...
      
      Math.sin(1); // 0.8414709848078965
      Math.cos(1); // 0.5403023058681398
      Math.abs(-3); // 3
      ...
      
      Math.max(1, 2, 3, 10); // 10
      Math.min(1, 2, 3, 10); // 1
      Math.random(); // [0..1)
      ...
    • Object contains methods for parsing JSON and converting stringified values to JSON.
      const json = {
        value: "123"
      }
      const stringifiedJson = JSON.stringify(json); // "{"value":"123"}"
      const parsedJson = JSON.parse(stringifiedJson); // { value: "123" }
  8. Functions

    Function is a "subprogram" that can be called by code external (or internal in the case of the recursion) to the function.
    A function is composed of a sequence of statements called the function body. Values can be passed to a function, and the function will return a value. In JavaScript, functions are first-class objects, because they can have properties and methods just like any other object.
    There are three ways to declare function in Javascript:

    • function foo() {}
      foo(); // Can be accessed before declaration
      
      function foo() {
        console.log(`I'm a hoisted function ${bar()}`); // "I'm a hoisted function with a nested function"
      
        function bar() {
          return "with a nested function";
        }
      }
      const obj = {
        prop: 1,
        func: contextLog
      }
      obj.func();
      
      function contextLog() {
        console.log(this); // { prop: 1, func: f }
      }
    • const foo = function () {}
      const foo = function() {
        const bar = function() {
          return "with a nested function";
        }
        console.log(`I'm not a hoisted function ${bar()}`); // "I'm not a hoisted function with a nested function"
      }
      
      foo(); // Must be accessed only after declaration
      const obj = {
        prop: 1,
        func: function() {
          console.log(this); // { prop: 1, func: f }
        }
      }
      obj.func();
    • const foo = () => {}
      const foo = () => {
        const bar = () => {
          return "with a nested arrow function";
        }
        console.log(`I'm not a hoisted arrow function ${bar()}`); // "I'm not a hoisted arrow function with a nested arrow function"
      }
      
      foo(); // Must be accessed only after declaration
      const obj = {
        prop: 1,
        func: () => {
          console.log(this); // { Window }
        },
      
      
        functionNestedExpressions: function() {
          const nestedExpression = function() {
            console.log(this); // { Window }
          }
          console.log(this); // { prop: 1, func: f, functionNestedExpressions: f, functionNestedArrow: f }
          nestedExpression();
        },
      
      
        functionNestedArrow: function() {
          const nestedArrow = () => {
            console.log(this); // { prop: 1, func: f, functionNestedExpressions: f, functionNestedArrow: f }
          }
          console.log(this); // { prop: 1, func: f, functionNestedExpressions: f, functionNestedArrow: f }
          nestedArrow();
        }
      }
      obj.func();
      obj.functionNestedExpressions();
      obj.functionNestedArrow();
      setTimeout(() => console.log("I have waited a second to run"), 1000);
  9. Loops and iteration

    Loops offer a quick and easy way to do something repeatedly. The statements for loops provided in JavaScript are:

    • for (let step = 0; step < stepCount; step++) {
        const value = step * intervalComputed;
      
        if (value === 0) {
          labelsFromRange.push('0');
          continue;
        }
      
        const labelText = formatNumber(value, decimalSeparator);
        const labelWidth = getTextWidth(labelText);
        const isLastStep = step === stepCountWithMargin;
      
        if (isLastStep && remainingAxisLength < labelWidth) {
          break;
        }
      
        labelsFromRange.push(labelText);
      }
    • let level = currentLevel;
      
      while(level >= 0) {
        // do something
        level--;
      }
    • do {
        // do something
        level--;
      } while (lavel >= 0);
    • const users = ['Mike', 'Alex'];
      users.forEach((value, index, array) => console.log(value)); // 'Mike', 'Alex'
      
      users.forEach((value, index) => {
        if (index > 0) return; // Skips the iteration
        console.log(value); // 'Mike'
      });
      
      // LIFEHACK
      // Traverse not all the elements of the huge arrays
      users.some((value, index) => {
        if (value === 'Skip me!') return false; // Skips the iteration
        if (index === 1) return true;
        console.log(value); // 'Mike'
      });
    • const users = ['Mike', 'Alex'];
      for (let value of users) {
        console.log(value); // Mike, Alex
      }
      
      const user = { firstName: 'Mike', lastName: 'Din' };
      for (let value of user) {
        console.log(value); // Mike, Din
      }
      Object.values(user).forEach(value => console.log(value)); // Mike, Din
    • const obj = {
        prop1: "I'm a property 1",
        prop2: "Property indeed!"
      }
      
      for (const prop in obj) { // OWN + INHERITED
        console.log(prop); // prop1, prop2
      }
      Object.keys(obj) // ONLY OWN PROPERTIES
        .forEach(value => console.log(value)); // prop1, prop2
Level 2 back to top

Building a webpage

Difficulty: Let's spice it up 🔥. Objectives: Gain practical knowledge.
  1. const API_URL = 'https://api.github.com/repos/oleksandr-danylchenko/street-fighter/contents/resources/api/fighters.json';
    const SECURITY_HEADERS = {
      headers: {
        authorization: "token %your_token%"
      }
    };
    
    fetch(API_URL, SECURITY_HEADERS);
    • const responsePromise = fetch(API_URL, SECURITY_HEADERS);
      console.log(responsePromise);
    • responsePromise.then(response => { console.log(response) });
    • responsePromise.then(response => response.json());
    • fetch(API_URL, SECURITY_HEADERS)
        .then(response => response.json())
        .then(file => {
          const fighters = JSON.parse(atob(file.content));
          console.log(fighters);
        });
  2. const names = fighters.map(it => it.name);
    const namesStr = names.join('\n');
  3. const rootElement = document.getElementById('root');
    
    rootElement.innerText = 'Loading...';
    
    fetch(API_URL, SECURITY_HEADERS)
      .then(response => response.json())
      .then(file => {
        const fighters = JSON.parse(atob(file.content));
        const names = fighters.map(it => it.name);
        const namesStr = names.join('\n');
        rootElement.innerText = namesStr;
      });
  4. Learn more about Promises:
    • fetch(API_URL, SECURITY_HEADERS)
        .then(
          response => response.json(),
          error => console.warn(error)
        )
    • fetch(API_URL, SECURITY_HEADERS)
        .then()
        .then()
        .catch(error => {
          console.warn(error);
          rootElement.innerText = 'Failed to load data';
        });
    • fetch(API_URL, SECURITY_HEADERS)
        .then(response => {
          if (!response.ok) {
            throw new Error('Failed load data');
          }
          return response.json();
        })
    • if (!response.ok) {
        return Promise.reject(Error('Failed load data'));
      }
    • <div id="loading-overlay">
        <img src="resources/logo.png"/>
      </div>
      const loadingElement = document.getElementById('loading-overlay');
      
      loadingElement.remove();
    • fetch(API_URL, SECURITY_HEADERS)
        .then()
        .then()
        .catch()
        .finally(() => {
          loadingElement.remove();
        });
    • const greetingPromise = Promise.resolve({ value: 'Hello World' });  
      greetingPromise.then(res=> { console.log(res.value + '!') });  
    • const promise = new Promise((resolve, reject) => {
        // some asynchronous code
        let isValid = true 
      	
        if (isValid) {  
          resolve("Success");  
        } else {  
          reject(Error("Error"));  
        }  
      });
    • const createTimeoutPromise = (message, ms) =>
          new Promise(resolve => {
              setTimeout(() => resolve(message), ms);
          });
      
      const one = createTimeoutPromise('first resolved', 2000);
      const two = createTimeoutPromise('second resolved', 3000);
      
      const promiseRace = Promise.race([one, two]);
      const promiseAll = Promise.all([one, two]);
      
      promiseRace.then(res => console.log(res)); // first resolved
      promiseAll.then(res => console.log(res)); // ['first resolved', 'second resolved']
  5. const BASE_API_URL = 'https://api.github.com/';
    const SECURITY_HEADERS = {
      headers: {
        authorization: "token %your_token%"
      }
    };
    
    const rootElement = document.getElementById('root');
    const loadingElement = document.getElementById('loading-overlay');
    
     function startApp() {
      const endpoint = 'repos/oleksandr-danylchenko/street-fighter/contents/resources/api/fighters.json';
      const fightersPromise = callApi(endpoint, 'GET');
      fightersPromise.then(fighters =>
        rootElement.innerText = getFightersNames(fighters)
      );
    }
    
    function callApi(endpoint, method) {
      const url = BASE_API_URL + endpoint;
      const options = { method, ...SECURITY_HEADERS };
    
      return fetch(url, options)
        .then(response => response.ok ? response.json() : Promise.reject(Error('Failed to load')))
        .then(file => JSON.parse(atob(file.content)))
        .catch(error => {
          console.warn(error);
          rootElement.innerText = 'Failed to load data';
        })
        .finally(() => {
          loadingElement.remove();
        });
    }
    
    function getFightersNames(fighters) {
      return fighters.map(it => it.name).join('\n');
    }
    
    startApp();
    • function callApi(endpoind, method = 'GET') {
      }
    • async function startApp() {
        const endpoint = 'repos/oleksandr-danylchenko/street-fighter/contents/resources/api/fighters.json';
        const fighters = await callApi(endpoint);
        rootElement.innerText = getFightersNames(fighters);
      }
        const fighters = await callApi(endpoint);
        const fightersArr = await callApi(endpoint);
      
        const [fighters, fightersArr] = await Promise.all([
          callApi(endpoint),
          callApi(endpoint)
        ]);
    • function* generateSequence() {
        const result = yield 1;
        console.log(result); // 'args'
        yield 2;
        yield 3;
      }
      
      let generator = generateSequence();
      let one = generator.next(); // {value: 1, done: false}
      let two = generator.next('args'); // {value: 2, done: false}
      let three = generator.next(); // {value: 3, done: true}
  6. async function startApp() {
      try {
        loadingElement.style.visibility = 'visible';
    
        const endpoint = 'repos/oleksandr-danylchenko/street-fighter/contents/resources/api/fighters.json';
        const fighters = await callApi(endpoint);
    
        rootElement.innerText = getFightersNames(fighters);
      } catch (error) {
        console.warn(error);
        rootElement.innerText = 'Failed to load data';
      } finally {
        loadingElement.style.visibility = 'hidden';
      }
    }
    
    function callApi(endpoint, method = 'GET') {
      const url = BASE_API_URL + endpoint;
      const options = { method, ...SECURITY_HEADERS };
    
      return fetch(url, options)
        .then(response =>
          response.ok
            ? response.json()
            : Promise.reject(Error('Failed to load'))
        )
        .then(file => JSON.parse(atob(file.content)))
        .catch(error => { throw error });
    }
  7. Create more DOM elements and add event listeners
    • function createElement({ tagName, className = '', attributes = {} }) {
        const element = document.createElement(tagName);
        element.classList.add(className);
      	
        Object
          .keys(attributes)
          .forEach(key => element.setAttribute(key, attributes[key]));
      
        return element;
      }
    • function createName(name) {
        const nameElement = createElement({ tagName: 'span', className: 'name' });
        nameElement.innerText = name;
      
        return nameElement;
      }
      
      function createImage(source) {
        const attributes = { src: source };
        const imgElement = createElement({
          tagName: 'img',
          className: 'fighter-image',
          attributes
        });
      
        return imgElement;
      }
    • function createFighter(fighter) {
        const { name, source } = fighter;
        const nameElement = createName(name);
        const imageElement = createImage(source);
        const element = createElement({ tagName: 'div', className: 'fighter' });
      
        element.append(imageElement, nameElement);
      
        return element;
      }
      • Destructuring assignment — expression that unpacks values from arrays, or properties from objects, into distinct variables.

        const obj = {
          a: true,
          b: true,
          c: true
        };
        
         // ❌
        const a = obj.a;
        const secondProperty = obj.b;
        
        // 👌
        const {
          a,
          b: secondProperty
        } = obj;
        
        const words = ['a', 'b'];
        const [firstElement] = words; // ['a']
    • function createFighters(fighters) {
        const fighterElements = fighters.map(fighter => createFighter(fighter));
        const element = createElement({ tagName: 'div', className: 'fighters' });
      
        element.append(...fighterElements);
      
        return element;
      }
      • Using the spread operator and rest parameter:

        • const words = ['a', 'b'];
          const extendedWords = [...words, 'c']; // ['a', 'b', 'c']
          
          const obj = {
              a: true,
              b: true
          };
          const extendedObj = {
              ...obj,
              c: true
          }; // { a: true, b: true, c: true }
        • const words = ['a', 'b'];
          const [firstElement, ...restElements] = words; // ['a', ['b']]
          
          const obj = {
              a: true,
              b: true
          };
          const {
              a,
              ...restProps
          } = obj; // { a: true, restProps: { b: true } }
    • const fightersElement = createFighters(fighters);
      
      rootElement.appendChild(fightersElement);
    • element.addEventListener('click', (event) => handleFighterClick(event, 'wrapper'), false)
      imageElement.addEventListener('click', (event) => handleFighterClick(event, 'image'), false)
      
      function handleFighterClick(event, el) {
        console.log(el);
      }
  8. const fightersDetailsMap = new Map();
    
    element.addEventListener('click', (event) => handleFighterClick(event, fighter), false)
    
    function handleFighterClick(event, fighter) {
      const { _id } = fighter;
    
      if(!fightersDetailsMap.has(_id)) {
        // send request here
        fightersDetailsMap.set(_id, fighter);
      }
    
      console.log(fightersDetailsMap.get(_id));
    }
  9. Add classes (use inheritance and static properties)
    • class FighterService {
       #endpoint = 'repos/oleksandr-danylchenko/street-fighter/contents/resources/api/fighters.json'
      
       async getFighters() {
         try {
           const apiResult = await callApi(this.#endpoint, 'GET');
           return JSON.parse(atob(apiResult.content));
         } catch (error) {
           throw error;
         }
       }
      }
      const fighterService = new FighterService();
    • class View {
        element;
      
        createElement({ tagName, className = '', attributes = {} }) {
          const element = document.createElement(tagName);
          element.classList.add(className);
          
          Object.keys(attributes).forEach(key => element.setAttribute(key, attributes[key]));
      
          return element;
        }
      }
    • get element() {
        return this.element;
      }
      
      set element(value) {
        this.element = value;
      }
    • class FighterView extends View {
        constructor(fighter, handleClick) {
          super();
          this.createFighter(fighter, handleClick);
        }
      
        createFighter(fighter, handleClick) {
          const { name, source } = fighter;
          const nameElement = this.createName(name);
          const imageElement = this.createImage(source);
      
          this.element = this.createElement({ tagName: 'div', className: 'fighter' });
          this.element.append(imageElement, nameElement);
          this.element.addEventListener('click', event => handleClick(event, fighter), false);
        }
      
        createName(name) {
          const nameElement = this.createElement({ tagName: 'span', className: 'name' });
          nameElement.innerText = name;
          return nameElement;
        }
      
        createImage(source) {
          const attributes = { src: source };
          return this.createElement({
            tagName: 'img',
            className: 'fighter-image',
            attributes
          });
        }
      }
      
      class FightersView extends View {
        fightersDetailsMap = new Map();
      
        constructor(fighters) {
          super();
          this.createFighters(fighters);
        }
      
        createFighters(fighters) {
          const fighterElements = fighters.map(fighter => {
            const fighterView = new FighterView(fighter, this.handleFighterClick);
            return fighterView.element;
          });
      
          this.element = this.createElement({ tagName: 'div', className: 'fighters' });
          this.element.append(...fighterElements);
        }
      
        handleFighterClick(event, fighter) {
          this.fightersDetailsMap.set(fighter._id, fighter);
          console.log('clicked')
          // get from map or load info and add to fightersMap
          // show modal with fighter info
          // allow to edit health and power in this modal
        }
      }
    • class Lumberjack {
        cut() {
          console.log("Cutting tree");
        }
      }
      
      class SmartLumberjack extends Lumberjack {
        cut() {
          console.log("Measuring sizes of the tree");
          super.cut();
        }
      }
      
      const smartLumberjack = new SmartLumberjack();
      smartLumberjack.cut(); /*
                              * Measuring sizes of the tree
                              * Cutting tree
                              */
    • class App {
        static rootElement = document.getElementById('root');
        static loadingElement = document.getElementById('loading-overlay');
      
        static async startApp() {
          try {
            App.loadingElement.style.visibility = 'visible';
      
            const fighters = await fighterService.getFighters();
            const fightersView = new FightersView(fighters);
      
            App.rootElement.appendChild(fightersView.element);
          } catch (error) {
            console.warn(error);
            App.rootElement.innerText = 'Failed to load data';
          } finally {
            App.loadingElement.style.visibility = 'hidden';
          }
        }
      }
      
      App.startApp();
    • // .bind()
      const obj = { a: 1 };
      const bindedFunction = foo.bind(obj);
      bindedFunction(); // { a: 1 }
      
      function foo() {
        console.log(this);
      }
      // .call()
      const obj = { a: 1 };
      foo.call(obj); // { a: 1 }
      
      function foo() {
        console.log(this);
      }
      // .apply()
      const obj = { a: 1 };
      const args = ['firstArg', 'secondArg'];
      foo.apply(obj, args); // { a: 1 }, 'firstArg', 'secondArg'
      
      foo.call(obj, ...args); // alternative
      
      function foo(arg1, arg2) {
        console.log(this, arg1, arg2);
      }
      class FightersView extends View {
        constructor(fighters) {
          super();
      
          this.handleClick = this.handleFighterClick.bind(this);
          this.createFighters(fighters);
        }
      
        createFighters(fighters) {
          const fighterElements = fighters.map(fighter => {
      
            // 1. Class function with context
            const fighterView = new FighterView(fighter, this.handleClick);
      
            // 2. Inline context binding
            const fighterView = new FighterView(fighter, this.handleFighterClick.bind(this));
      
            // 3. Arrow function
            const fighterView = new FighterView(fighter, (event, fighters) => this.handleFighterClick(event, fighters));
      
            return fighterView.element;
          });
      
          // ...
        }
      }
  10. 💥Boom, done! There is half already!
Level 3 back to top

Modules, transpiling, bundling, etc.

Difficulty: And now we gonna roll 🤸. Objectives: Get the most out of the JS.
  1. Hold on, How JavaScript Classes Work Under the Hood again?

    • const person = {
        walk: true
      };
      
      const student = {
        studying: 'Javascript'
      };
      
      student.__proto__ = person; // ❌ - controversial and discouraged
      Object.setPrototypeOf(student, person); // 👌 - recommended alternative
      
      console.log(student.walk) // true
    • function Animal(name, sound) {
        this.name = name; 
        this.sound = sound;
      }
    • Animal.prototype.makeSound = function() {
        console.log(`${this.name} ${this.sound}`);
      };
    • const cat = new Animal("Sam", "meowing");
      console.log(cat.sound);
      cat.makeSound();
    • function Bird(name, sound) {
        Animal.apply(this, arguments);
      
        this._home = "tree"; // Private property naming convention
      }
      
      Bird.prototype = Object.create(Animal.prototype);
      Bird.prototype.constructor = Bird;
      
      Bird.prototype.fly = function() {
        Animal.prototype.makeSound.call(this);
        console.log(`Bird ${this.name} fly`);
      };
      
      const bird = new Bird("Kiwi", "singing");
      bird.fly();
  2. Let's learn more about the transpilers — tools that read source code written in one programming language, and produce the equivalent code in another language. The most popular one, Babel, is mainly used to convert ECMAScript 2015+ code into a backwards compatible version of JavaScript in current and older browsers or environments. Let's try it out:

    • Install node.js, open Terminal in Visual Studio Code (TerminalNew Terminal), and run npm init.
    • Now install babel with npm i -D @babel/core @babel/cli
    • Add "scripts" to the package.json file
      "scripts": {
        "build": "babel index.js -d dist"
      }
      and run npm run build in the terminal to see if it works
    • Set up transpiling of ES2015+ code — npm install @babel/preset-env --save-dev, create a .babelrc in the root of your project, with the following content:
      {
        "presets": [
          [
            "@babel/preset-env",
            {
              "targets": {
                "chrome": "67",
                "firefox": "60"
              }
            }
          ]
        ]
      }
    • Let's watch for changes by running babel with -w flag
      "scripts": {
        "build": "babel src -d dist -w"
      }
  3. What’s great about ES6 modules?
    • export const fighterService = new FighterService();
      
      export { callApi as apiHelper };
      
      export default View;
      
      export * from './apiHelper';
      export { callApi } from './apiHelper';
      
      export { default } from './View';
    • import App from './src/javascript/app';
      
      import { fighterService } from './services/fightersService';
      
      import { callApi as apiHelper } from '../helpers/apiHelper';
      
      import * as helper from '../helpers/apiHelper';
      helper.callApi();
  4. Alright, let's npminstall everything set up Webpack — a module bundler for JavaScript applications:
    • Install Webpack by running npm install webpack webpack-cli --save-dev, then create a file named webpack.config.js in the root of your project, with the following content:
      const path = require('path');
      
      module.exports = {
        entry: './index.js',
        output: {
          path: path.resolve(__dirname, 'dist'), 
          filename: 'bundle.js'
        },
        mode: 'development'
      }
    • Set build command in package.json's scripts key to "build": "webpack"
    • Set up loaders:

      • npm install babel-loader --save-dev and edit webpack.config.js to include module key:
          module: {
            rules: [
              {
                test: /\.m?js$/,
                exclude: /(node_modules|bower_components)/,
                use: [
                  {
                    loader: "babel-loader",
                    options: {
                      configFile: "./babel.config.js",
                      cacheDirectory: true
                    }
                  }
                ]
              }
            ]
          }
        Rename .babelrc to babel.config.js to consistent with webpack config type and change it's content to:
        const presets = [
          [
            "@babel/preset-env",
            {
              targets: {
                firefox: "60",
                chrome: "67"
              }
            }
          ]
        ];
        
        module.exports = { presets };
      • As usual, just run npm install css-loader style-loader --save-dev and edit webpack.config.js to include the new rules:
          module: {
            rules: [
              {
                test: /\.css$/,
                use: ["style-loader", "css-loader"]
              },
              // other rules
            ]
          }
    • Set up plugins:

      • npm install html-webpack-plugin --save-dev and edit webpack.config.js to include plugins key:
          // other settings
          plugins: [
            new HtmlWebpackPlugin({
              template: "index.html"
            })
          ],
        • npm install html-loader --save-dev and edit webpack.config.js to include the new rule:
            // other settings
            module: {
              rules: [
                // other rules
                {
                  test: /\.html$/,
                  use: [
                    {
                      loader: "html-loader"
                    }
                  ]
                }
              ]
            },
    • Add Webpack DevServernpm install webpack-dev-server --save-dev and modify webpack.config.js to include devServer and publicPath keys:
      
        output: {
          // other output settings
          publicPath: '/'
        },
        // other Webpack settings
        devServer: {
          port: 9000
        }

      Add "dev": "webpack-dev-server" to package.json's scripts key

    • Maybe add devtool option to the webpack.config.js for much better debugging experience — devtool: "source-map"
  5. 🥁Badum tss! Відрефакторено!

― That's all, folks! 🐷

Якщо ти доскролив аж до цього моменту, то тоді вітаю — це вже фініш 🙂. Дякую за увагу 🙏! Чекаю на запитання та коментарі під цією лекцією у особистому кабінеті Binary Studio Academy. І звичайно ж — успіхів з домашкою! Enjoy 🙃