Основы основ в JavaScript для того, чтобы идти дальше

Stas Bagretsov
19 min readJul 29, 2018

--

Эта статья включает в себя все основные принципы работы в JavaScript, которые вам понадобятся для понимания библиотек, фреймворков, нового функционала, классовости со всей её разносторонностью и много всего другого связанного с этим языком программирования.

По-факту это адаптированный перевод четырёх статей, со сносками на другие, более детальные статьи по этим же темам из моего блога:

Learn these JavaScript fundamentals and become a better developer

Javascript Fundamentals

Learn these core JavaScript concepts in just a few minutes

Understanding Classes in JavaScript

👉Мой Твиттер — там много из мира фронтенда, да и вообще поговорим🖖. Подписывайтесь, будет интересно: ) ✈️

Оглавление статьи

Примитивы
Переменные
Функции
this
arguments
Массивы
Объекты
Деструктуризация
Классы
Динамическое типизирование
Однопоточность
Исключения
Паттерны прототипов
MVC
Async/await
Замыкание
Callback

Примитивы

Числа, булины(или логический тип данных, булины это сленг для простоты упоминания в и так сложных для восприятия текстах), строки, undefined и null это всё примитивы.

Числа

Есть только один тип числа в JavaScript, это число двойной точности. Арифметика десятичных чисел не является точной.

Как вы уже можете знать, 0.1 + 0.2 не выдаст в результате 0.3. Но с целыми числами, такая арифметика будет верна, так что 1+2 === 3.

Числа наследуют методы из объекта Number.prototype. Методы могут вызываться на числах:

(123).toString(); //”123"(1.23).toFixed(1); //”1.2"

Есть глобальные функции для конвертирования в числа: parseInt(), parseFloat() и Number():

parseInt(“1”) //1parseInt(“text”) //NaNparseFloat(“1.234”) //1.234Number(“1”) //1Number(“1.234”) //1.234

Неверные арифметические операции или неверные конверсии не будут обрабатываться как исключения, а выдадут NaN (Not-a-Number) значение. isNan() может определять NaN.

Оператор + может добавлять или конкатенировать:

1 + 1 //2“1” + “1” //”11"1 + “1” //”11"

Более подробно эта тема расписана в статье JavaScript. Почему 3 + true = 4? И другие хитрые уравнения

Строки

Строки хранят в себе набор символов Unicode. Текст может находиться внутри двойных скобок “” или одинарных ‘’.

У строк есть такие замечательные методы, как: substring(), indexOf() и конечно же concat().

“text”.substring(1,3) //ex“text”.indexOf(‘x’) //2“text”.concat(“ end”) //text end

Строки ведут себя как примитивы, они постоянны. Для примера, concat() не модифицирует существующую строку, но создаёт новую.

Булины

У булинов есть только два значения: true и false.

То есть простым языком, верное и ложное значение. false, null, undefined, ‘’, 0 и NaN выдают false. Все другие значения, включая все объекты, выдают true.

Верное значение это true, говоря в контексте булинов. Ложное это false, соответственно. Посмотрите на следующий пример, показывающий результат при false.

let text = ‘’;if(text) {console.log(“This is true”);} else {console.log(“This is false”);}

Переменные

Назначать переменные можно используя var, let и const.

var объявляет и при необходимости инициализирует переменную. Значение переменной, которая не инициализирована — undefined. Переменные объявляются с помощью var имеют функциональную область видимости.

Переменные let, имеют блочную область видимости.

Переменные объявленные с помощью const не могут быть переназначены. Но их значение, вопреки, все таки может быть изменено. const замораживает переменную, Object.freeze() замораживает объект. cosnt имеет блочную область видимости.

Область видимости переменной, объявленной вне любой функции является глобальной. Более подробно об этом можно почитать в этой статье — Разбираемся с “поднятием” (hoisting) в JavaScript

Функции

Функции это независимые единицы поведения. Функции это объекты. Функции могут быть назначены переменным, храниться в массивах или объектах, передаваться как аргумент для другой функции и возвращаться из других функций.

