Skip to content

Latest commit

 

History

History
2224 lines (1803 loc) · 114 KB

JavaScript.md

File metadata and controls

2224 lines (1803 loc) · 114 KB

Основы JavaScript

Типизация

JavaScript является типизированным языком и имеет динамическую, слабую, неявную типизацию.

Статическая и динамическая

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

Пример языков со статической типизацией: Java, C#.

/* Java */
public class Notes {
  public static void main(String []args){
    int number = 1; // числовой тип
    number = true; // error: incompatible types: boolean cannot be converted to int
  }
}

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

Пример языков с динамической типизацией: Python, JavaScript.

/* JavaScript */
let a; // тип неизвестен
a = 1; // числовой тип
a = true; // логический тип

Слабая и сильная

При слабой (нестрогой) типизации автоматически выполняется множество неявных преобразований типов даже при условии неоднозначности преобразования или возможности потери точности данных.

Пример языка со слабой типизацией: JavaScript.

/* JavaScript */
console.log(1 + [] + {} + 'notes'); // "1[object Object]notes"
console.log(1 - []); // 1

При сильной (строгой) типизации в выражениях не разрешено смешивать различные типы. Автоматическое неявное преобразование не производится.

Пример языков с сильной типизацией: Java, Python.

Например, нельзя сложить число и массив.

/* Java */
public class Notes {
  public static void main(String []args){
    int number = 17;
    int array[] = new int[3];
    System.out.println(number + array); // error: bad operand types for binary operator '+'
  }
}

Явная и неявная

При явной типизации тип новых переменных, функции, их аргументов и возвращаемых ими значений нужно задавать явно.

Пример языков с явной типизацией: C++, C#.

/* C++ */
int sum(int a, int b) {
    return a + b;
}

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

Пример языка с неявной типизацией: JavaScript.

let a; // неизвестно, какого типа будет значение переменной
function fn (arg) { /* .. */ } // неизвестно, какого типа параметр функции и что она возвращает

Типы данных и переменные

Переменная состоит из имени и выделенной под это имя области памяти.

Имя переменной может содержать буквы, цифры, $, _.
Регистр важен (ALL и all - разные переменные).

Константы принято называть в UPPERCASE: ANY_NAME.

6 примитивных типов и объект

  • number 1, 2.17, NaN, Infinity
  • string 'str', "str"
  • boolean true, false
  • null null
  • undefined undefined
  • symbol Symbol(str)
  • object {}

Значение null не является «ссылкой на нулевой адрес/объект» или чем-то подобным.
Значение null специальное и имеет смысл «ничего» или «значение неизвестно».
Значение undefined означает «переменная не присвоена».

Символ (Symbol) — уникальный и неизменяемый тип данных, используемый в качестве идентификатора для свойства объекта.

Symbol('notes') === Symbol('notes'); // false

Символы являются неперечисляемыми (not enumerable), что делает их недоступными при переборе свойств.

const symbol = Symbol('notes');

const foo = { [symbol]: 'notes' };
console.log(Object.keys(foo)); // []
console.log(Object.getOwnPropertyNames(foo)); // []

console.log(foo.notes); // undefined
console.log(foo[Symbol('notes')]); // undefined (символы уникальны)
// но
console.log(foo[symbol]); // notes

Способы объявить переменную

Блок (Block Statement) — всё, что лежит внутри фигурных скобок {}.

Например, конструкции if-else, while, switch, try-catch, циклы for, функции содержат блоки. Тем не менее можно использовать блоки и без этих конструкций.

Переменная let имеет блочную область видимости, то есть её нельзя использовать за пределами блока, в котором она объявлена.

{
  let foo = 1;
  foo = 7;
  console.log(foo); // 7;
}
console.log(foo); // ReferenceError: foo is not defined
for (let i = 0; i < 10; i++) {
  /* ... */
}
console.log(i); // ReferenceError: i is not defined

Переменную let нельзя использовать до её инициализации.

console.log(foo); // ReferenceError: Cannot access 'foo' before initialization
let foo = 'notes';

Переменную let нельзя объявить дважды с одним и тем же именем.

let foo = 1;
let foo = 7; // SyntaxError: Identifier 'foo' has already been declared

Переменная const имеет те же свойства, что и переменная let, но вдобавок её значение нельзя переопределить.

const foo = 1;
foo = 7; // TypeError: Assignment to constant variable.

Переменная var является предком переменных let и const и не обладает их ограничениями.

Переменная var не имеет блочной области видимости.

if (true) {
  var foo = 1;
}
console.log(foo);
for (var i = 0; i < 10; i++) {
  /* ... */
}
console.log(i); // 10

Переменную var можно использовать до её инициализации значением. Такое поведение называется всплытием. Всплывает только объявление переменной, а её временным значением (до инициализации) становится undefined.

console.log(foo); // undefined
var foo = 'notes';
console.log(foo); // notes

Переменную var можно объявить дважды (redeclaration) с тем же именем.

var foo = 1;
console.log(foo); // 1
var foo = 7;
console.log(foo); // 7

Переменная var, находящаяяся вне каких-либо функций, размещается в глобальном объекте. Например, это может быть window.

var foo = 'notes';
console.log(this.foo); // 'notes'
console.log(window.foo); // 'notes' (если window является this)
this.foo = 'note';
console.log(foo); // 'note'

Глобальная переменная не является переменной как таковой, а является свойством глобального объектаJavaScript им является window, в NodeJSglobal). Поэтому, в отличие от остальных переменных, её можно удалить оператором delete.

foo = 1;
window.foo = 7;
console.log(foo); // 7
delete foo;
console.log(foo); // ReferenceError: foo is not defined

Преобразование типов

Бинарный оператор + приводит значения либо к числам и совершает сложение, либо к строкам и совершает конкатенацию.

Виды преобразований:

  • Строковое.
  • Числовое.
  • Логическое.

Явное и неявное преобразование

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

console.log(1 + [] + {} + 'notes'); // "1[object Object]notes"
console.log(1 - []); // 1

Если преобразование типов задаётся разработчиком вручную (явно), то оно называется явным.

В JavaScript есть множество способов явно преобразовать тип.

/* приведение к числу */
console.log(+'017.6'); // 17.6 (при помощи унарного оператора "+")
console.log(Number('')); // 0 (при помощи Number())
console.log(parseInt('11.1abc', 10)); // 11.1 (при помощи parseInt)
console.log(parseFloat('11.1abc', 10)); // 11.1 (при помощи parseFloat)

/* приведение к логическому значению при помощи Boolean() */
console.log(Boolean('notes')); // true
console.log(Boolean('')); // false
console.log(Boolean(-1)); // true

/* приведение к логическому значению при помощи String() */
console.log(String(null)); // 'null'
console.log(String({})); // '[object Object]'

Пример явного преобразования в Java.

/* Java */
class Notes {
  public static void main(String[] args) {
    double foo = 11.1;
    int bar = (int)a; // приведение double к int
    System.out.println(bar); // 11
  }
}

Преобразование объекта к примитивному значению

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

Каждый объект имеет метод valueOf(), который возвращает примитивное значение объекта. По умолчанию метод возвращает сам объект (непримитивное значение).

const a = {};
console.log(a.valueOf()); // {}
console.log(a.valueOf() === a); // true

const b = [];
console.log(b.valueOf()); // []
console.log(b.valueOf() === b); // true

function c() {}
console.log(c.valueOf()); // ƒ c() {}
console.log(c.valueOf() === c); // true

Метод valueOf() можно переопределить. Например, он переопределён у Date.

const a = { valueOf: () => 7 };
console.log(a); // { valueOf: ƒ }
console.log(a.valueOf()); // 7

const date = new Date();
console.log(date.valueOf()); // 1573833066283

Помимо valueOf(), каждый объект имеет метод toString(), возвращающий строковое представление объекта.

const a = {};
console.log(a.toString()); // "[object Object]"

console.log([].toString()); // ""
console.log([1, 2, 3].toString()); // "1,2,3"

const date = new Date();
console.log(date.toString()); // "Sun Nov 17 2139 19:13:50 GMT+0300 (Moscow Standard Time)"

function fn() { /* ... */ }
console.log(fn.toString()); // "function fn() { /* ... */ }"

