RUVDS.com corporate blog
Website development
JavaScript
25 February

12 концепций JavaScript, о которых нужно знать

Original author: Nick Scialli
Translation Tutorial
JavaScript — это сложный язык. Если вы, на любом уровне, занимаетесь JavaScript-разработкой, это значит, что вам жизненно необходимо понимать базовые концепции этого языка. В материале, перевод которого мы сегодня публикуем, рассмотрены 12 важнейших концепций JavaScript. Конечно, JavaScript-разработчику нужно знать гораздо больше, но без того, о чём мы будем сегодня говорить, ему точно не обойтись.



1. Переменные, хранящие значения и ссылки


Понимание того, как именно в JavaScript назначаются значения переменных крайне важно для тех, кто хочет писать правильно работающий код. Непонимание этого механизма ведёт к написанию программ, в которых значения переменных могут непреднамеренно меняться.

JavaScript, если некая сущность имеет один из примитивных типов (в частности — это типы Boolean, null, undefined, String и Number), всегда работает со значением этой сущности. То есть, в соответствующую переменную записывается именно значение. Если же речь идёт об объекте (это, например, типы Object, Array, Function), то, при назначении его переменной, в неё записывается ссылка на него, адрес, по которому он расположен в памяти.

Рассмотрим пример. В следующем фрагменте кода в переменную var1 записана строка. После этого в переменную var2 записано значение переменной var1. Так как переменная var1 имеет примитивный тип (String), то в var2 будет записана копия строки, имеющейся в var1. Это позволяет рассматривать var2 как переменную, полностью независимую от var1, хотя и хранящую то же значение, что и var1. Запись в var2 нового значения на var1 не влияет.

let var1 = 'My string';
let var2 = var1;
var2 = 'My new string';
console.log(var1);
// 'My string'
console.log(var2);
// 'My new string'

Теперь рассмотрим пример работы с объектами.

let var1 = { name: 'Jim' }
let var2 = var1;
var2.name = 'John';
console.log(var1);
// { name: 'John' }
console.log(var2);
// { name: 'John' }

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

2. Замыкания


Замыкание — это важный паттерн проектирования в JavaScript, который позволяет организовать защищённую работу с переменными. В следующем примере функция createGreeter() возвращает анонимную функцию, у которой есть доступ к предоставленному исходной функции аргументу greeting, содержащему строку Hello. Ссылка на эту анонимную функцию записывается в переменную sayHello. После этого, сколько раз бы мы ни вызывали функцию sayHello(), у неё всегда будет доступ к значению greeting. При этом доступ к greeting будет только у анонимной функции, ссылка на которую записана в sayHello.

function createGreeter(greeting) {
  return function(name) {
    console.log(greeting + ', ' + name);
  }
}
const sayHello = createGreeter('Hello');
sayHello('Joe');
// Hello, Joe

Это был очень простой пример. Если же рассмотреть нечто, более близкое к реальному миру, то можно представить себе, например, функцию для подключения к некоему API (назовём её apiConnect()), которой, при первом её вызове, передаётся ключ доступа к API. Эта функция, в свою очередь, возвращает объект, содержащий несколько методов, которые пользуются переданным apiConnect() ключом доступа к API. При этом ключ хранится в замыкании и при вызове этих методов упоминать его больше не требуется.

function apiConnect(apiKey) {
  function get(route) {
    return fetch(`${route}?key=${apiKey}`);
  }
  function post(route, params) {
    return fetch(route, {
      method: 'POST',
      body: JSON.stringify(params),
        headers: {
          'Authorization': `Bearer ${apiKey}`
        }
      })
  }
  return { get, post }
}
const api = apiConnect('my-secret-key');
// Использовать ключ доступа к API больше уже не нужно
api.get('http://www.example.com/get-endpoint');
api.post('http://www.example.com/post-endpoint', { name: 'Joe' });

3. Деструктурирующее присваивание


Если вы до сих пор не пользовались деструктурирующим присваиванием в JavaScript, то это пора исправить. Деструктурирующее присваивание представляет собой распространённый способ извлечения свойств объектов с использованием аккуратной синтаксической конструкции языка.

const obj = {
  name: 'Joe',
  food: 'cake'
}
const { name, food } = obj;
console.log(name, food);
// 'Joe' 'cake'

Если извлечённым свойствам нужно присвоить имена, отличающиеся от тех, которые они имеют в объекте, можно поступить так:

