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 definedfor (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 ||):
true17,-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 ||):
false0(ноль),-0(отрицательный ноль),0n(BigInt ноль)'',"", (пустая строка)nullundefinedNaN
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 circleJSON.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: 'user@email.com',
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 definedfunction 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 definedconsole.log(foo) // ReferenceError: Cannot access 'foo' before initialization
const foo = {};SyntaxError - ошибка при попытке интерпретировать синтаксически неправильный код.
const foo; // SyntaxError: Missing initializer in const declarationfunction(){ /* ... */ }() // SyntaxError: Function statements require a function namefunction foo(){ /* ... */ }() // SyntaxError: Unexpected token )JSON.parse('{ "field":"value", }'); // SyntaxError: Unexpected token } in JSON at position 19TypeError - ошибка при наличии значения несовместимого (неожидаемого) типа.
const foo = {};
foo.method(); // TypeError: foo.method is not a functionconst foo = 1;
foo = 7; // TypeError: Assignment to constant variableRangeError — ошибка в случае нахождения значения за пределами допустимого диапазона.
const foo = new Array(-1); // RangeError: Invalid array lengthconst foo = 3;
foo.toFixed(101); // RangeError: toFixed() digits argument must be between 0 and 100function 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) Errorf(2000).catch(e => console.log(e));
// Notes 1
// Notes 2
// Errorconst 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) Errorf(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.