Метод toString() также можно переопределить.

const a = {};
a.toString = () => 'a{}';
console.log(a.toString()); // 'a{}'

Объект приводится к примитивному значению при помощи функции toPrimitive(argument, preferredType), работающей по следующему алгоритму.

  • Если valueпримитивное значение (number, string, boolean, null, undefined), то вернуть его.
  • Иначе вызвать value.valueOf(). Если результатпримитивное значение, то вернуть его.
  • Иначе вызвать value.toString(). Если результатпримитивное значение, то вернуть его.
  • Выбросить исключение TypeError('Cannot convert object to primitive value').

По умолчанию параметр preferredType имеет занчение 'number'. Если передать 'string', то шаги алгоритма с valueOf() и toString() меняются местами.

const isPrimitive = argument => !['object', 'function'].includes(typeof argument) || argument === null;

const toPrimitive = (argument, preferredType = 'number') => {
  if (isPrimitive(argument)) {
    return argument;
  }
  
  if (preferredType === 'number') {
    /* сперва valueOf(), затем toString() */
    if (argument.valueOf && isPrimitive(argument.valueOf())) {
      return argument.valueOf();
    }

    if (argument.toString && isPrimitive(argument.toString())) {
      return argument.toString();
    }
  } else if (preferredType === 'string') {
    /* сперва toString(), затем valueOf() */
    if (argument.toString && isPrimitive(argument.toString())) {
      return argument.toString();
    }
    
    if (argument.valueOf && isPrimitive(argument.valueOf())) {
      return argument.valueOf();
    }
  }
  
  throw new TypeError('Cannot convert object to primitive value');
};

Преобразование к строке

Преобразование значения к типу string производится функцией ToString(argument) по следующим правилам.

  • Если значение argument имеет тип string, то вернуть значение.
  • Иначе, если argument имеет тип Symbol, выбросить TypeError.
  • Иначе, если argument имеет примитивный тип number, boolean, undefined, null, обернуть его в строку и вернуть: "null", "undefined", "1.2", "NaN", "true", "false".
  • Иначе, если argument имеет тип Object, вернуть результат ToString(ToPrimitive(argument)).
const ToString = (argument) => {
  if (typeof argument === 'string') {
    return argument;
  }

  if (typeof argument === 'symbol') {
    throw new TypeError('Cannot convert a Symbol value to a string');
  }
  
  const allowedPrimitives = ['number', 'boolean', 'undefined'];
  if (allowedPrimitives.includes(typeof argument) || argument === null) {
    return `${argument}`;
  }

  if (!isPrimitive(argument)) {
    return ToNumber(ToPrimitive(argument));
  }

  throw new TypeError('Cannot convert argument to string');
}

Преобразование к логическому значению

Преобразование значения к типу boolean производится функцией ToBoolean(argument) по следующим правилам.

  • Если argument имеет тип boolean, то вернуть значение.
  • Иначе, если argument равно undefined, null, 0, NaN, "" (пустая строка), то вернуть false.
  • В остальных случаях (Object, Symbol, числа кроме 0 и непустые строки) вернуть true.
const ToBoolean = (argument) => {
  if (typeof argument === 'boolean') {
    return argument;
  }

  if ([undefined, null, 0, NaN, ''].includes(argument)) {
    return false;
  }
  
  return true;
}

Преобразование к числу

Преобразование значения к типу number производится функцией ToNumber(argument) по следующим правилам.

  • Если значение argument имеет тип number, то вернуть значение.
  • Иначе, если argument имеет тип boolean, вернуть 1 (true) или 0 (false).
  • Иначе, если argument имеет тип string, попытаться преобразовать строку к числу или вернуть NaN в случае неудачи. Пустая строка приводится к нулю.
  • Иначе, если argument имеет тип Symbol, выбросить TypeError.
  • Иначе, если argument равно undefined, вернуть NaN.
  • Иначе, если argument равно null, вернуть 0.
  • Иначе, если argument имеет тип Object, вернуть результат ToNumber(ToPrimitive(argument)).
const ToNumber = (argument) => {
  if (typeof argument === 'number') {
    return argument;
  }

  if (typeof argument === 'boolean') {
    return argument ? 1 : 0;
  }
  
  if (typeof argument === 'string') {
    return argument === '' ? 0 : parseFloat(argument, 10);
  }
  
  if (typeof argument === 'symbol') {
    throw new TypeError('Cannot convert a Symbol value to a number');
  }
  
  if (argument === undefined) {
    return NaN;
  }
  
  if (argument === null) {
    return 0;
  }

  if (!isPrimitive(argument)) {
    return ToNumber(ToPrimitive(argument));
  }
  
  throw new TypeError('Cannot convert argument to number');
}

Оператор нестрогого равенства == производит неявное преобразование типа к числу (если оба операнда не являются строками).

Интересный пример.

[] == ![] // true
// оператор ! имеет больший приоритет, чем ==, поэтому он вызовется раньше
// ![] --> !toBoolean([]) --> !true --> false --> 0
[] == 0
// toPrimitive([]) --> [].valueOf() ~ [] (не подходит) --> [].toString() ~ '' --> '' --> 0
0 == 0 // true

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

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

Видимость (visibility), доступность (accessibility) переменных отражает понятие область видимости, скоуп (англ. scope).

Существует два типа области видимости: глобальная и локальная.

Глобальная область видимости (англ. Global Scope) - это область видимости всей программы (всего скрипта). В браузере глобальная область видимости представлена объектом window. На NodeJS-сервере глобальная область видимости представлена объектом global.

Локальная область видимости (англ. Local Scope) - это область видимости любой функции, объявленной в скрипте. Каждая функция при своём вызове создаёт локальную область видимости. Переменные, определённые внутри функции, недоступны извне.

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

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

const fn = () => {
  var foo = 1;
}
console.log(foo); // ReferenceError: foo is not defined
fn();
console.log(foo); // ReferenceError: foo is not defined

Замыкание (англ. closure) — это функция вместе с ссылками на её окружение, называемое лексическим окружением (Lexical Environment). По сути говоря, любая функция в JavaScript представляет собой замыкание.

Примеры

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

Переменная var

Объявление переменной var всплывает в самое начало скрипта, что позволяет использовать имя этой переменной до её объявления. Тем не менее, значение переменной var изменяется в области видимости лишь при инициализации.

/* main.js */

// >>> Global Scope: { a: undefined }
var a = 5;
// >>> Global Scope: { a: 5 }

Переменная const

Переменная let

Блок кода {}

Всплытие (hoisting)

В JavaScript переменная может быть использована перед тем, как она была определена (declared) в коде.

Всплытие (англ. hoising) — поведение JavaScript, размещающее объявления (англ. declarations) вверху текущей области видимости (англ. current scope).

foo = 3;
console.log(foo); // 3
var foo;

При попытке использования необъявленной переменной выдаётся ошибка.

console.log(bar); // ReferenceError: bar is not defined

Всплывают (англ. hoist) только сами объявления (англ. declarations), но не присвоенные им значения (англ. initializations).
Это связано с тем, что переменная создаётся в области видимости на первом этапе интерпретации, а инициализируется значением на втором этапе.

console.log(foo); // undefined
var foo = 3;
console.log(bar); // undefined
bar = 'notes';

Всплывают переменные var и глобальные переменные, а let и const не всплывают: выдаётся ошибка.

console.log(bar); // ReferenceError: Cannot access 'bar' before initialization
let bar = 3;

Контекст, контекст выполнения и ключевое слово this

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

О контексте в литературе

Контекстом (англ. context) называют совокупность фактов и обстоятельств, в окружении которых происходит некоторое событие, существует некоторое явление или некоторый объект.

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

  1. Пример события. Например, исторические события - Битва под Оршей, принятие Билля о правах.
  2. Пример объекта. Например, историческая личность - Эммелин Панкхёрст и исторически значимое место - Собор Парижской Богоматери.
  3. Пример явления. Например, северное сияние. Сбор информации на любую тему выше равносилен наполнению контекста, соответствующего этой теме.

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

Например, рассмотрим следующий пример описания человека.

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

Если бы компьютер мог выделять контекст из написанного, то он бы сделал это примерно следующим образом:

const he = {
  name: 'Джек Лондон',
  sex: 'мужчина',
  profession: 'писатель',
  genres: ['приключение', 'роман'],
  traits: ['мужественный', 'отважный', 'целенаправленный', 'решительный'],
}

С каждым новым предложением этот контекст бы продолжил наполняться новыми деталями.

Вырывание из контекста

Если убрать первое сообшение из примера выше, то тогда не понятно, о ком идёт речь. В таком случае говорят о нарушении целостности контекста или вырывании из контекста.

Тем, кому довелось знать его лично, описывали его как мужественного, отважного, целенаправленного и решительного человека...

Ещё пример

Например, контекстом текущего документа Notes/JavaScript.md является язык JavaScript и всё, что с ним тесно связано. И в то же время любой другой язык программирования (скажем, Java) находится вне рассматриваемого контекста. Ещё пример: в контексте данной главы рассматриваются понятия "контекст", ключевое слово this и не рассматриваются типы данных.

Контекст выполнения в JavaScript

Если вернуться к JavaScript, то под "контекстом" обычно подразумевают контекст выполнения (англ. Execution Context, EC).

Всего можно выделить два контекста выполнения:

  1. Контекст выполнения функции (англ. Function Execution Context, FEC).
  2. Глобальный контекст выполнения (англ. Global Execution Context, GEC).

Глобальный контекст выполнения

Операторы

Оператор typeof

Оператор typeof возвращает тип аргумента.
Результатом действия оператора является строка.

В JavaScript массивы и функции так же являются объектами, но оператор typeof имеет тип "function" для удобства.

typeof undefined // "undefined"
typeof 0 // "number"
typeof true // "boolean"
typeof "foo" // "string"
typeof Symbol("foo"); // "symbol" 
typeof {} // "object"
typeof [] // "object"
typeof null // "object" (врождённая ошибка языка)
typeof function(){} // "function"

Оператор typeof cчитает null объектом, что является врождённой ошибкой JavaScript, которую не исправляют в целях поддержки совместимости с предыдущими версиями.
Тем не менее, null не является объектом как таковым: это примитивное значение.

console.log(typeof null); // "object"
console.log(null instanceof Object); // false

Оператор typeof считает, что NaN (Not-a-Number) является "number". Это объясняется тем, что NaN появляется только при операциях с числами, а также содержится в Number.NaN (как и метод Number.isNaN(value)).

console.log(typeof NaN); // "number"

Оператор typeof и undefined

Раньше в JavaScript undefined являлся названием глобальной переменной, по умолчанию не имеющей значения. То есть переменная undefined имела примитивное значение undefined, но его можно было переопределить.

var foo = {};
console.log(foo.prop === undefined); // true (нет такого свойства)
undefined = 17;
console.log(foo.prop === undefined); // false

Из-за изменяемости (mutability) undefined не использовали явно, а получали другим способом.

Например, typeof foo.prop === 'undefined'.

Сейчас такой ошибки нет.

Оператор void

Оператор voidунарный оператор, выполнящий принимаемое выражение и возвращающий undefined.
Его можно использовать со скобками и без:

void 3 // undefined
void(3) // undefined
void(3 == '3') // undefined
void 3 == '3';   // undefined == '3' --> false

Преобразование Function Declaration в Function Expression для самовызывающихся функций (IIFE):

(function() { /* ... */ })()
// эквивалентно
void function(){ /* ... */ }()
// дважды SyntaxError (название функции и круглые скобки), если
function(){ /* ... */ }()

Избегание явного использования undefined, а также краткий способ его записать (иногда можно встретить в минифицированном коде):

if (field === void 0)

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

const onClick = () => void this.setState({ isClicked: true });

Здесь стоит обратить внимание, что код ниже выдаст ошибку: приоритет => ниже, чем у void.

const onClick = void () => this.setState({ isClicked: true }); // SyntaxError: Malformed arrow function parameter list

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

Оператор запятая

Оператор запятая (comma operator) выполняет каждый из его операндов слева направо и возвращает значение последнего. Операнды могут быть представлены выражениями.

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

let foo = 2, 3; // SyntaxError: Unexpected number

Ошибка выше связана с тем, что оператор присваивания = выполняется раньше, чем оператор запятая, поскольку имеет более высокий приоритет. Впереди стоит let, применяющийся ко всем операндам оператора запятая: let foo = 2 и let 3 (название переменной не может быть числом).

Избежать ошибки можно при помощи оператора группировки ( ), имеющего самый высокий приоритет среди операторов.

let bar = (2, 3);
console.log(bar); // 3 (последний операнд)

К слову, пример ниже отработает без ошибок. В первом операнде foo = 2 происходит присвоение значения глобальной переменной, во втором просто возвращается 3.

foo = 2, 3;
console.log(foo); // 2

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

const getDataType = data => typeof data;
// заменяем на
const getDataType = data => (console.log(data), typeof data);

getDataType('notes') // можно увидеть значение 'notes' в консоли

Другой пример: выполнить операцию над чем-то и сразу вернуть её результат.

const array = ['n', 'o', 't', 'e'];
console.log(array.push('s')) // 5 (вернулась длина массива после добавления элемента)

// хотим вернуть новый массив:
const array = ['n', 'o', 't', 'e'];
const push = (arr, val) => (arr.push(val), arr);
console.log(push(array, 's')); // ['n', 'o', 't', 'e', 's']

Здесь стоит ещё раз отметить важность оператора группировки.

const push = (arr, val) => arr.push(val), arr; // SyntaxError: Missing initializer in const declaration

Код выше воспринимается интерпретатором как const push = /* ... */ и const arr (константы обязаны иметь какое-то значение при создании). С let ошибки бы не было.

Оператор delete

Оператор deleteунарный оператор, удаляющий свойство из объекта (массива, функции и других наследников Object).

При успешном удалении delete возвращает true (в том числе, если удаляется несуществующее свойство), false иначе.

const foo = { a: 1, b: 7 };
console.log(foo); // { a: 1, b: 7 }
delete foo.a; // true
console.log(foo); // { b: 7 }

При работе с массивами, delete создаёт дыры в них.

const bar = [1, 2, 3];
delete bar[0]; // true
console.log(bar); // [empty, 2, 3]
console.log(bar.length) // 3
delete bar[2]; // true
console.log(bar); // [empty, 2, empty]
console.log(bar.length) // 3

Оператор delete может удалить глобальную переменную, поскольку на самом деле она является свойством глобального объекта window.

foo = 'notes';
console.log(window.foo); // 'notes'
delete foo; // true
console.log(window.foo); // undefined

Оператор delete не может удалять переменные var, let, const и функции.

var foo = 'notes';
delete foo; // false
console.log(foo); // 'notes'

function bar () {}
delete bar; // false
console.log(bar); // ƒ bar () {}

Оператор delete не связан с очисткой памяти. Очиста памяти осуществляется сборщиком мусора при разрыве ссылок.

Оператор нулевого слияния ??

Оператор нулевого слияния ?? (англ. Nullish coalescing operator) — логический оператор, возвращающий значение правого операнда, если значение левого операнда содержит null или undefined, иначе возвращается значение левого операнда.

// right operator
(null ?? true) === true
(undefined ?? true) === true

// everything else - left operand
('' ?? true) === ''
(0 ?? true) === 0
(false ?? true) === false
(NaN ?? true) // NaN
('hi' ?? true) === 'hi'
(-1 ?? true) === -1
([] ?? true) // []
({} ?? true) // {}
...

Оператор нулевого присваивания ??=

Истинноподобные значения (англ. truthy values) - значения, эквивалентные true при их приведении к логическому типу (явному Boolean(x) и !!x и неявному if (x), x &&, x ||):

  • true
  • 17, -17, 1.7, 17n (любые ненулевые числа)
  • Infinity, -Infinity (бесконечности)
  • ' ', "0", 'hi' (непустые строки)
  • new Boolean(false), {}, [], function foo(){} (любые объекты)

Интересный пример

new Boolean(false) === true // false
new Boolean(false) === false // false
new Boolean(false) == true // false
new Boolean(false) == false // true

// Объяснение: при сравнении берётся `.valueOf()` объекта класса `Boolean`
(new Boolean(false)).valueOf() // false 

