JS-гайд: основные концепции JavaScript с примерами кода

Гайд по принципу Парето: 20% языка, которые нужны вам в 80% случаев. Только основные концепции JavaScript с примерами кода.

JS-гайд: основные концепции JavaScript с примерами кода

С момента появления JavaScript 20 лет назад он прошел долгий путь от скромного инструмента для простеньких анимаций до первой десятки рейтинга Tiobe.

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

Версии и стандарты

Язык JavaScript реализует стандарт ECMAScript, поэтому название его версий начинается с букв ES: ES6, ES2016, ES2018 и так далее. Версии имеют порядковый номер, а также нумеруются по году релиза. На данный момент последняя утвержденная версия – ES2017, он же ES8.

За развитие языка отвечает комитет TC39. Каждая новая фича должна пройти несколько этапов от предложения до стандарта.

Стайлгайды

Чтобы JavaScript-код был чистым и аккуратным, следует выработать систему соглашений и строго их придерживаться. Удобно использовать готовые стайлгайды, например, от Google или AirBnb.

Переменные

Имена переменных и функций в JavaScript должны начинаться с буквы, $ или символа подчеркивания. Они могут даже содержать эмодзи или иероглифы! Идентификаторы регистрозависимы: something и SomeThing – это разные переменные.

Нельзя использовать в качестве имен зарезервированные слова языка:

break
do
instanceof
typeof
case
else
new
var
catch
finally
return
void
continue
for
switch
while
debugger
function
this
with
default
if
throw
delete
in
try
class
enum
extends
super
const
export
import
implements
let
private
public
interface
package
protected
static
yield

Для создания переменной нужно использовать одно из трех ключевых слов: var, let или const.

// до ES6 были только var-переменные
var a = 'variable' 
var b = 1, c = 2 

// let-переменные можно изменять
let x = 10
x = 20

// const-переменные нельзя изменять
const y = 10
y = 20 // ошибка
  • var-переменные имеют контекстную область видимости и обладают свойством хойстинга (поднятия).
  • У let и const видимость блочная, и они не поднимаются.
  • неизменяемость const-переменных широко используется для обеспечения иммутабельности.

Выражения

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

// арифметические выражения преобразуются к числу
1 / 2
i++
i -= 2
i * 2

// строковые - к строке
"привет, " + "мир"
'hello, ' += 'world'

// логические - к булеву значению
a && b
a || b
!a

// литералы, переменные, константы - это тоже выражения
2
0.02
'something'
true
false
this 
undefined
i

// как и некоторые ключевые слова
function
class
function* 
yield 
/pattern/i
() // группирующие скобки

// выражения создания и инициализации
[]
{a: 1, b: 2}
new Person()
function() {}
a => a

// выражения вызова функций и методов
Math.paw(2, 3)
window.resize()

Примитивные типы данных

Числа

Все числа в JavaScript (даже целые) имеют тип float (число с плавающей точкой). Мы подготовили отдельную подробную статью об особенностях чисел и математических методах в JavaScript.

Строки

Строки – это последовательность символов в одинарных или двойных кавычках. Принципиальной разницы между ними нет.

'Одна строка'
"Другая строка"

// кавычки внутри строк необходимо экранировать
// двойные, если строка в двойных кавычках
"Ресторан \"У конца вселенной\""
// одинарные, если в одинарных
'I\'m Groot'

// строки могут содержать управляющие последовательности
"Первая строка\nВторая строка"

Для конкатенации строк используется оператор +:

"Hello, " + "world"

Строку можно заполнить символами до определенной длины (с начала или с конца):

padStart(targetLength [, padString])
padEnd(targetLength [, padString])

'test'.padStart(7) // ' test'
'test'.padStart(7, 'a') // 'aaatest'
'test'.padStart(7, 'abcd') // 'abctest'

'test'.padEnd(7) // 'test '
'test'.padEnd(7, 'a') // 'testaaa'
'test'.padEnd(7, 'abcd') // 'testabc'

В ES6 появился новый синтаксис для создания строк, допускающий интерполяцию выражений и многострочность:

// для шаблонных строк используются обратные кавычки
let str = `шаблонная строка`

let answer = 42
let a = `Ответ на главный вопрос жизни, вселенной и всего такого - ${answer}`
let b = `Дважды два равно ${ 2*2 }`
let c = `something ${ foo() ? 'x' : 'y' }`