const obj = {
  name: 'Joe',
  food: 'cake'
}
const { name: myName, food: myFood } = obj;
console.log(myName, myFood);
// 'Joe' 'cake'

В следующем примере деструктурирование используется для аккуратной передачи значений, хранящихся в свойствах объекта person, функции introduce(). Это — пример того, как данная конструкция используется при объявлении функции для извлечения данных из переданного ей объекта с параметрами. Кстати, если вы знакомы с React, то вы, вероятно, уже такое видели.

const person = {
  name: 'Eddie',
  age: 24
}
function introduce({ name, age }) {
  console.log(`I'm ${name} and I'm ${age} years old!`);
}
console.log(introduce(person));
// "I'm Eddie and I'm 24 years old!"

4. Оператор spread


Оператор spread — это довольно простая конструкция, которая может показаться неподготовленному человеку непонятной. В следующем примере есть числовой массив, максимальное значение, хранящееся в котором, нам нужно найти. Мы хотим использовать для этого метод Math.max(), но он с массивами работать не умеет. Он, в качестве аргументов, принимает самостоятельные числовые значения. Для того чтобы извлечь из массива его элементы мы используем оператор spread, который выглядит как три точки.

const arr = [4, 6, -1, 3, 10, 4];
const max = Math.max(...arr);
console.log(max);
// 10

5. Оператор rest


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

function myFunc(...args) {
  console.log(args[0] + args[1]);
}
myFunc(1, 2, 3, 4);
// 3

6. Методы массивов


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

Здесь мы рассмотрим несколько таких методов, объединённых по принципу их схожести друг с другом. Надо отметить, что тут я расскажу далеко не обо всех методах массивов. Найти их полный список можно на MDN (кстати, это — мой любимый справочник по JavaScript).

▍Методы map(), filter() и reduce()


Методы массивов map(), filter() и reduce() позволяют трансформировать массивы или сводить массивы к одному значению (которое может быть объектом).

Метод map() возвращает новый массив, содержащий трансформированные значения обрабатываемого массива. То, как именно они будут трансформированы, задаётся в передаваемой этому методу функции.

const arr = [1, 2, 3, 4, 5, 6];
const mapped = arr.map(el => el + 20);
console.log(mapped);
// [21, 22, 23, 24, 25, 26]

Метод filter() возвращает массив элементов, проверяя значения которых функция, переданная этому методу, возвратила true.

const arr = [1, 2, 3, 4, 5, 6];
const filtered = arr.filter(el => el === 2 || el === 4);
console.log(filtered);
// [2, 4]

Метод reduce() возвращает некое значение, представляющее собой результат обработки всех элементов массива.

const arr = [1, 2, 3, 4, 5, 6];
const reduced = arr.reduce((total, current) => total + current);
console.log(reduced);
// 21

▍Методы find(), findIndex() и indexOf()


Методы массивов find(), findIndex() и indexOf() легко перепутать друг с другом. Ниже даны пояснения, помогающие понять их особенности.

Метод find() возвращает первый элемент массива, соответствующий заданному критерию. Этот метод, найдя первый подходящий элемент, не продолжает поиск по массиву.

const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const found = arr.find(el => el > 5);
console.log(found);
// 6

Обратите внимание на то, что в нашем примере заданному критерию соответствуют все элементы массива, следующие за тем, который содержит число 5, но возвращается лишь первый подходящий элемент. Этот метод весьма полезен в ситуациях, в которых, пользуясь для перебора и анализа массивов циклами for, такие циклы, при обнаружении в массиве нужного элемента, прерывают, используя инструкцию break.

Метод findIndex() очень похож на find(), но он, вместо того, чтобы возвращать первый подходящий элемент массива, возвращает индекс такого элемента. Для того чтобы лучше понять этот метод — взгляните на следующий пример, в котором используется массив строковых значений.

const arr = ['Nick', 'Frank', 'Joe', 'Frank'];
const foundIndex = arr.findIndex(el => el === 'Frank');
console.log(foundIndex);
// 1

Метод indexOf() очень похож на метод findIndex(), но он принимает в качестве аргумента не функцию, а обычное значение. Использовать его можно в том случае, если при поиске нужного элемента массива не нужна сложная логика.

const arr = ['Nick', 'Frank', 'Joe', 'Frank'];
const foundIndex = arr.indexOf('Frank');
console.log(foundIndex);
// 1