// ещё примеры с неявным использованием `.valueOf()`:
(new Number(0)) == false // 0 == false
(new String('')) == false // '' == false

Ещё один интересный пример

(new String('')) && 0 // 0
// берётся `valueOf`: ''
// приводится к `Boolean`: false
// ложноподобное значение пропускается
// берётся следующее значение: 0

Ложноподобные значения (англ. falsy values) - значения, эквивалентные false при их приведении к логическому типу (явному Boolean(x) и !!x и неявному if (x), x &&, x ||):

  • false
  • 0 (ноль), -0 (отрицательный ноль), 0n (BigInt ноль)
  • '', "", (пустая строка)
  • null
  • undefined
  • NaN
Boolean(false) === false
Boolean(0) === false
Boolean('') === Boolean('') === Boolean(``) === false
Boolean(null) === false
Boolean(undefined) === false
Boolean(NaN) === false

Значения, похожие на null (англ. nullish values) - это null и undefined.

Оператор нулевого присваивания ??= (англ. Nullish coalescing assignment, Logical nullish assignment) — логический оператор, присваивающий правый операнд к левому только если левый операнд равняется null или undefined.

let x = null;
x ??= 'foo'
x ??= 'bar'
console.log(x) // 'foo'

let y; // undefined
y ??= 0
y ??= 1
console.log(y) // 0

let z; // undefined
z ??= undefined
z ??= null
z ??= false
z ??= true
console.log(z) // false

Оператор присваивания логического "И"

Оператор присваивание логического И

Объекты

Перечисление свойств объекта

Цикл for...in перебирает все несимвольные (non-Symbol) перечисляемые свойства (enumerable properties) объекта, включая свойства из цепочки прототипов (prototype chain).

Метод Object.keys(obj) возвращает массив названий всех собственных (own) перечисляемых свойств объекта obj в том же порядке, в котором они обходились бы циклом for..in. Поскольку свойства собственные, цепочка прототипов не включается в перечисление.

Метод Object.getOwnPropertyNames(obj) возвращает массив названий всех собственных свойств объекта obj.

Является ли объектом

const isObject = value => typeof value === 'object' && !Array.isArray(value) && value !== null;

Такая реализация обусловлена следующим поведением оператора typeof.

typeof({}) === 'object' // true
typeof([]) === 'object' // true
typeof(() => {}) === 'function' // true
typeof(null) === 'object' // true

Можно проще.

({}) instanceof Object // true

Клонирование объектов

В JavaScript объекты и массивы (тоже являющиеся объектами) передаются по ссылке (by reference).

Существует множество способов клонировать объект (object clone), среди которых есть плохие и хорошие.

Плохие способы клонирования

Клонирование через оператор присваивания = означает запись ссылки на объект в новую переменную. Если изменить эту переменную (не заменить полностью, а изменить поля), то изменится и оригинальный объект.

const obj = { a: 7 };
const copy = obj; // передача ссылки
console.log(foo === copy); // true
copy.a = 3;
console.log(foo.a) // 3;

Следует избегать поведения, при котором изменение копии влияет на оригинальный объект.

Клонирование через Object.create() не имеет смысла, поскольку Object.create(proto) создаёт новый объект, используя существующий объект proto в качество прототипа для нового.

const obj = { a: 7 };
const copy = Object.create(obj);
console.log(copy); // {}
console.log(copy.a); // 7 (не найдено в самом объекте, но найдено в прототипе)
console.log(copy.__proto__); // { a: 7 }
obj.hasOwnProperty('a'); // true
copy.hasOwnProperty('a'); // false

Клонирование в цикле for..in означает копирование не только собственных (own) свойств объекта, но и свойств прототипа. Само свойство, отвечающее за прототип, не копируется.

const cloneObject = (obj) => {
  const copy = {};
  for (let key in obj) {
    copy[key] = obj[key];
  }
  return copy;
};

const prototype = { prop: 'prototype property' };
const obj = { field: 'value' };
obj.__proto__ = prototype; // так делать не желательно, но для примера можно
console.log(obj); // { field: 'value' }
console.log(obj.prop); // 'prototype property'

const copy = cloneObject(obj);
console.log(copy); // { field: "value", prop: "prototype property" }

Клонирование через eval является самым худшим вариантом.

const cloneObject = obj => eval(uneval(obj));

const obj = { /* ... */ };
const copy = cloneObject(obj);

Во-первых, использование eval - это плохо.

Eval is not evil. Using eval poorly is.

Во-вторых, требуется поддержки функции uneval, имеющаяся только у Firefox, но даже в нём это может не сработать из-за политики безопастности контента (Content Security Policy): EvalError: call to eval() blocked by CSP.

Неглубокое клонирование

Неглубокое клонирование (shallow clone) подразумевает копирование неглубоких свойств (shallow properties) оригинального объекта в новый объект. Если свойство само является объектом (prop: {}), то оно передаётся по ссылке (оригинальное и скопированное свойство ссылаются на один объект).

Неглубокое свойство obj.prop, глубокое свойство: obj.prop.nestedProp. Глубокие свойства (deep properties) объекта клонируются автоматически, поскольку содержатся в объектах, являющихся неглубокими свойствами.

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

Клонирование через Object.assign().

const cloneObject = obj => Object.assign({}, obj);

const obj = {
  field: {
    nestedField: 'notes',
  },
};
const copy = cloneObject(obj);
console.log(obj === copy); // false
copy.field.nestedField = 'changed';
console.log(obj.field.nestedField); // 'changed' (изменение клона повлияло на оригинал)
console.log(obj.field === copy.field); // true (ссылаются на один объект)

Клонирование через Spread-оператор ... работает аналогично Object.assign().

const cloneObject = obj => ({ ...obj });

Клонирование при помощи Object.keys() подразумевает перебор и копирование собственных свойств оригинального объекта.

const cloneObject = (obj) => {
  const copy = {};
  Object.keys(obj).forEach((key) => {
    copy[key] = obj[key];
  });
  return copy;
};

В случае, если Object.assign и ... не поддерживаются, можно написать полифилл с использованием Object.keys.

Аналогичного Object.keys поведения можно добиться от клонирования в цикле for..in, добавив в нём дополнительную проверку на принадлежность свойства.

for (let key in obj) {
 if (obj.hasOwnProperty(key)) { /* ... */ }
}

Готовым решением неглубокого копирования является функция _.clone(value) из библиотеки lodash.

Глубокое клонирование

Глубокое клонирование (deep clone) подразумевает копирование свойств на всех уровнях, то есть на каждом уровне вложенности вместо передаче по ссылке создаётся новый объект с теми же свойствами.

Клонирование через JSON-сериализацию очень популярно благодаря простоте, скорости работы (JSON-сериализация реализована и оптимизирована браузером) и возможности глубокого клонирования.

const cloneObject = obj => JSON.parse(JSON.stringify(obj));

const obj = {
  field: {
    nestedField: 'notes',
  },
};
const copy = cloneObject(obj);
console.log(obj === copy); // false
copy.field.nestedField = 'changed';
console.log(obj.field.nestedField); // 'notes' (изменение клона не повлияло на оригинал)
console.log(obj.field === copy.field); // false

Недостаток: утрата некоторых данных (data loss), а точнее тех данных, которые не поддерживаются в JSON.

const cloneObject = obj => JSON.parse(JSON.stringify(obj));

const copy = cloneObject({
  a: () => {}, // поле опускается
  b: Infinity, // значение заменяется на null
  c: NaN, // значение заменяется на null
  d: new Date(), // превратится в строку
  e: undefined, // поле опускается
  f: Symbol(''), // поле опускается
});
console.log(copy); // { b: null, c: null, d: "XXXX-XX-XXTXX:XX:XX.XXXZ" }

Более того, некоторые данные вообще не могут быть преобразованы в JSON. Например, циклическая ссылка (англ. circular reference) или BigInt вызовут исключение (ошибку), которое нужно будет где-то обработать.

// циклическая ссылка
let foo = {};
foo.foo = foo;
JSON.stringify(foo); // TypeError: Converting circular structure to JSON
    --> starting at object with constructor 'Object'
    --- property 'foo' closes the circle

image

JSON.stringify({ a: BigInt(124) }); // TypeError: Do not know how to serialize a BigInt