Есть три способа определения функции:

Объявление функции

Функциональное выражение

Стрелочная функция

Объявление функции

Function это первое слово в строке, оно должно иметь имя, оно может использоваться для определения. Объявленные функции “поднимаются” вверх своей области видимости.

function doSomething(){}

Функциональное выражение

Function не является первым словом в строке. Имя ей вообще опционально. Тут может быть анонимное функциональное выражение или названное.

Оно должно быть сначала определено, а уже затем оно может выполниться.

Оно может автоматически выполняться после объявления (Это называется IIFE, то есть немедленно вызываемые функции)

let doSomething = function() {}

Стрелочная функция

Это очень удобный синтаксис для создания анонимных функциональных выражений.

let doSomething = () = > {};

У стрелочных функций нет собственного this и аргументов.

Вызов функции

Функция определенная с помощью function, может быть вызвана разными способами:

В форме функции:doSomething(arguments)В форме метода:theObject.doSomething(arguments)theObject[“doSomething”](arguments)В форме конструктора:new doSomething(arguments)В форме applydoSomething.apply(theObject, [arguments])doSomething.call(theObject, arguments)В форме связки (bind)let doSomethingWithObject = doSomething.bind(theObject);doSomethingWithObject();

Функции могут быть вызваны с большим или меньшим количеством аргументов, чем указано в определении. Дополнительные аргументы будут проигнорированы, а недостающие параметры получат undefind.

this

this представляет контекст функции. Только функции объявленные с помощью function, имеют свой собственный this контекст. Его значение зависит от того, как была вызвана функция. Давайте посмотрим значения this, зависящие от формы вызова doSomething():

function doSomething(){console.log(this)}+ — — — — — — + — — — — — — — — — +| Form | this |+ — — — — — — + — — — — — — — — — -| Function | window/undefined || Method | theObject || Constructor| the new object || apply | theObject || bind | theObject |+ — — — — — — + — — — — — — — — — +

arguments

Псевдопараметр arguments дается всем аргументам применяемым в вызове. Это схожий объект, проходящий на массив, но на самом деле не массив. Ему не хватает методов массива.

function reduceToSum(total, value){return total + value;}function sum(){let args = Array.prototype.slice.call(arguments);return args.reduce(reduceToSum, 0);}sum(1,2,3);

Альтернативой является новый синтаксис параметров. В этот раз args это множественный объект.

function sum(…args){return args.reduce(reduceToSum, 0);}return

Функция без return отдаст undefined. Обращайте внимание на вставление точки с запятой при использовании return. Следующая функция не отдаст пустой объект, а выдаст undefined.

function getObject(){return{}}getObject()

Чтобы избежать этой проблемы, используйте { на одной строке с return } :

function getObject(){return {}}

Массивы

Есть два синтаксиса для создания пустого массива:

let arr = new Array();let arr = [];let fruits = [“Apple”, “Orange”, “Plum”];

Выше у нас есть как и объявленные, так и назначенные значения в одном объявлении.

Добавляем данные с использованием push

let fruits = [“Apple”, “Orange”];fruits.push(“Pear”);alert(fruits); // Apple, Orange, Pear

Это синтаксис добавления данных в массив, он довольно прост — arrayname.push(item).

Когда мы добавляем данные с использованием push, то они добавляются в конец массива.

Добавление данных с использованием unshift

let fruits = [“Orange”, “Pear”];fruits.push(“Apple”);alert(fruits); // Apple, Orange, Pear

А это синтаксис для подставки данных в массив — arrayname.unshift(item).

Когда мы добавляем данные с использованием unshift, то они добавляются в начало, так как в unshift все элементы массива сдвигаются на одну позицию вправо и первое место остаётся за добавляемым значением.

let fruits = [“Apple”];fruits.push(“Orange”, “Peach”);fruits.unshift(“Pinnaple”, “Lemon”);// [“Pinnaple”, “Lemon”, “Apple”, “Orange”, “Peach”]alert(fruits);