let multiline = `Первая строка
вторая строка

третья строка`

Логические значения

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

// приводятся к false в логическом контексте
0
-0
NaN
undefined
null
'' // пустая строка

// остальные значения становятся true

А вот подборка зубодробительных особенностей логики JavaScript.

null

null означает отсутствие значения у переменной. У этой концепции JavaScript есть аналоги в других языках программирования, например, nil или None.

undefined

undefined означает, что переменная неинициализирована и не имеет значения.

Функции без директивы return возвращают именно undefined. Неинициализированные параметры функций также являются undefined.

Функции

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

function dosomething(foo) {
  return foo * 2
}

const dosomething = function(foo) {
  return foo * 2
}

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

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

Параметры

С версии ES6 функции поддерживают параметры по умолчанию:

const foo = function(index = 0, testing = true) { /* ... */ }
foo()

А в списке параметров можно оставлять замыкающую запятую:

const doSomething = (var1, var2,) => {
  //...
}
doSomething('test2', 'test2',)

Возвращаемое значение

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

Замыкания

Эффект замыканий основан на том, что в концепции JavaScript области видимости ограничены функциями. Это сложная тема, которую, тем не менее, необходимо понять для успешной работы. Мы посвятили ей большой отдельный материал (часть 1, часть 2).

this

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

const car = {
  brand: 'Ford',
  model: 'Fiesta',
  start: function() {
    console.log(`Started ${this.brand} ${this.model}`)
  }
}
car.start() // Started Ford Fiesta

this можно установить искусственно с помощью методов call, apply и bind:

const car1 = {
  maker: 'Ford',
  model: 'Fiesta',
  drive() {
    console.log(`Driving a ${this.maker} ${this.model} car!`)
  }
}
const anotherCar = {
  maker: 'Audi',
  model: 'A4'
}
car1.drive.bind(anotherCar)()
//Driving a Audi A4 car!

const car2 = {
  maker: 'Ford',
  model: 'Fiesta'
}
const drive = function(kmh) {
  console.log(`Driving a ${this.maker} ${this.model} car at ${kmh} km/h!`)
}
drive.call(car2, 100)
//Driving a Ford Fiesta car at 100 km/h!
drive.apply(car2, [100])
//Driving a Ford Fiesta car at 100 km/h!

Если функция вызывается не в контексте объекта, ее this равен undefined.

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

В ES6 появился новый вид функций, который полностью изменил вид JS-кода. На первый взгляд они очень просты:

const foo1 = () => {
  //...
}

// можно даже в одну строку
const foo2 = () => doSomething()

// с передачей параметра
const foo3 = param => doSomething(param)


// неявный возврат значения
const foo4 = param => param * 2
foo4(5) // 10

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

const a = {
  method: () => {
    console.log(this);
  }
}

a.method() // undefined

IIFE

Immediately Invoked Function Expressions – функции, которые выполняются сразу же после объявления.

(function () {
  console.log('executed')
})()

// executed

Генераторы

Особые функции, работу которых можно приостановить с помощью ключевых слов yield и возобновить позже. Это позволяет использовать совершенно новые концепции JavaScript-программирования.

// создание функции-генератора
function *calculator(input) {
    var doubleThat = 2 * (yield (input / 2))
    var another = yield (doubleThat)
    return (input * doubleThat * another)
}

// инициализация со значением 10
const calc = calculator(10)

// запуск калькулятора
calc.next() 

// возвращает { done: false, value: 5 },
// где value - это результат выражения input / 2

// продолжаем с новым значением
// оно подставляется вместо первого yield
calc.next(7)

// возвращает { done: false, value: 14 },
// где value - это вычисленное значение doubleThat

// продолжаем с новым значением
// оно подставляется вместо второго yield
calc.next(100)

// функция отрабатывает до конца и возвращает
// { done: true, value: 14000 }
// где value = 10 * 14 * 100

Поймите концепции JavaScript на примере объяснений или задач.

Массивы

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

Объекты

В ES2015 объектные литералы получили новые возможности:

  • Упрощение синтаксиса включения переменных.
    // до ES2015
    const something = 'y'
    const x = {
      something: something
    }
    
    // ES2015
    const something = 'y'
    const x = {
      something
    }
  • Прототипы и ключевое слово super.
    const anObject = { y: 'y', test: () => 'zoo' }
    const x = {
      __proto__: anObject,
      test() {
        return super.test() + 'x'
      }
    }
    x.test() //zoox
  • Динамические имена свойств.
    const x = {
      ['a' + '_' + 'b']: 'z'
    }
    x.a_b //z

