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
.
- 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
, в NodeJS — global
). Поэтому, в отличие от остальных переменных, её можно удалить оператором 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
изменяется в области видимости лишь при инициализации.
/* main.js */
// >>> Global Scope: { a: undefined }
var a = 5;
// >>> Global Scope: { a: 5 }
В 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;
Прежде, чем приступать к определению контекста в рамках JavaScript, давайте разберёмся, что же такое контекст в широком смысле этого слова.
Контекстом (англ. context
) называют совокупность фактов и обстоятельств, в окружении которых происходит некоторое событие, существует некоторое явление или некоторый объект.
Например, можно сфокусироваться на одной из следующих тем ниже и начать описывать их.
- Пример события. Например, исторические события - Битва под Оршей, принятие Билля о правах.
- Пример объекта. Например, историческая личность - Эммелин Панкхёрст и исторически значимое место - Собор Парижской Богоматери.
- Пример явления. Например, северное сияние. Сбор информации на любую тему выше равносилен наполнению контекста, соответствующего этой теме.
Чем больше мы узнаём о чём-то, тем точнее мы можем воспроизводить это. Чем больше рассказчик даёт вам деталей, тем детальнее вы представляете то, что он видел своими глазами.
Например, рассмотрим следующий пример описания человека.
Джек Лондон — это американский писатель, который отбрёл известность благодаря своим приключенческим рассказам и романам. Тем, кому довелось знать его лично, описывали его как мужественного, отважного, целенаправленного и решительного человека... Как видно, в первом предложении указано имя человека. И во втором предложение нам уже ясно, что местоимение "его" ссылается на Джека Лондона.
Если бы компьютер мог выделять контекст из написанного, то он бы сделал это примерно следующим образом:
const he = {
name: 'Джек Лондон',
sex: 'мужчина',
profession: 'писатель',
genres: ['приключение', 'роман'],
traits: ['мужественный', 'отважный', 'целенаправленный', 'решительный'],
}
С каждым новым предложением этот контекст бы продолжил наполняться новыми деталями.
Если убрать первое сообшение из примера выше, то тогда не понятно, о ком идёт речь. В таком случае говорят о нарушении целостности контекста или вырывании из контекста.
Тем, кому довелось знать его лично, описывали его как мужественного, отважного, целенаправленного и решительного человека...
Например, контекстом текущего документа Notes/JavaScript.md
является язык JavaScript и всё, что с ним тесно связано. И в то же время любой другой язык программирования (скажем, Java) находится вне рассматриваемого контекста. Ещё пример: в контексте данной главы рассматриваются понятия "контекст", ключевое слово this
и не рассматриваются типы данных.
Если вернуться к JavaScript, то под "контекстом" обычно подразумевают контекст выполнения (англ. Execution Context
, EC
).
Всего можно выделить два контекста выполнения:
- Контекст выполнения функции (англ.
Function Execution Context
,FEC
). - Глобальный контекст выполнения (англ.
Global Execution Context
,GEC
).
Оператор 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"
Раньше в 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 — унарный оператор, выполнящий принимаемое выражение и возвращающий 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 — унарный оператор, удаляющий свойство из объекта (массива, функции и других наследников 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
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()
. Сейчас он запрещен (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 — встроенный объект, отслеживающий изменения 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
можно временно откладывать совершение действия, производить валидацю, отмену дейсвтия, устанавливать значение по умолчанию (если устанавливаемое значение невалидно), логгирование и многое другое.
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 — встроенный 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';
ReferenceError — ошибка при обращении к несуществующей переменной.
foo.field; // ReferenceError: foo is not defined
console.log(foo) // ReferenceError: Cannot access 'foo' before initialization
const foo = {};
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 - ошибка при наличии значения несовместимого (неожидаемого) типа.
const foo = {};
foo.method(); // TypeError: foo.method is not a function
const foo = 1;
foo = 7; // TypeError: Assignment to constant variable
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 — ошибка в глобальной функции 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 - ошибка при передаче недопустимых параметров в 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 - внутренняя ошибка в движке JavaScript. (только Firefox)
function foo() { foo() }
foo(); // InternalError: too much recursion
Все рассмотренные типы ошибок можно сгенерировать так же, как и Error, наследниками которого они являются:
throw new Error(/* ... */);
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(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));
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
Функциональный объект (function object) — объект, поддерживающий внутренний метод [[Call]].
Фукция-конструктор (constructor function), или просто конструктор (constructor), — функциональный объект, поддерживающий внутренний метод [[Construct]].
Метод [[Call]] (thisArgument, argumentsList) выполняет код, связанный с его функциональным объектом.
Вызывается при помощи выражения вызова функции:
object()
Аргументы: значение this и список аргументов, переданных функции выражением вызова.
Объекты, которые реализуют внутренний метод [[Call]], называются вызываемыми (callable).
Метод [[Construct]] (argumentsList, newTarget) cоздаёт и возвращает объекты.
Вызывается при помощи операторов new и super.
Аргументы: список аргументов оператора и объект, к которому изначально был применён оператор new.
Инстанцирование (instantiation) — создание экземпляра класса (instance).
Слово инстанционирование применяется к классу, создание (creation) - к объекту.
Несмотря на то, что функции в JavaScript являются объектами, в то же время они могут быть и классами, поэтому к ним и применяется слово инстанционирование.
Функциональные объекты инстанционируются при помощи:
InstantiateFunctionObject(scope)
function BindingIdentifier ( FormalParameters ) { FunctionBody }
- Положить в переменную strict true, если к коду функции применён strict мод, false иначе.
- Положить в переменную name строку BindingIdentifier или строку "default", если значение не задано.
- Положить в переменную F результат выполнения
FunctionCreate(Normal, FormalParameters, FunctionBody, scope, strict)
. - Создать конструктор с помощью
MakeConstructor(F)
. - Установить имя функции с помощью
SetFunctionName(F, name)
. - Вернуть F.
- Вернуть
NormalCompletion(empty)
.
function ( FormalParameters ) { FunctionBody }
Отсутствует.
- Положить в переменную strict true, если к коду функции применён strict мод, false иначе.
- Положить в переменную scope LexicalEnvironment из контекста выполнения.
- Положить в переменную closure результат выполнения
FunctionCreate(Normal, FormalParameters, FunctionBody, scope, strict)
. - Создать конструктор с помощью
MakeConstructor(F)
. - Вернуть closure.