В примере выше несколько элементов добавлены в массив с использованием push и unshift, синтаксисами arrayname.push(item1,item2,item3..) и arrayname.unshift(item1,item2,item3….).

Удаление элементов с использованием pop

let fruits = [“Apple”, “Orange”, “Plum”];alert(fruits.pop()); // удаляет “Pear” и показывает его.alert(fruits); // Apple, Orange

Это синтаксис для вырывания элемента из массива — arrayname.pop().

Когда мы удаляем элемент с использованием pop, элемент удаляется с конца.

Удаляем элемент с использованием shift

let fruits = [“Apple”, “Orange”, “Plum”];alert(fruits.shift()); // удаляет Apple и показывает его.alert(fruits); // Orange, Pear

Добавление, обновление, удаление элементов с использованием splice

arr.splice(index[, deleteCount, elem1, …, elemN])

Тут индекс обозначает позицию, удаляет число элементов указанных в deleteCount и затем вставляет элементы представленные с elem1 до elemN.

Пример 1. Удаление.

let arr = [“I”, “study”, “JavaScript”];arr.splice(1,1); // начиная с элемента под первым индексом, удалится один элемент.alert(arr); // [“I”, “JavaScript”]

Так вот, выше удалиться один элемент, начиная с индекса 1.

Пример 2. Удаление и вставление

let arr = [“I”, “study”, “JavaScript”, “right”, “now”];// Удаляем три элемента и замещаем их другимиarr.splice(0, 3, “Let’s”, “dance”);alert(arr) // теперь получается так, [“Let’s”, “dance”, “right”, “now”]

Тут начиная с индекса 0 до 3, элементы будут удалены, а 2 элемента будут добавлены. Таким образом, конечный массив будет выглядеть именно так, как в примере кода выше. Если мы попытаемся вернуть splice, то он вернет массив удаленных элементов.

Пример 3. Вставление

let arr = [“I”, “study”, “JavaScript”];// начиная с индекса 2// удаляем 0 элементов// и вставляем “complex” и “language”arr.splice(2, 0, “complex”, “language”);alert(arr); // “I”, “study”, “complex”, “language”, “JavaScript”

Тут удаляется 0 элементов и со второго индекса добавляются два элемента.

Создаем подмассивы, используя slice

arr.slice(start, end)

Тут start определяет позицию с которой вы хотите начать и end определяет индекс того, где вы хотите закончить. Так что в соответствии со Start и end указанными индексами, функция slice вернёт копию массива от start до end, но не включая end.

let str = “test”;let arr = [“t”, “e”, “s”, “t”];alert ( str.slice(1, 3) ); // esalert ( arr.slice(1, 3) ); // e, salert ( str.slice(-2) ); // stalert ( arr.slice(-2) ); // s, t

Объединяем массивы с элементами или массивами, используя concat

arr.concat(arg1, arg2…)

Concat возвращает новый массив, соединяя массивы с аргументами, которые могут быть как элементами, так и массивами.

let arr = [1, 2];// соединяем arr с [3,4]alert ( arr.concat([3, 4])); // 1, 2, 3, 4//соединяем arr c [3, 4] и [5, 6]alert ( arr.concat( [3, 5], [5, 6])); // 1, 2, 3, 4, 5, 6//соединяем arr с [3, 4], а затем добавляем значения 5 и 6alert ( arr.concat([3, 4], 5, 6)); // 1, 2, 3, 4, 5, 6

Поиск

Тут методы схожи с теми, которые используются для строк, но работают на элементах, вместо символов.

arr.indexOf(item, from) ищет элемент item, начиная с элемента from и возвращает индекс найденного элемента, в проливном случае возвращается -1.

arr.lastIndexOf(item, from) — тоже самое, только ищет с права налево.

arr.includes(item, from) — ищет элемент item, начиная с индекса from, возвращая true при нахождении его.

let arr = [1, 0, false];alert( arr.indexOf(0) ); // 1alert( arr.indexOf(false) ); // 2alert( arr.indexOf(null) ); // -1alert ( arr.includes(1) ); // true

Эти методы используют === для сравнения.