Получение ключей и значений объекта

// массив значений собственных свойств объекта
const person = { name: 'Fred', age: 87 }
Object.values(person) // ['Fred', 87]

const people = ['Fred', 'Tony']
Object.values(people) // ['Fred', 'Tony']

// массив собственных свойств объекта в виде пар [ключ, значение]
const person = { name: 'Fred', age: 87 }
Object.entries(person) // [['name', 'Fred'], ['age', 87]]

const people = ['Fred', 'Tony']
Object.entries(people) // [['0', 'Fred'], ['1', 'Tony']]

// набор дескрипторов всех собственных свойств объекта
Object.getOwnPropertyDescriptors(object)

Циклы

for

const list = ['a', 'b', 'c']
for (let i = 0; i < list.length; i++) {
  console.log(list[i]) //value
  console.log(i) //index
}

for-each

const list = ['a', 'b', 'c']
list.forEach((item, index) => {
  console.log(item) //value
  console.log(index) //index
})
//index is optional
list.forEach(item => console.log(item))

do-while

const list = ['a', 'b', 'c']
let i = 0
do {
  console.log(list[i]) //value
  console.log(i) //index
  i = i + 1
} while (i < list.length)

while

const list = ['a', 'b', 'c']
let i = 0
while (i < list.length) {
  console.log(list[i]) //value
  console.log(i) //index
  i = i + 1
}

for-in

for (let property in object) {
  console.log(property) //property name
  console.log(object[property]) //property value
}

for-of

Сочетает лаконичность метода массивов forEach с возможностью прерывания цикла.

for (const v of ['a', 'b', 'c']) {
  console.log(v);
}

for (const [i, v] of ['a', 'b', 'c'].entries()) {
  console.log(i, v);
}

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

Спред-оператор

Дает возможность развернуть массив, объект или строку на элементы:

const a = [1, 2, 3]

// простое объединение массивов
const b = [...a, 4, 5, 6]

// простое копирование массива
const c = [...a]

// работает и с объектами
const newObj = { ...oldObj }

// и со строками
const hey = 'hey'
const arrayized = [...hey] // ['h', 'e', 'y']

// позволяет передавать в функции параметры-массивы
const f = (foo, bar) => {}
const a = [1, 2]
f(...a)

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

Дает возможность извлечь из объекта нужные значения и поместить их в именованные переменные:

const person = {
  firstName: 'Tom',
  lastName: 'Cruise',
  actor: true,
  age: 54, 
}
const {firstName: name, age} = person

// работает и с массивами
const a = [1,2,3,4,5]
[first, second, , , fifth] = a

ООП

В ООП-концепции JavaScript главное место занимают прототипы.

Прототипное наследование

Каждый объект имеет свойство prototype, в котором хранится ссылка на его прототип – своего рода хранилище методов и свойств. У прототипа в свою очередь есть свой прототип, к которому объект также имеет доступ "по цепочке".

// создание массива
const list = []

// его прототипом является прототип объекта Array
Array.isPrototypeOf(list) //true
list.__proto__ == Array.prototype // true

Классы

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

class Person {
  // метод constructor используется для инициализации экземпляра
  constructor(name) {
    this.name = name
  }
  hello() {
    return 'Hello, I am ' + this.name + '.'
  }
}

// наследование классов
class Actor extends Person {
  hello() {
    // super позволяет ссылаться на методы родительского класса
    return super.hello() + ' I am an actor.'
  }
}
var tomCruise = new Actor('Tom Cruise')
tomCruise.hello() // "Hello, I am Tom Cruise. I am an actor."

Для свойств класса можно создавать геттеры и сеттеры:

class Person {
  get fullName() {
    return `${this.firstName} ${this.lastName}`
  }

  set age(years) {
    this.theAge = years
  }
}

Исключения

Если при выполнении кода возникает неожиданная проблема, JavaScript выбрасывает исключение. Можно создавать исключения самостоятельно с помощью ключевого слова throw:

throw value

Для обработки нативных и кастомных исключений используется конструкция try-catch-finally.