Клонирование через V8-сериализацию в Node.js (экспериментальная функциональность).

const v8 = require('v8');
const clone = obj => v8.deserialize(v8.serialize(obj));

Пример глубокого клонирования конкретного объекта без всяких функций.

const user = {
  email: '[email protected]',
  settings: { theme: 'dark' },
  comments: ['Hi!', 'Agree'].
};

const clone = {
  ...user,
  settings: { ...user.settings },
  comments: [...user.comments],
};

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

const isObject = value => typeof value === 'object' && !Array.isArray(value) && value !== null;

const cloneObject = (obj) => {
  let copy = {};
  for (let prop in obj) {
    if (obj.hasOwnProperty(prop)) {
      const value = obj[prop];
      /* если значение является объектом, рекурсивно копируем его свойства */
      copy[prop] = isObject(value) ? cloneObject(value) : value;
    }
  }
  return copy;
}

const foo = { g: { h: 'h' } };
condt bar = cloneObject(foo); // { g: { h: 'h' } }
console.log(foo === bar); // false
console.log(foo.g === bar.g); // false

Эта функция не может обработать все случаи. Например, отдельно следует описывать работу с массивами и функциями, а также с циклическими ссылками, выбрасывающими исключения «too much recursion» и подобные.

const copy = {};
copy.proto = copy; // циклическая ссылка
console.log(copy); // { proto: {...} }
console.log(copy.proto); // { proto: {...} }
console.log(copy.proto.proto.proto); // { proto: {...} }

Таким образом, для глубокого клонирования лучше всего использовать готовые решения. Такими являются _.cloneDeep(obj) в библиотеке lodash, jQuery.extend(true, {}, obj), angular.clone(obj) и другие.

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

Операторы сравнения

В JavaScript есть два оператора сравнения: нестрогий (abstract) == и строгий (strict) ===.

При сравнении объектов A и B оба оператора вернут true лишь в том случае, если ссылки A и B будут указывать на один и тот же объект.

const a = {};
const b = {};
const c = a;
console.log(a == b, a === b); // false false
console.log(a == c, a === c); // true true

Неглубокое сравнение

Неглубокое сравнение (shallow comparison) объектов A и B подразумевает проверку на строгое равенство (===) только неглубоких свойств (shallow properties) объектов (проверка не рекурсивна). Если все неглубокие свойства совпадают, то объекты считаются эквивалентными (shallow equal). Если A === B, то A и B по определению считаются эквивалентными, поскольку ссылаются на один объект.

Неглубокое свойство obj.prop, глубокое свойство: obj.prop.nestedProp.

Примеры неглубокого сравнения.

  • { a: 1 } и { a: 1 } считаются эквивалентными, поскольку их неглубокие свойства a совпадают (1 === 1).
  • { a: {}} и { a: {}} считаются не эквивалентными, поскольку их неглубокие свойства a представленны объектами с разными ссылками ({} !== {}).

Реализация неглубокого сравнения для любых значений.

const isObject = value => typeof value === 'object' && value !== null;

const compareObjects = (A, B) => {
  const keysA = Object.keys(A);
  const keysB = Object.keys(B);
 
  /* Если количество свойств не совпадает, то объекты не эквивалентны. */
  if (keysA.length !== keysB.length) {
    return false;
  }
 
  /* Рассматриваются свойства объекта A в объекте B. Если объект B не имеет хотя бы одно 
  собственное (own) свойство или значения свойств не строго равны, то объекты не эквивалентны. */
  return !keysA.some(key => !B.hasOwnProperty(key) || A[key] !== B[key]);
};

const shallowEqual = (A, B) => {
  /* Если значения A и B проходят строгое равенство, то они эквивалентны. */
  if (A === B) {
    return true;
  }
 
  /* Если оба значения равны NaN, то они эквивалентны. */
  if ([A, B].every(Number.isNaN)) {
    return true;
  }
  
  /* Eсли A и/или B не являются объектами, то они не эквивалентны,  
  поскольку не прошли проверки выше. */
  if (![A, B].every(isObject)) {
    return false;
  }
  
  /* Остался случай, когда A и B — объекты */
  return compareObjects(A, B);
};

const a = { field: 1 };
const b = { field: 2 };
const c = { field: { field: 1 } };
const d = { field: { field: 1 } };

console.log(shallowEqual(1, 1)); // true
console.log(shallowEqual(1, 2)); // false
console.log(shallowEqual(null, null)); // true
console.log(shallowEqual(NaN, NaN)); // true
console.log(shallowEqual([], [])); // true
console.log(shallowEqual([1], [2])); // false
console.log(shallowEqual({}, {})); // true
console.log(shallowEqual({}, a)); // false
console.log(shallowEqual(a, b)); // false
console.log(shallowEqual(a, c)); // false
console.log(shallowEqual(c, d)); // false

Применение неглубокого сравнения в React, чтобы сделать PureComponent.

import shallowCompare from 'react-addons-shallow-compare'; 

class Foo extends Component {
  shouldComponentUpdate(nextProps, nextState) {
    return shallowCompare(this, nextProps, nextState);
  }
  render() { /* ... */ }
}

Неглубокого сравнение применяется в Redux: shallowEqual(oldState, newState), чтобы выяснить, изменился ли State. Именно поэтому очень важно не мутировать State: измененяются и новый, и старый State одновременно — неглубокое сравнение не видит различий между ними.

Глубокое сравнение

Глубокое сравнение (deep comparison) объектов A и B подразумевает рекурсивный обход и сравнение всех свойств (в том числе и глубоких) объектов A и B.

Одним из способов провести глубокое сравнения является JSON-сериализация (сравниваются получившиеся строки). Это достаточно быстрый способ.

const deepEqual = (A, B) => JSON.stringify(A) === JSON.stringify(B);

const c = { field: { field: 1 } };
const d = { field: { field: 1 } };
const e = { field: { field: 2 } };
console.log(deepEqual(c, d)); // true
console.log(deepEqual(c, e)); // false

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

const a = { f1: '1', f2: '2' };
const b = { f2: '2', f1: '1' };
deepEqual(a, b); // false

Других встроенных решений не существует.
Можно переписать функцию shallowEqual, сделав её рекурсивной, или подключить готовые функции из сторонних библиотек. В Node.js есть встроенная функция assert.deepEqual(), которая также представлена в виде отдельного модуля deep-equal.

Глубокое сравнение работает медленнее, чем неглубокое.
Не стоит его использовать, если в этом нет необходимости.

Мутабельность

Мутабельность объекта — его способность изменяться после создания, мутация (mutation) — соответствующее изменение.

Объекты в JavaScript передаются по ссылке, поэтому по умолчанию являются мутабельными.

const foo = {
  a: 'value',
};

foo.a = 'new value'; // мутация
foo.b = 17; // мутация

Переменная const разрешает мутацию объекта, поскольку хранит лишь ссылку на объект, которая не меняется (остаётся константой).

Отслеживание мутаций

Object.observe()

Для отслеживания мутаций раньше был доступен метод Object.observe(). Сейчас он запрещен (deprecated), поскольку были добавлены другие, более эффективные способы отслеживать мутации.

Object.observe(obj, callback);
  • obj — объект, изменения которого должны отслеживаться.
  • callback — функция обратного вызова, принимающая массив объектов, описывающих изменения.

Метод Object.observe() работает асинхронно. Он возвращет массив объектов со всеми изменениями. Объекты в массиве расположены в том же порядке, в котором происходили изменения при выполнении скрипта.

const foo = {
  a: 17,
  b: 'notes',
};

const callback = changes => console.log(changes);
Object.observe(foo, callback);

foo.c = 'mutations'; // добавление
foo.a = 7; // изменение
delete foo.b; // удаление

/* changes: [{
  name: 'c',
  object: <foo>,
  type: 'add',
}, {
  name: 'a',
  object: <foo>,
  type: 'update',
  oldValue: 17,
}, {
  name: 'b',
  object: <foo>,
  type: 'delete',
  oldValue: 'notes',
}] */

MutationObserver

MutationObserver — встроенный объект, отслеживающий изменения DOM-элементов.