▍Методы push(), pop(), shift() и unshift()


Методы push(), pop(), shift() и unshift() применяются для добавления в массивы новых элементов и для извлечения из массивов уже имеющихся в них элементов. При этом работа производится с элементами, находящимися в начале или в конце массива.

Метод push() позволяет добавлять элементы в конец массива. Он модифицирует массив, и, после завершения работы, возвращает элемент, добавленный в массив.

let arr = [1, 2, 3, 4];
const pushed = arr.push(5);
console.log(arr);
// [1, 2, 3, 4, 5]
console.log(pushed);
// 5

Метод pop() удаляет из массива последний элемент. Он модифицирует массив и возвращает удалённый из него элемент.

let arr = [1, 2, 3, 4];
const popped = arr.pop();
console.log(arr);
// [1, 2, 3]
console.log(popped);
// 4

Метод shift() удаляет из массива первый элемент и возвращает его. Он тоже модифицирует массив, для которого его вызывают.

let arr = [1, 2, 3, 4];
const shifted = arr.shift();
console.log(arr);
// [2, 3, 4]
console.log(shifted);
// 1

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

let arr = [1, 2, 3, 4];
const unshifted = arr.unshift(5, 6, 7);
console.log(arr);
// [5, 6, 7, 1, 2, 3, 4]
console.log(unshifted);
// 7

▍Методы slice() и splice()


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

Метод splice() меняет содержимое массива, удаляя существующие элементы или заменяя их на другие элементы. Он умеет и добавлять в массив новые элементы. Этот метод модифицирует массив.

Следующий пример, если описать его обычным языком, выглядит так: нужно, в позиции массива 1, удалить 0 элементов и добавить элемент, содержащий b.

let arr = ['a', 'c', 'd', 'e'];
arr.splice(1, 0, 'b')

Метод slice() возвращает неглубокую копию массива, содержащую его элементы, начиная с заданной начальной позиции и заканчивая позицией, предшествующей заданной конечной позиции. Если при его вызове задана только начальная позиция, то он вернёт весь массив, начиная с этой позиции. Этот метод не модифицирует массив. Он лишь возвращает описанную при его вызове часть этого массива.

let arr = ['a', 'b', 'c', 'd', 'e'];
const sliced = arr.slice(2, 4);
console.log(sliced);
// ['c', 'd']
console.log(arr);
// ['a', 'b', 'c', 'd', 'e']

▍Метод sort()


Метод sort() выполняет сортировку массива в соответствии с условием, заданным переданной ему функцией. Эта функция принимает два элемента массива (например, они могут быть представлены в виде параметров a и b), и, сравнивая их, возвращает, в том случае, если элементы менять местами не надо, 0, если a нужно поставить по меньшему индексу, чем b — отрицательное число, а если b нужно поставить по меньшему индексу, чем a — положительное число.

let arr = [1, 7, 3, -1, 5, 7, 2];
const sorter = (firstEl, secondEl) => firstEl - secondEl;
arr.sort(sorter);
console.log(arr);
// [-1, 1, 2, 3, 5, 7, 7]

Если вы не можете, впервые ознакомившись с этими методами, их запомнить — ничего страшного. Самое главное это то, что вы теперь знаете о том, что умеют стандартные методы массивов. Поэтому, если вы и не можете сходу вспомнить особенности того или иного метода, то, что вы о нём знаете, позволит вам быстро найти то, что нужно, в документации.

7. Генераторы


Генераторы в JavaScript объявляют, используя символ звёздочки. Они позволяют задавать то, какое значение будет возвращено при очередном вызове метода next(). Генераторы могут быть рассчитаны на возврат ограниченного количества значений. Если подобный генератор возвратил все такие значения, то очередной вызов next() вернёт undefined. Можно создавать и генераторы, рассчитанные на возврат неограниченного количества значений с использованием циклов.

Вот генератор, рассчитанный на возврат ограниченного числа значений:

function* greeter() {
  yield 'Hi';
  yield 'How are you?';
  yield 'Bye';
}
const greet = greeter();
console.log(greet.next().value);
// 'Hi'
console.log(greet.next().value);
// 'How are you?'
console.log(greet.next().value);
// 'Bye'
console.log(greet.next().value);
// undefined

А вот генератор, рассчитанный на возврат бесконечного количества значений посредством цикла.