try {
  // здесь выполняется код
} catch (e) {
  // здесь обрабатываются исключения, если они появились
} finally {
  // этот код выполняется при любом исходе
}

События

В браузерном JavaScript действует событийная модель программирования. Нужное событие отслеживается и обрабатывается с помощью специальной функции.

Каждое событие представлено объектом со множеством свойств и распространяется на веб-странице в три стадии:

  • Перехват (capturing). Событие спускается от корневого элемента к своей непосредственной цели. На этой стадии его можно перехватить.
  • Срабатывание на целевом элементе.
  • Всплытие - обратный путь от цели наверх.

Установить обработчик можно тремя способами:

// через атрибут html-элемента
<a href="site.com" onclick="dosomething();">Ссылка</a>


// через on-свойство
window.onload = () => {
  //window loaded
}

// с помощью метода addEventListener
window.addEventListener('load', () => {
  //window loaded
})

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

Цикл событий

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

// простая программа
const bar = () => console.log('bar')
const baz = () => console.log('baz')
const foo = () => {
  console.log('foo')
  bar()
  baz()
}
foo()

// вывод:
// foo
// bar
// baz

Асинхронность

Для более глубокого понимания темы мы подготовили материал по основным концепциям асинхронного программирования.

Коллбэки

Исторически асинхронность в JavaScript обеспечивалась с помощью обратных вызовов:

document.getElementById('button').addEventListener('click', () => {
  //item clicked
})

window.addEventListener('load', () => {
  //window loaded
})

setTimeout(() => {
  // runs after 2 seconds
}, 2000)

Однако при большом уровне вложенности код превращался в настоящий кошмар – ад коллбэков.

window.addEventListener('load', () => {
  document.getElementById('button').addEventListener('click', () => {
    setTimeout(() => {
      items.forEach(item => {
        //your code here
      })
    }, 2000)
  })
})

Промисы

Промисы были созданы, чтобы избавиться от этой вложенности. Вот, что они могут:

// без промисов
setTimeout(function() {
  console.log('I promised to run after 1s')
  setTimeout(function() {
    console.log('I promised to run after 2s')
  }, 1000)
}, 1000)

// с промисами
const wait = () => new Promise((resolve, reject) => {
  setTimeout(resolve, 1000)
})
wait().then(() => {
  console.log('I promised to run after 1s')
  return wait()
})
.then(() => console.log('I promised to run after 2s'))

Основы работы с промисами:

// создание
let done = true
const isItDoneYet = new Promise(
  (resolve, reject) => {
    if (done) {
      const workDone = 'Here is the thing I built'
      resolve(workDone)
    } else {
      const why = 'Still working on something else'
      reject(why)
    }
  }
)

// цепочка промисов
const status = (response) => {
  if (response.status >= 200 && response.status < 300) {
    return Promise.resolve(response)
  }
  return Promise.reject(new Error(response.statusText))
}
const json = (response) => response.json()
fetch('/todos.json')
  .then(status)
  .then(json)
  .then((data) => { console.log('Request succeeded with JSON response', data) })
  .catch((error) => { console.log('Request failed', error) })

// выполнение нескольких промисов
const f1 = fetch('/something.json')
const f2 = fetch('/something2.json')
Promise.all([f1, f2]).then((res) => {
    console.log('Array of results', res)
})
.catch((err) => {
  console.error(err)
})

// выполнение первого из нескольких промисов
const first = new Promise((resolve, reject) => {
    setTimeout(resolve, 500, 'first')
})
const second = new Promise((resolve, reject) => {
    setTimeout(resolve, 100, 'second')
})
Promise.race([first, second]).then((result) => {
  console.log(result) // second
})

Асинхронные функции

Сочетание промисов и генераторов – асинхронная абстракция более высокого уровня и с более простым синтаксисом.

function doSomethingAsync() {
    return new Promise((resolve) => {
        setTimeout(() => resolve('I did something'), 3000)
    })
}
async function doSomething() {
    console.log(await doSomethingAsync())
}
console.log('Before')
doSomething()
console.log('After')

// Вывод программы:
// Before
// After
// I did something - через 3 секунды

Асинхронные функции легко объединять в цепочки:

function promiseToDoSomething() {
    return new Promise((resolve)=>{
        setTimeout(() => resolve('I did something'), 10000)
    })
}
async function watchOverSomeoneDoingSomething() {
    const something = await promiseToDoSomething()
    return something + ' and I watched'
}
async function watchOverSomeoneWatchingSomeoneDoingSomething() {
    const something = await watchOverSomeoneDoingSomething()
    return something + ' and I watched as well'
}
watchOverSomeoneWatchingSomeoneDoingSomething().then((res) => {
    console.log(res)
})

Мы подготовили небольшой обзор плюсов и минусов async/await.

Таймеры

Таймеры – один из способов асинхронного выполнения кода.

let timerId = setTimeout(() => {
  // запустится через 2 секунды
}, 2000)
clearTimeout(timerId) // очистка таймаута

let intervalId = setInterval(() => {
  // будет запускаться каждые 2 секунды
}, 2000)
clearInterval(intervalId ) // очистка интервала

// рекурсивный setTimeout
const myFunction = () => {
  // do something
  setTimeout(myFunction, 1000)
}
setTimeout(
  myFunction()
}, 1000)

Модули

До ES2015 было по крайней мере три конкурирующих стандарта модулей: AMD, RequireJS и CommonJS, но теперь появился единый формат.

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

<script type="module" src="module.js"></script>
<script nomodule src="fallback.js"></script>

Импорт модуля осуществляется с помощью директивы import:

import * from 'mymodule'
import React from 'react'
import { React, Component } from 'react'
import React as MyLibrary from 'react'

А экспорт с помощью слова export:

export var foo = 2
export function bar() { /* ... */ }

Платформа Node.js продолжает использовать модули CommonJS.

Разделенная память и атомарные операции

Для создания многопоточных программ в браузере используются веб-воркеры и специальный протокол обмена сообщениями через события. В ES2017 можно создать массив общей памяти между веб-воркерами и их создателем, используя SharedArrayBuffer.

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

Подробности вы можете найти в спецификации предложения.

ES2018

Стандарт ES2018 вводит несколько новых языковых фич.

Асинхронная итерация

Новый цикл for-await-of позволяет асинхронно перебирать свойства итерируемого объекта:

for await (const line of readLines(filePath)) {
  console.log(line)
}

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

Promise.prototype.finally

Позволяет запускать определенный код независимо от успешного или неудачного завершения цепочки промисов.

fetch('file.json')
  .then(data => data.json())
  .catch(error => console.error(error))
  .finally(() => console.log('finished'))

Улучшение регулярных выражений

Опережающие (lookahead) и ретроспективные (lookbehind) проверки

// ?= ищет строку, за которой следует конкретная строка
/Roger(?=Waters)/
/Roger(?= Waters)/.test('Roger is my dog') //false
/Roger(?= Waters)/.test('Roger is my dog and Roger Waters is a famous musician') //true

// ?! выполняет обратную операцию
/Roger(?!Waters)/
/Roger(?! Waters)/.test('Roger is my dog') //true
/Roger(?! Waters)/.test('Roger Waters is a famous musician') //false

// ?<= ищет строку, перед которой идет конкретная строка
/(?<=Roger) Waters/
/(?<=Roger) Waters/.test('Pink Waters is my dog') //false
/(?<=Roger) Waters/.test('Roger is my dog and Roger Waters is a famous musician') //true

// ?<! выполняет обратную операцию
/(?<!Roger) Waters/
/(?<!Roger) Waters/.test('Pink Waters is my dog') //true
/(?<!Roger) Waters/.test('Roger is my dog and Roger Waters is a famous musician') //false

Паттерны для символов Юникода (и его отрицание)

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

/^\p{ASCII}+$/u.test('abc')   // true
/^\p{ASCII}+$/u.test('ABC@')  // true
/^\p{ASCII}+$/u.test('ABC?') // false

Подробнее обо всех свойствах вы можете прочитать в самом предложении.

Именованные группы захвата

const re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/
const result = re.exec('2015-01-02')
// result.groups.year === '2015';
// result.groups.month === '01';
// result.groups.day === '02';

Флаг s

s – сокращение от single line. Позволяет символу . (точка) совпадать с символами новой строки.

/hi.welcome/.test('hi\nwelcome') // false
/hi.welcome/s.test('hi\nwelcome') // true
А какие фичи и концепции JavaScript вы используете чаще всего?

Оригинал: The Complete JavaScript Handbook

МЕРОПРИЯТИЯ

Комментарии

ВАКАНСИИ

Добавить вакансию

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