const observer = new MutationObserver(callback); // инициализация
observer.observe(element, observerOptions); // подписка на изменения DOM-элемента
  • callback — функция обратного вызова, принимающая список объектов, описывающих изменения.
  • element — DOM-элемент, изменения которого должны отслеживаться.
  • observerOptions — объект с параметрами, определяющими, какие изменения должны отслеживаться.

Более развернётый пример.

const callback = (mutationList) => {
  for (let mutation of mutationList) {
    if (mutation.type === 'childList') {
      console.log('Дочерний элемент добавлен или удален');
      /* mutation: { type, addedNodes, deletedNodes } */
    } else if (mutation.type === 'attributes') {
      console.log(`Атрибут ${mutation.attributeName} был изменён`);
      /* mutation: { type, target, attributeName, oldValue } */
    }
  }
};

const observer = new MutationObserver(callback);

const el = document.querySelector('.elem');
const observerOptions = {
  childList: true,
  attributes: true,
  subtree: true, // false - только родительская вершина, true - родительская и дочерние
};

observer.observe(el, observerOptions);

Proxy

Proxy (прокси) — встроенный объект, позволяющий не только отлавливать любое совершаемое над объектом действие, но и влиять на его исход.

При помощи Proxy можно временно откладывать совершение действия, производить валидацю, отмену дейсвтия, устанавливать значение по умолчанию (если устанавливаемое значение невалидно), логгирование и многое другое.

const proxy = new Proxy(target, handler);
  • targetпроксируемый объект.
  • handler— объект с методами-ловушками (traps), каждый из которых отвечает за определённый тип действий над объектом.

Основные методы-ловушки

  • set — запись свойства (внутренний метод [[Set]]).
  • get — чтение свойства (внутренний метод [[Get]]).
  • deleteProperty — удаление свойства (внутренний метод [[Delete]]).
  • has — проверка наличия свойства при помощи in (внутренний метод [[HasProperty]]).
  • construct — создание через new (внутренний метод [[Construct]]).
  • apply — вызов функции (внутренний метод [[Call]]).
  • getOwnPropertyDescriptor — переборы через Object.keys, Object.values, Object.entries, for..in и Object.getOwnPropertyDescriptor (внутренний метод [[GetOwnProperty]]).

JavaScript накладывает условия на использование некоторых ловушек. Например, методы set и delete должны возвращать true, если изменения вступили в силу, и false — иначе.

Пример валидации перед установкой свойства проксируемому объекту (ловушка set). Проверяется, что передаваемое значение также является объектом.

const storage = {};

const proxy = new Proxy(storage, {
  set(target, property, value) {
    if (value instanceof Object) {
      target[property] = value;
      return true;
    }
    return false;
  },
});

proxy.a = 17;
console.log(proxy.a); // undefined
console.log(storage); // {}

proxy.b = { name: 'Alen' };
console.log(proxy.b); // { name: "Alen" }
console.log(storage); // { b: { name: "Alen" } }

Пример логирования проксируемой функции при её вызове (ловушка apply).

const increment = a => a + 1;

const proxy = new Proxy(increment, {
  apply(target, thisArg, args) {
    console.log(`Incrementing the value "${args[0]}"`);
    const result = target(...args);
    console.log(`Result: "${result}"`);
    return result;
  },
});

increment(5); 
/* ничего не выводится */

proxy(5);
/* Incrementing the value "5".
Result: "6" */

На примере выше заметно, что прямое взаимодействие с проксируемым объектом не имеет никакого эффекта — нужно всегда использовать созданный Proxy вместо него.

Reflect

Reflect — встроенный JavaScript-объект, предоставляющий методы для всех действий, которые перехватывает Proxy (для каждой ловушки).

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

  • Reflect.get(target, property) эквивалетно target[property].
  • Reflect.set(target, property, value) эквивалетно target[property] = value. и так далее.

Пример создания экземпляра класса при помощи Reflect.construct.

class Animal {
  constructor(kind, sex, age) {
    this.kind = kind;
    this.sex = sex;
    this.age = age;
  }
}

const elephant = Reflect.construct(Animal, ['elephant', 'male', 7]);
console.log(elephant);
/* Animal { kind: "elephant", sex: "male", age: 7 } */

Пример с установкой значения по умолчанию при помощи Proxy и Reflect.get.

const guest = { type: 'guest' }; // пользователь по умолчанию

const userTable = {
  tom: { type: 'user', username: 'Tom' },
  max: { type: 'user', username: 'Max' },
  frank: { type: 'user', username: 'Frank' }
};

const proxy = new Proxy(userTable, {
  get(target, property) {
    if (property in target) {
      return Reflect.get(target, property); // эквивалетно target[property];
    } else {
      return guest;
    }
  },
});

console.log(proxy['garry']); // { type: "guest" }
console.log(proxy['max']); // { type: "user", username: "Max" }

Иммутабельность

Неизменяемый, иммутабельный (immutable) объект — объект, состояние которого не может быть изменено после создания.

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

Иммутабельность затрагивает только сам объект, но не его свойства. Это работает как неглубокое копирование: ссылки на объекты-свойства остаются прежними.

Итерируемые объекты

Итерируемый объект (iterable) — любой объект, элементы которого можно перебрать в цикле for..of.

По умолчанию итерируемыми являются встроенные типы Array, Set, Map и String, в то время как Object не является.

Любой объект можно сделать итерируемым, реализовав метод Symbol.iterator.

В примере ниже реализуется итератор для объекта notes, содержащего буквенные значения по индексам. В функции итератора замкнуты две переменные: начальный и конечный индексы. Для реализации метода next() используется стрелочная функция, поскольку необходим доступ к буквам.

const notes = {
  0: 'n',
  1: 'o',
  2: 't',
  3: 'e',
  4: 's',
  [Symbol.iterator]: function() {
    let current = 0;
    let last = 4;
    return {
      next: () => {
        if (current <= last) {
          return { done: false, value: this[current++] }
        }
        return { done: true };
      },
    };
  },
};

for (i of notes) {
  console.log(i); // n, o, t, e, s
}
console.log(notes.length); // undefined

Массивы

Массив (Array) — встроенный итерируемый объект (можно перебрать через for..of), который хранит элементы по индексам 0, 1, 2, ..., имеет свойство length, а также имеет доступ к методам Array.prototype (find, includes, reduce и другие).

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

Создать массив можно двумя способами: через синтаксис [] или при помощи класса Array и его методов.

const foo = [1, 3, 7];
console.log(foo); // [1, 3, 7];

const bar = Array(1, 3, 7);
console.log(bar); // [1, 3, 7];

В массиве по некоторым индекстам могут лежать пустые элементы (empty).

const foo = [, 0, 1, 2]; 
console.log(foo); // [empty, 0, 1, 2]
console.log(foo[0]); // undefined

const bar = [,,,,,];
console.log(bar); // [empty × 5]

const baz = Array(100); // пустой массив длины 100
console.log(baz); // [empty × 100]

const qaz = [];
qaz[1000] = 7; 
console.log(qaz); // [empty × 1000, 7]

Массив можно создать из любого итерируемого объекта при помощи Array.from(iterable) или оператора ....

const iterable = 'notes'; /* строка - итерируемый объект */

const foo = Array.from(iterable);
console.log(foo); // ['n', 'o', 't', 'e', 's']

const bar = [...iterable];
console.log(bar); //['n', 'o', 't', 'e', 's']

Интересные примеры создания массивов.

const foo = Array(100).fill(0);
console.log(foo); // [0 x 100]

const bar = Array.from(Array(100).keys());
console.log(bar); // [0, 1, 2, ..., 99]

const baz = Array.from({ length: 100 }, (item, index) => index + 1); 
console.log(baz); // [1, 2, 3, ..., 100]

Обращение к элементам массива

Обращение к элементам массива не отличается от обращения к объектам, то есть производится по ключу ([]).

Как и у обычного объекта, ключи массива являются строками.

const foo = [1, 3, 7];
console.log(Object.keys(foo)); // ["0", "1", "2"]
console.log(foo[1]); // 3
console.log(foo["1"]); // 3

Добавление и удаление элементов

Массив в JavaScript имеет методы, характерные двухсторонней очереди (deque, double ended queue), что позволяет достаточно просто добавлять и удалять элементы на обоих концах массива.

let foo = [2];