function* idCreator() {
  let i = 0;
  while (true)
    yield i++;
}
const ids = idCreator();
console.log(ids.next().value);
// 0
console.log(ids.next().value);
// 1
console.log(ids.next().value);
// 2
// и так далее...

8. Операторы проверки равенства (==) и строгого равенства (===) значений


Любому JS-разработчику чрезвычайно важно понимать разницу между операторами равенства (==) и строгого равенства (===). Дело в том, что оператор ==, перед сравнением значений, выполняет преобразование их типов (что может приводить к странным, на первый взгляд, последствиям), а оператор === преобразование типов не производит.

console.log(0 == '0');
// true
console.log(0 === '0');
// false

9. Сравнение объектов


Мне периодически приходится видеть, как новички в JS-программировании совершают одну и ту же ошибку. Они пытаются напрямую сравнивать объекты. Переменные, в которых «хранятся» объекты, содержат в себе ссылки на них, а не сами эти объекты.

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

const joe1 = { name: 'Joe' };
const joe2 = { name: 'Joe' };
console.log(joe1 === joe2);
// false

При этом в следующем примере оказывается, что joe1 равно joe2 так как обе переменные хранят ссылку на один и тот же объект.

const joe1 = { name: 'Joe' };
const joe2 = joe1;
console.log(joe1 === joe2);
// true

Один из методов настоящего сравнения объектов заключается в их предварительном преобразовании в формат JSON-строк. Правда, у такого подхода есть одна проблема, которая заключается в том, что в полученном строковом представлении объекта не гарантируется определённый порядок следования его свойств. Более надёжный способ сравнения объектов заключается в использовании специальной библиотеки, содержащей средства для глубокого сравнения объектов (например — это метод isEqual() библиотеки lodash).

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

10. Функции обратного вызова


Функции обратного вызова — это довольно простая концепция JavaScript, с которой у новичков иногда возникают сложности. Рассмотрим следующий пример. Здесь функция console.log (именно так — без скобок) передаётся функции myFunc() в качестве функции обратного вызова. Эта функция устанавливает таймер, по срабатыванию которого вызывается console.log() и переданная функции myFunc() строка выводится в консоль.

function myFunc(text, callback) {
  setTimeout(function() {
    callback(text);
  }, 2000);
}
myFunc('Hello world!', console.log);
// 'Hello world!'

11. Промисы


После того, как вы освоите функции обратного вызова и начнёте всюду их использовать, вы очень скоро можете обнаружить себя в так называемом «аду коллбэков». Если вы и правда там оказались — взгляните на промисы. Асинхронный код можно обернуть в промис, и, после его успешного выполнения, сообщить системе об успешном разрешении промиса, вызвав соответствующий метод, а если что-то пойдёт не так — вызвать метод, указывающий на это и отклонить промис. Для того чтобы обработать результаты, возвращаемые промисом, воспользуйтесь методом then(), а для обработки ошибок — методом catch().

const myPromise = new Promise(function(res, rej) {
  setTimeout(function(){
    if (Math.random() < 0.9) {
      return res('Hooray!');
    }
    return rej('Oh no!');
  }, 1000);
});
myPromise
  .then(function(data) {
    console.log('Success: ' + data);
   })
   .catch(function(err) {
    console.log('Error: ' + err);
   });
   
// Если Math.random() вернёт значение, меньшее, чем 0.9, в консоль попадёт следующее:
// "Success: Hooray!"
// Если Math.random() вернёт значение, большее, чем 0.9, или 0.9, в консоль попадёт следующее:
// "Error: On no!"

12. Конструкция async/await


После того, как вы поработаете с промисами, то вам, вполне возможно, захочется чего-то большего. Например — освоить конструкцию async/await. Она представляет собой «синтаксический сахар» для промисов. В следующем примере мы создаём, с помощью ключевого слова async, асинхронную функцию, и в ней, пользуясь ключевым словом await, организуем ожидание выполнения промиса greeter.

const greeter = new Promise((res, rej) => {
  setTimeout(() => res('Hello world!'), 2000);
})
async function myFunc() {
  const greeting = await greeter;
  console.log(greeting);
}
myFunc();
// 'Hello world!'

Итоги


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

Уважаемые читатели! Какие ещё концепции JavaScript вы добавили бы в эту статью?


+31
51.5k 395
Comments 61
Top of the day