Если нам просто нужно знать, содержит ли массив нужный элемент, то тогда мы используем includes. Также includes может правильно обрабатывать NaN, в отличии от indexOf и lastIndexOf.

Найти и найти индекс

let result = arr.find(function(item, index, array) {// должно отдать true, если элемент это то, что мы реально ищем});

Синтаксис сверху возвращает элемент, если находит его или если ничего не находится, то возвращается undefined.

let users = [{id: 1, name: “John”},{id: 2, name: “Pete”},{id: 3, name: “Mary”}];let user = users.find(item => item.id == 1);alert(user.name); // John

И так, выше мы использовали стрелочную функцию, где элемент это аргумент и мы отдаем элемент, где item.id == 1.

findIndex делает тоже самое, только отдаёт индекс найденного элемента, вместо самого элемента.

Filter

Функция фильтра работает также, как и find, но вместо отдачи просто первого элемента, она возвращает все подходящие элементы.

Let results = arr.filter(function(item, index, array) {// должно отдать true, если элемент проходит filter});

Пример ниже отдаёт все элементы, где item.id < 3.

let users = [{id: 1, name: “John”},{id: 2, name: “Pete”},{id: 3, name: “Mary”}];// возвращает массив из первых двух пользователей.let someUsers = users.filter(item => item.id < 3);alert(someUsers.length); //2

Трансформируем массив

Ниже находятся функции, которые трансформируют или меняют порядок массива, без изменения чего-либо в оригинальном массиве. Это рассматривается, как один из самых важных подходов, так что уделите время для того, чтобы понять этот.

Map

let result = arr.map(function(item, index, array) {// возвращает новое значение, вместо элемента});

Тут результат переменной отдаёт то что сделала функция с массивом, без изменения самого массива. Для примера, ниже у нас есть новый массив, где каждый элемент конвертируется в свою длину.

let lengths = [“Bilbo”, “Gandalf”, “Nazgul”].map(item => item.length)alert(lengths); // 5, 6, 7

И тут мы снова применяем стрелочную функцию, где элемент это аргумент и каждый элемент трансформируется в свою длину.

Reduce

Для итерирования по массиву мы можем использовать loop. Для трансформирования значения каждого элемента, мы можем использовать maps. Но если мы хотим отдать одно значение, основанное на всём массиве, то нам на помощь приходит reduce.

let value = arr.reduce(function(previousValue, item, index, arr) {// …}, initial);

Тут previousValue означает значение, которое оно возвращает с предидущего вызова. Индекс указывает откуда нам начать применять функцию, а initial в конце, определяет изначальное значение previousValue. Давайте посмотрим на пример, чтобы понять это получше.

let arr = [1, 2, 3, 4, 5];let result = arr.reduce((sum, current) => sum + current, 0);alert(result); //15

В это функции sum это previousValue и назначено изначальное значение равное нулю. Функция возвращает sum + current в каждом запросе и так как индекс не указан, он покрывает весь массив. И в конце он возвращает сумму всех элементов массива.

Иной принцип объяснения некоторых методов можно прочитать в этих статьях моего блога.

Объясняем методы в массивах JavaScript: Filter vs Map vs Reduce vs Foreach

Методы JavaScript, которые улучшат ваши умения за несколько минут

Объекты

Вот синтаксис для создания новых объектов.

let user = new Object(); // Синтаксис “конструктора объекта”let user = {}; // синтаксис “объект литерала”Тут мы создаем объект с такими свойствами.let user = { // объектname: “John”, // под ключом name хранится значение “John”age: 30 // под ключом age хранится значение 30};

Тут мы добавляем новое булинное свойство.

user.isAdmin = true;

Тут мы удаляем свойство из объекта.

delete user.age;

Тут давайте посмотрим, как мы назначаем значения свойств объектам, используя функции.

function makeUser(name, age) {return {name: name,age: age// другие свойства};}let user = makeUser(“John”, 30);alert(user.name); // John

А вот сокращение для этого же. Так как мы назначаем имя свойства имени аргумента, то мы можем избежать дополнительный синтаксис, выполнив этот.

function makeUser(name, age) {return {name, // то же, что и name: nameage // то же, что и age: age};}

Давайте посмотрим, как мы можем получить значения ключей объекта, используя цикл.

for (key in object) {// выполняется тело для каждого ключа среди свойств объекта}

Рассмотрим, как это выполняется в реальной жизни.

let user = {name: “John”,age: 30,isAdmin: true};for(let key in user) {// keysalert( key ); // name, age, isAdminalert( user[key] ); // John, 30, true}

Методы объекта

Если бы у нас был объект как ниже, то мы смогли бы посмотреть на то, как мы можем применять методы объектов для доступа к ключам, значениям и к тому и другому.

let user = {name: “John”,age: 30};Object.keys(user) = [name, age]Object.values(user) = [“John”, 30]Object.entries(user) = [[name, age], [“John”, 30]]

Деструктуризация

Деструктуризационное назначение это специальный синтаксис, который позволяет “распаковать” массив или объект в кучку переменных, так как зачастую это гораздо удобнее. Деструктуризация также часто отлично работает с сложными функциями, которые имеют множество параметров, стандартных значений и т.п. Сейчас вы увидите как это прекрасно работает и как с этим справляет деструктуризация.

Начнем с объектов:

const student = {firstname: ‘Glad’,lastname: ‘Chinda’,country: ‘Nigeria’};// Деструктурируем объектconst { firstname, lastname, country } = student;console.log(firstname, lastname, country); // Glad Chinda Nigeria

Теперь посмотрим на массивы:

let red = 100;let green = 200;let blue = 50;const rgb = [200, 255, 100];// Деструктуризируем назначения на красные и зеленые[red, green] = rgb;console.log(`R: ${red}, G: ${green}, B: ${blue}`); // R: 200, G: 255, B: 50

Посмотрите ещё на парочку интересных моментов в деструктуризации:

let user = {};[user.name, user.surname] = “Ilya Kantor.split(‘ ‘);alert(user.name); // Ilya

В примере выше, мы назначаем свойства объекта деструктуризацией.

let user = {name: “John”,age: 30};// Проходимся по всем ключам и значениямfor (let [key, value] of Object.entries(user)) {alert( `${key}:${value}`); // name: John, потом age:30}

Пример выше хорош тогда, когда мы можем получить связку ключ-значение из объекта, используя деструктуризацию в цикле.

Вообще деструктуризация это очень сложная, но полезная тема. Примеры выше дают только понятие того, что это такое. В этой статье Деструктуризация в ES6. Полное руководство можно подробно узнать о том, как работает реструктуризация в JavaScript. Обязательно углубитесь в эту тему, она сможет сэкономить вам уйму времени и сил.

Классы

Это очень важный момент для понимания библиотек, современных фреймворков и т.п. Очень внимательно прочтите этот раздел.

Классы это функции

Класс в JavaScript это тип функции. Классы объявляются с помощью слова class. Сейчас мы будем использовать функциональное выражение для инициализации функции и классовое выражение для инициализации класса.

// Инициализируем функцию при помощи функционального выраженияconst x = function() {}// Инициализируем класс с помощью классового выраженияconst y = class {}

Мы можем получить доступ к [[Prototype]] объекта, используя метод Object.getPrototypeOf(). Давайте его используем, чтобы протестировать созданную нами, пустую функцию.

Object.getPrototypeOf(x);Получаемƒ () { [native code] }Мы также можем использовать этот метод на созданном нами классе.Object.getPrototypeOf(y);Outputƒ () { [native code] }

Код созданный с помощью function и class, возвращает функцию [[Prototype]]. С прототипами, любая функция функция может быть стать экземпляром конструктора при использовании слова new.

const x = function() {}// Инициализируем конструктор из функцииconst constructorFromFunction = new x();console.log(constructorFromFunction);Получаем на выходе:x {}constructor: ƒ ()

Это также применимо и к классам:

const y = class {}// Инициализируем конструктор из классаconst constructorFromClass = new y();console.log(constructorFromClass);Outputy {}constructor: class

Эти примеры конструкторов пусты, но мы можем видеть как в глубине синтаксиса, оба метода достигают одного и того же результата.

Создаем класс

Функция конструктор инициализируется с параметрами, которые будут назначены как свойства от this в отношении самой функции. Первая буква в названии должна писаться с большой буквы.

// Создаем функцию конструкторfunction Hero(name, level) {this.name = name;this.level = level;}

Когда мы переводим её в классовый синтаксис, показанный ниже, то мы видим, что он структурирован очень похоже.

// Инициализируем классclass Hero {constructor(name, level) {this.name = name;this.level = level;}}

Мы знаем, что функция конструктор подразумевается как план объекта, при написании первой буквы инициализатора с заглавной и с помощью схожести синтаксиса. Слово class более понятное и доходчивое, учитывая цели нашей функции.

Единственная разница в инициализации это использование слова class, вместо function и назначение свойств внутри метода constructor().

Создание методов

Распространенная практика с функцией конструтором в том, чтобы назначать методы напрямую prototype, вместо инициализации, это видно на примере метода greet() ниже:

function Hero(name, level) {this.name = name;this.level = level;}// Добавляем метод конструкторуHero.prototype.greet = function() {return `${this.name} says hello.`;}

При помощи классов этот синтаксис упрощается и метод может быть добавлен напрямую в класс. Используя сокращенное создание классов из ES6, теперь этот процесс гораздо короче.

class Hero {constructor(name, level) {this.name = name;this.level = level;}// Добавляем конструктору методgreet() {return `${this.name} says hello.`;}}

Давайте посмотрим на свойства и методы в действии. Мы создадим новый экземпляр Hero, с использованием слова new и назначим ему несколько значений.

const hero1 = new Hero(‘Varg’, 1);

Если мы выведем больше информации о нашем новом объекте с помощью console.log(hero1), то мы можем увидеть больше деталей о том, что происходит с классом при инициализации.

Вывод:Hero {name: “Varg”, level: 1}__proto__:▶ constructor: class Hero▶ greet: ƒ greet()

Мы можем видеть в выводе, что constructor() и greet() функции, были применены к _proto_ или [[Prototype]] от hero1 и не напрямую как метод на объекте hero1. Когда это понятно создание функций конструктора, это не так очевидно при создании классов.

В случае с классами мы получаем более простой и сжатый синтаксис, но жертвуем некоей ясностью в процессе.

Наследование

Преимущество функции конструктора и классов в том, что они могут наследоваться в новый объект, основанный на родителе. Это исключает повторения в коде для похожих объектов, но требует дополнительных и более специфичных функций.

Новые функции конструктора могут быть созданы из родителя с использованием метода call(). В примере ниже мы создадим специальный класс персонажа под именем Mage и назначим свойства для Hero, используя call(), также как и добавив дополнительные свойства.

// Создаем новый конструктор из родителяfunction Mage(name, level, spell) {// Связываем конструктор с allHero.call(this, name, level);this.spell = spell;}

На этом этапе мы можем создать новый экземпляр Mage, используя одни и те же свойства, что и Hero, так и новые добавленные.

const hero2 = new Mage(‘Lejon’, 2, ‘Magic Missile’);

Отправляя hero2 в консоль, мы можем видеть, что новый Mage основан на конструкторе.

OutputMage {name: “Lejon”, level: 2, spell: “Magic Missile”}__proto__:▶ constructor: ƒ Mage(name, level, spell)

С классами ES6, слово super использует вместо call, чтобы получить доступ к родительским функциям. Мы будем использовать extends, чтобы сослаться к родительскому классу.

// Создаем новый класс из родителяclass Mage extends Hero {constructor(name, level, spell) {// Связываем с supersuper(name, level);// Добавляем новое свойствоthis.spell = spell;}}

И так, теперь мы можем создать новый экземпляр Mage, тем же способом.

const hero2 = new Mage(‘Lejon’, 2, ‘Magic Missile’);Вы выедем hero2 в консоль и посмотрим, что нам выдаст.OutputMage {name: “Lejon”, level: 2, spell: “Magic Missile”}__proto__: Hero▶ constructor: class Mage

Вывод практически тот же самый, кроме того, что класс конструктора [[Prototype]], ссылается на родителя, а в этом случае это Hero.

Ниже вы увидите прямо сравнение всего процесса инициализации, добавления методов и наследования функции конструктора и класса.

function Hero(name, level) {this.name = name;this.level = level;}// Добавляем метод конструкторуHero.prototype.greet = function() {return `${this.name} says hello.`;}// Создаем новый конструктор из родителяfunction Mage(name, level, spell) {// Связываем новый конструктор из родителяHero.call(this, name, level);this.spell = spell;}Теперь посмотрим классы.// Инициализируем классclass Hero {constructor(name, level) {this.name = name;this.level = level;}// Добавляем метод конструкторуgreet() {return `${this.name} says hello.`;}}// Создаем новый класс из родителяclass Mage extends Hero {constructor(name, level, spell) {// Связываем конструктор с supersuper(name, level);// Добавляем новое свойствоthis.spell = spell;}}

Хоть синтаксис и довольно отличается, лежащий в основе результат среди двух методов почти такой же. Классы дают нам более сжатый способ создания макетов для объектов, а конструктор функция более точно описывает то, что происходит внутри “под капотом”.

Динамическое типизирование

У JavaScript есть динамическое типизирование. У значений есть типы, у переменных нет. Типы могут меняться в процессе работы.

function log(value){console.log(value);}log(1);log(“text”);log({message : “text”});Оператора typeof() может проверять тип переменной:let n = 1;typeof(n); //numberlet s = “text”;typeof(s); //stringlet fn = function() {};typeof(fn); //function

Однопоточность

Основы рабочего процесса в JavaScript однопоточны. Две функции не могут быть запущены одновременно. Рабочий процесс содержит очередь событий, которые хранят список того, что нужно выполнить. Тут нет состояний гонки или тупиков с взаимными блокировками. Однако, коду в очереди событий нужно работать быстрее. В противном случае, браузер перестанет отвечать и запросит завершение задачи.

Исключения

У JavaScript есть механизм обработки исключений. Он работает так, как вы и подумали, обертывая код с использованием try/catch. Есть только один блок catch, который кэширует все исключения.

Очень полезно знать то, что JavaScript иногда имеет предпочтения для скрытых ошибок. Следующий код не выкинет исключение при попытке изменения замороженного объекта:

let obj = Object.freeze({});obj.message = “text”;

Strict mode устраняет некоторые скрытые ошибки в JavaScript. Что бы его включить, используйте “use strict”;

Паттерны прототипов

Object.create(), функция конструктор и class, создают объекты по системе прототипов. Рассмотрим следующий пример:

let service = {doSomething : function() {}}let specializedService = Object.create(service);console.log(specializedService.__proto__ === service); //true

Я использовал Object.create(), чтобы создать новый объект specializedService, который имеет объект service, в виде прототипа. Это означает то, что doSomething() доступно на объекте specializedService. Также это означает то, что свойство _proto_ объекта specializedService указывает на объект service.

Давайте сделаем схожий объект, используя class.

class Service {doSomething(){}}class SpecializedService extends Service {}let specializedService = new SpecializedService();console.log(specializedService.__proto__ === SpecializedService.prototype);

Все методы определенные в классе Service, будут добавлены объекту Service.prototype. Экземпляры класса Service будут иметь тот же объект прототип (Service.prototype). Все они будут отсылать запросы методов к Service.prototype объекту. Методы определяются единожды в Service.prototype и потом наследуются всеми экземплярами.

Цепочки прототипов

Объекты наследуют от других объектов. У каждого объекта есть прототип и он наследует свои свойства из него. Прототип может выдать скрытое свойство _proto_.

Когда вы запрашиваете свойство, которое не содержит объект, JavaScript посмотрит вниз по цепочке прототипов до тех пор пока он либо не найдет запрашиваемое свойство, либо не дойдет до конца цепочки.

MVC

Model-view-controller это дизайн фреймворк (не язык программирования), который позволяет нам разделять поведение в практическую, реальную структуру. Практически 85% веб-приложений на данный момент содержат в основе этот паттерн, в том или ином виде. Есть другие типы дизайн фреймворков, но этот пока что самый фундаментальный и легкий для понимания паттерн.

Почему он релевантен?

Масштабируемость и надежность

Легко улучшать, обновлять и дебагить.

Легко настроить

Даёт структуру и общее представление

Пример

Давайте посмотрим на короткий пример дизайн фреймворка MVC.

ES5

ES6

Как показано в примере выше, мы могли бы обыкновенно разделять view, model и controller в разные директории/файлы, в целях лучшей практики, но чтобы проиллюстрировать концепцию, мы вставили все это в один файл. Основными задачами дизайн фреймворка являются упрощение процесса разработки и поддержка устойчивой совместной среды разработки.

Async/await

Стоять и ждать, пока что-то не решится. Это дает способ поддерживать асинхронный процесс в более синхронном виде. Для примера, вам надо проверить верен ли пароль пользователя (то есть сравнить с существующим на сервере), перед тем как позволить пользователю войти в систему. Или возможно вы сделали REST API запрос и хотите, чтобы данные загрузились полностью перед тем как выставлять их на всеобщее обозрение.

Почему это релевантно?

Синхронные возможности

Контроль поведения

Сокращает “callback ад”

Пример

Давайте предположим, что вы хотите получить всех пользователей из rest API и показать эти результаты в JSON формате.

ES5

ES6

Чтобы использовать await, нам надо обернуть его во внутрь async функции, чтобы дать JS знать, что мы работаем с промисами. Как показано в примере, мы ожидаем двух вещей: response и users. Перед тем, как мы сможем конвертировать response в JSON формат, нам надо убедиться, что response подтянут, с другой стороны в конечном итоге мы будем конвертировать response, которого ещё нет, что вероятнее всего приведет к ошибке.

Замыкание

Замыкание это просто функция внутри ещё одной функции. Оно используется тогда, когда вам надо расширить поведение, например передать переменную, методы или массивы из внешней функции во внутреннюю. Мы также можем получить доступ к контексту, определенному во внешней функции из внутренней функции, но не в обратном порядке. Помните про области видимости и “поднятие”. О чем мы говорили выше.

Почему это релевантно?

Расширяет поведение

Полезно при работе с событиями

Пример

Давайте представим, что вы инженер-разработчик в Volvo и им нужна функция, которая просто показывает имя машины.

ES5

ES6

Функция showName() это замыкание, потому что она расширяет поведение функции showInfio() и также имеет доступ к переменной carType.

Callback

Callback это функция, которая выполняется после другой выполненной функции. Это также относится к call-after. В мире JavaScript, функция которая ожидает выполнения другой функции или отдачи значения(массив или объект) рассматривается как callback. Callback это способ делать асинхронные операции более синхронными (последовательный порядок).

Почему это релевантно?

Ожидает выполнения события

Дает возможности для синхронности

Практический способ связывать функционалы (Если A завершено, то затем выполняется B и так далее)

Дает структуру кода и контроль

Аккуратнее, вы возможно слышали про callback ад. На самом деле это означает, что у вас есть рекурсивная структура колбеков. (колбеки в колбэках в других колбэках и так далее).

Пример

Предположим, что Илон Маск в SpaceX нуждается в функционале, который будет зажигать двигатели Falcon Heavy 27 Merlin при нажатии на кнопку.

ES5

ES6

Обратите внимание, что он ждет свершения события(клика по кнопке) перед самим действием, то есть зажиганием двигателей. В вкратце, мы передали fireUpEngines() функцию как аргумент (callback) к pressButton() функции. Когда пользователь нажимает на кнопку, то зажигаются двигатели.

--

--

Stas Bagretsov

Надеюсь верую вовеки не придет ко мне позорное благоразумие. webdev/sports/books