/* добавление элемента в конец */
foo.push(3);
console.log(foo); // [2, 3]

/* добавление элемента в начало */
foo.unshift(1);
console.log(foo); // [1, 2, 3]

/* удаление последнего элемента */
const lastElem = foo.pop();
console.log(lastElem); // 3
console.log(foo); // [1, 2]

/* удаление первого элемента */
const firstElem = foo.shift();
console.log(firstElem); // 1
console.log(foo); // [2]

Добавлять элементы можно и при помощи оператора ....

let bar = [2];

/* добавление элемента в начало */
bar = [1, ...bar];
console.log(bar); // [1, 2]

/* добавление элемента в конец */
bar = [...bar, 3];
console.log(bar); // [1, 2, 3]

Удаление при помощи delete создаёт пустую ячейку в массиве.

let baz = [3];
delete baz[0];
console.log(baz); // [empty]
console.log(baz[0]); // undefined

Является ли массивом

[] instanceof Array // true
Array.isArray([]) // true

Сортировка

Сортировка (sorting) — упорядочивание элементов в списке (массиве) по какому-то правилу.

В JavaScript для сортировки массива имеется метод Array.prototype.sort(comparator), принимающий в качестве аргумента компаратор (comparator) — функцию comparator(a, b), задающую порядок сортировки. Если a и b равны, то функция должна вернуть 0, если a > b — что-то больше нуля (например, 1), если a < b — что-то меньше нуля (например, -1).

const numbers = [3, 2, 1];
console.log(numbers.sort()); //  [1, 2, 3]

Компаратор (в электронике) — устройство, принимающее два входных сигнала и определяющее, какой из них больше (возвращает 1, если больше первый, 0 — если второй).

В объектно-ориентированных языках программирования компаратор может быть классом или интерфейсом, имеющим метод compare.

Если не задать компаратор в методе sort(), то применится компаратор по умолчанию, сравнивающий элементы в лексикографическом порядке (как строки, посимвольно).

const numbers = [11, 1, 8, 10, 9];
console.log(numbers.sort()); // [1, 10, 11, 8, 9]
// поскольку '1' > '8', то '10' > '8' и `11` > `8`

Определим компараторы для сортировки массива из чисел по возрастанию (ascending) и по убыванию (descending).

/* по возрастанию */
const ascendingComparator = (a, b) => a - b; // если a > b, то a - b > 0

/* более делальная версия, делающая то же самое  */
const anotherAscendingComparator = (a, b) => {
  /* оператор > приводит свои операнды к числу */
  if (a > b) {
    return 1;
  }
  if (b > a) {
    return -1;
  }
  return 0;
}

/* по убыванию */
const descendingComparator = (a, b) => b - a; // если a > b, то b - a < 0

const numbers = [11, 1, 8, 10, 9];
console.log(numbers.sort(ascendingComparator)); // [1, 8, 9, 10, 11]
console.log(numbers.sort(anotherAscendingComparator)); // [1, 8, 9, 10, 11]
console.log(numbers.sort(descendingComparator)); // [11, 10, 9, 8, 1]

Аналогично можно сортировать и более сложные сущности.
Например: объекты по их конкретным полям.

const enginerComparator = (a, b) => b.skill - a.skill;

const enginers = [{ skill: 3 }, { skill: 1 }, { skill: 2 }];
console.log(enginers.sort(enginerComparator));
// [{ skill: 3 }, { skill: 2 }, { skill: 1 }]

Псевдомассивы

Псевдомассив (pseudo-array) — обычный объект, который как и массив, в качестве ключей имеет индексы 0, 1, 2, ... и свойство length, но при этом не является итерируемым и не имеет доступа к методам Array.prototype.

Псевдомассив можно сделать итерируемым объектом.

Примером итерируемого псевдомассива является arguments, хранящий все аргументы функции function, в которой он используется.

(function fn() {
  console.log(arguments instanceof Array); // false
  console.log(arguments instanceof Object); // true
  console.log(arguments); // { 0: 1, 1: 2, 2: 3 callee: f, length: 3, Symbol(Symbol.iterator): f }
  for (i of arguments) {
    console.log(i); // 1, 2, 3
  }
})(1, 2, 3);

Функции

Параметры и аргументы функции

Параметры функции — имена, перечисленные в определении функции.

Аргументы функции — значения, передаваемые в функцию.

Параметр функции является переменной, копирующей значение аргумента.

Фактически, параметры ведут себя следующим образом.

  const f = (param = {}) => {
    var param = param || {}; // скрытое поведение
  };

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

const foo = 1;
const bar = { a: 1 };
const baz = { c: 1 };

const fn = (param1, param2, param3) => {
  param1 = 2;
  console.log(param1 === foo); // false (притимивные значения копируются напрямую)

  param2.b = 2;
  console.log(param2 === bar); // true (мутация аргумента по ссылке)
  console.log(bar); // { a: 1, b: 2 }

  param3 = { d: 2 };
  console.log(param3 === baz); // false (перезапись переменной, утрата ссылки)
  console.log(baz); // { c: 1 }
};

fn(foo, bar, baz);

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

Стрелочная функция (Arrow Function Expression) является функциональным выражением, которое, помимо укороченного синтаксиса, обладает рядом свойств по сравнению с функциональным выражением, объявленным через function (Function Expression).

const inc = val => val += 1;
const sum = (a, b) => a + b;
const mul = (a, b) => {
  return a * b;
};

Стрелочная функция не имеет своих this и arguments — их значения ищутся снаружи (из внешнего лексического окружения).

const Foo = () => {
  console.log(this); 
  console.log(arguments); 
};
Foo();
// Window {...}
// ReferenceError: arguments is not defined
function Bar () {
  console.log(this);
  const Foo = () => {
    console.log(this);
    console.log(arguments);
  };
  Foo();
}
Bar();
// Window {...}
// Window {...}
// Arguments [...]
new Bar();
// Bar {...}
// Bar {...}
// Arguments [...]

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

const Article = () => {};
const article = new Article(); // TypeError: Article is not a constructor

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

Например, следующий пример вызовет ошибку, поскольку у void приоритет выше, чем у =>, и он обрабатывается раньше.

const fn = void () => console.log('notes'); // SyntaxError: Malformed arrow function parameter list

С async такой ошибки не возникает, поскольку async вообще не является оператором, поскольку не рассматривается отдельно от function.

const fn = async () => 'Notes';

Встроенные объекты

Error и его наследники

ReferenceError

ReferenceError — ошибка при обращении к несуществующей переменной.

foo.field; // ReferenceError: foo is not defined
console.log(foo) // ReferenceError: Cannot access 'foo' before initialization
const foo = {};

SyntaxError

SyntaxError - ошибка при попытке интерпретировать синтаксически неправильный код.

const foo; // SyntaxError: Missing initializer in const declaration
function(){ /* ... */ }() // SyntaxError: Function statements require a function name
function foo(){ /* ... */ }() // SyntaxError: Unexpected token )
JSON.parse('{ "field":"value", }'); // SyntaxError: Unexpected token } in JSON at position 19

TypeError

TypeError - ошибка при наличии значения несовместимого (неожидаемого) типа.

const foo = {};
foo.method(); // TypeError: foo.method is not a function
const foo = 1;
foo = 7; // TypeError: Assignment to constant variable

RangeError

RangeError — ошибка в случае нахождения значения за пределами допустимого диапазона.

const foo = new Array(-1); // RangeError: Invalid array length
const foo = 3;
foo.toFixed(101); // RangeError: toFixed() digits argument must be between 0 and 100
function foo() { foo() }
foo(); // RangeError: Maximum call stack size exceeded (везде, кроме Firefox)

EvalError

EvalError — ошибка в глобальной функции eval(). В текущей спецификации не используется и остаётся лишь для совместимости.

Ошибка ниже связана с проведением браузерами политики безопастности контента (Content Security Policy), которая помогает избежать многих потенциальных XSS (cross-site scripting) атак.
Ранее её тип был EvalError, сейчас он просто опускается:

window.setInterval("alert('notes')", 25); // Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source of script in the following Content Security Policy directive: "script-src github.githubassets.com".

URIError

URIError - ошибка при передаче недопустимых параметров в encodeURI() или decodeURI().

encodeURI('\uD900'); // URIError: malformed URI sequence (Firefox)
encodeURI('\uD900'); // URIError: The URI to be encoded contains an invalid character (Edge)
encodeURI('\uD900'); // URIError: URI malformed (Chrome and others)

InternalError

InternalError - внутренняя ошибка в движке JavaScript. (только Firefox)

function foo() { foo() }
foo(); // InternalError: too much recursion

Все рассмотренные типы ошибок можно сгенерировать так же, как и Error, наследниками которого они являются:

throw new Error(/* ... */);

Promise

Promise (промис) – специальный объект, содержащий своё состояние.
Изначально состояние pending (ожидание).
Затем либо resolved/fulfilled (выполнено успешно), либо rejected (выполнено с ошибкой).

/* создание Promise */
const executor = (resolve, reject) => { /* ... */ };
const promise = new Promise(executor); 

Функция executor(resolve, reject) вызывается автоматически. В ней можно выполнять любые асинхронные операции. По их завершении следует вызвать либо resolve(value), либо reject(reason).

После вызова resolve или reject промис меняет своё состояние, которое становится конечным (больше его изменить нельзя).

Отреагировать на изменение состояния промиса можно при помощи then и catch.

const onResolved = value => { /* ... */ };
const onRejected = reason => { /* ... */ };

// функция onResolved сработает при успешном выполнении
promise.then(onResolved);
// функция onRejected – при выполнении с ошибкой
promise.then(onResolved, onRejected);
promise.catch(onRejected);

Пример с setTimeout, где промис успешно выполнится не менее, чем через 3 секунды.

const executor = resolve => void setTimeout(resolve, 3000);
const promise = new Promise(executor);
promise.then(() => console.log('resolved!'));
// через ~3 секунды выведется 'resolved!'

Пример с передачей значения в resolve.

const executor = resolve => void setTimeout(() => resolve('resolved!'), 3000);
const promise = new Promise(executor);
promise.then(console.log);
// через ~3 секунды выведется 'resolved!'

Пример с передачей причины в reject.

const executor = (resolve, reject) => void setTimeout(() => reject('rejected!'), 3000);
const promise = new Promise(executor);
promise.catch(console.log);
// через ~3 секунды выведется 'rejected!'

Промисификация

Промисификациясоздание обёртки, возвращающей Promise, вокруг асинхронной функциональности.

Обычно промисифицируют асинхронные функции, построенные на функциях обратного вызова (callbacks).

/* Принимается функция fn и возвращается функция-обёртка, возвращающая Promise. */
const promisify = fn => (...args) => new Promise((resolve, reject) => {    
  const callback = (err, data) => err ? reject(err) : resolve(data);
  fn(...args, callback);
});

Цепочка промисов

Если нужно выполнять асинхронные операции в определённой последовательности, можно каждую из них обернуть в промис и создать цепочку промисов (Promise chain). Для создания такой цепочки необходимо в .then() или .catch() вернуть промис.

Порядок в Promise.all()

Функция Promise.all(iterable) принимает итерируемый объект (обычно массив), содержащий промисы (элементы, не являющиеся промисами, помещаются в Promise.resolve()), дожидается выполнения каждого из промисов и возвращает массив, состоящий из их значений.

Несмотря на то, что промисы выполняются асинхронно, порядок в результирующем массиве значений совпадает с порядком промисов в начальном итерируемом объекте благодаря внутреннему свойству [[Index]]:

const slow = new Promise(resolve => setTimeout(resolve, 250, 'slow'));
const instant = 'instant'; // тип не Promise , поэтому преобразуется в Promise.resolve('instant')
const quick = new Promise(resolve => setTimeout(resolve, 50, 'quick'));

const onResolved = responses => responses.map(response => console.log(response));
Promise.all([slow, instant, quick]).then(onResolved);

// или то же самое с помощью async/await
try {
  const responses = await Promise.all([slow, instant, quick]);
  responses.map(response => console.log(response)); // 'slow', 'instant', 'quick'
} catch (e) {
  /* ... */
}

Поскольку тип String является итерируемым, его тоже можно передать в Promise.all():

Promise.all('notes').then(res => console.log(res)); // ['n', 'o', 't', 'e', 's']
// что эквивалентно
Promise.all(['n', 'o', 't', 'e', 's']).then(res => console.log(res));

Promise и async/await

async function f(time) {
  const foo = await new Promise(res => setTimeout(() => res('Notes 1'), time));
  console.log(foo);
  const bar = await Promise.resolve('Notes 2');
  console.log(bar);
  await Promise.reject('Error');
}

f(2000);
// Notes 1
// Notes 2
// Uncaught (in promise) Error
f(2000).catch(e => console.log(e));
// Notes 1
// Notes 2
// Error
const asyncGeneratorStep = (gen, resolve, reject, _next, _throw, method, arg) => {
  try {
    const { value, done } = gen[method](arg);
    if (done) {
      resolve(value);
    } else {
      Promise.resolve(value).then(_next, _throw);
    }
  } catch (error) {
    reject(error);
  }
}

const _asyncToGenerator = fn => (...args) =>
  new Promise((resolve, reject) => {
    const _next = value => void step('next', value);
    const _throw = error => void step('throw', error);
    const gen = fn(args);
    function step (method, arg) {
      asyncGeneratorStep(gen, resolve, reject, _next, _throw, method, arg);
    }
    _next(undefined);
  });

const generator = function* (time) {
  const foo = yield new Promise(res => setTimeout(() => res('Notes 1'), time));
  console.log(foo);
  const bar = yield Promise.resolve('Notes 2');
  console.log(bar);
  yield Promise.reject('Error');
};

function f() {
  return _asyncToGenerator(generator).apply(this, arguments);
}

f(2000);
// Notes 1
// Notes 2
// Uncaught (in promise) Error
f(2000).catch(e => console.log(e));
// Notes 1
// Notes 2
// Error

Спецификация

Функции под капотом JavaScript

Функциональный объект (function object) — объект, поддерживающий внутренний метод [[Call]].

Фукция-конструктор (constructor function), или просто конструктор (constructor), — функциональный объект, поддерживающий внутренний метод [[Construct]].

Метод [[Call]]

Метод [[Call]] (thisArgument, argumentsList) выполняет код, связанный с его функциональным объектом.

Вызывается при помощи выражения вызова функции:

object()

Аргументы: значение this и список аргументов, переданных функции выражением вызова.

Объекты, которые реализуют внутренний метод [[Call]], называются вызываемыми (callable).

Метод [[Construct]]

Метод [[Construct]] (argumentsList, newTarget) cоздаёт и возвращает объекты.

Вызывается при помощи операторов new и super.

Аргументы: список аргументов оператора и объект, к которому изначально был применён оператор new.

Инстанцирование функционального объекта

Инстанцирование (instantiation) — создание экземпляра класса (instance).
Слово инстанционирование применяется к классу, создание (creation) - к объекту.

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

Функциональные объекты инстанционируются при помощи:

InstantiateFunctionObject(scope)

Function Declaration

function BindingIdentifier ( FormalParameters ) { FunctionBody }

I этап интерпретации - инстанционирование

  1. Положить в переменную strict true, если к коду функции применён strict мод, false иначе.
  2. Положить в переменную name строку BindingIdentifier или строку "default", если значение не задано.
  3. Положить в переменную F результат выполнения FunctionCreate(Normal, FormalParameters, FunctionBody, scope, strict).
  4. Создать конструктор с помощью MakeConstructor(F).
  5. Установить имя функции с помощью SetFunctionName(F, name).
  6. Вернуть F.

II этап интерпретации - оценка (Evaluation)

  1. Вернуть NormalCompletion(empty).

Function Expression

function ( FormalParameters ) { FunctionBody }

I этап интерпретации

Отсутствует.

II этап интерпретации - оценка (Evaluation)

  1. Положить в переменную strict true, если к коду функции применён strict мод, false иначе.
  2. Положить в переменную scope LexicalEnvironment из контекста выполнения.
  3. Положить в переменную closure результат выполнения FunctionCreate(Normal, FormalParameters, FunctionBody, scope, strict).
  4. Создать конструктор с помощью MakeConstructor(F).
  5. Вернуть closure.