Skip to content

Latest commit

History

History
2224 lines (1803 loc) · 114 KB

File metadata and controls

2224 lines (1803 loc) · 114 KB

Основы JavaScript

Типизация

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

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

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

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

/* Java */publicclassNotes {
publicstaticvoidmain(String []args){
intnumber = 1; // числовой типnumber = true; // error: incompatible types: boolean cannot be converted to int
}
}

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

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

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

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

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

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

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

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

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

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

/* Java */publicclassNotes {
publicstaticvoidmain(String []args){
intnumber = 17;
intarray[] = newint[3];
System.out.println(number + array); // error: bad operand types for binary operator '+'
}
}

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

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

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

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

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

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

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

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

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

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

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

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

  • number1, 2.17, NaN, Infinity
  • string'str', "str"
  • booleantrue, false
  • nullnull
  • undefinedundefined
  • symbolSymbol(str)
  • object{}

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

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

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

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

constsymbol=Symbol('notes');constfoo={[symbol]: 'notes'};console.log(Object.keys(foo));// []console.log(Object.getOwnPropertyNames(foo));// []console.log(foo.notes);// undefinedconsole.log(foo[Symbol('notes')]);// undefined (символы уникальны)// ноconsole.log(foo[symbol]);// notes

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

console.log(foo);// undefinedvarfoo='notes';console.log(foo);// notes

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

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

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

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

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

foo=1;window.foo=7;console.log(foo);// 7deletefoo;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'));// trueconsole.log(Boolean(''));// falseconsole.log(Boolean(-1));// true/* приведение к логическому значению при помощи String() */console.log(String(null));// 'null'console.log(String({}));// '[object Object]'

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

/* Java */classNotes{publicstaticvoidmain(String[]args){
double foo=11.1;intbar=(int)a;// приведение double к intSystem.out.println(bar);// 11}}

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

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

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

consta={};console.log(a.valueOf());// {}console.log(a.valueOf()===a);// trueconstb=[];console.log(b.valueOf());// []console.log(b.valueOf()===b);// truefunctionc(){}console.log(c.valueOf());// ƒ c() {}console.log(c.valueOf()===c);// true

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

consta={valueOf: ()=>7};console.log(a);// { valueOf: ƒ }console.log(a.valueOf());// 7constdate=newDate();console.log(date.valueOf());// 1573833066283

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

consta={};console.log(a.toString());// "[object Object]"console.log([].toString());// ""console.log([1,2,3].toString());// "1,2,3"constdate=newDate();console.log(date.toString());// "Sun Nov 17 2139 19:13:50 GMT+0300 (Moscow Standard Time)"functionfn(){/* ... */}console.log(fn.toString());// "function fn() { /* ... */ }"

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

consta={};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()меняются местами.

constisPrimitive=argument=>!['object','function'].includes(typeofargument)||argument===null;consttoPrimitive=(argument,preferredType='number')=>{if(isPrimitive(argument)){returnargument;}if(preferredType==='number'){/* сперва valueOf(), затем toString() */if(argument.valueOf&&isPrimitive(argument.valueOf())){returnargument.valueOf();}if(argument.toString&&isPrimitive(argument.toString())){returnargument.toString();}}elseif(preferredType==='string'){/* сперва toString(), затем valueOf() */if(argument.toString&&isPrimitive(argument.toString())){returnargument.toString();}if(argument.valueOf&&isPrimitive(argument.valueOf())){returnargument.valueOf();}}thrownewTypeError('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)).
constToString=(argument)=>{if(typeofargument==='string'){returnargument;}if(typeofargument==='symbol'){thrownewTypeError('Cannot convert a Symbol value to a string');}constallowedPrimitives=['number','boolean','undefined'];if(allowedPrimitives.includes(typeofargument)||argument===null){return`${argument}`;}if(!isPrimitive(argument)){returnToNumber(ToPrimitive(argument));}thrownewTypeError('Cannot convert argument to string');}

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

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

  • Если argument имеет тип boolean, то вернуть значение.
  • Иначе, если argument равно undefined, null, 0, NaN, "" (пустая строка), то вернутьfalse.
  • В остальных случаях (Object, Symbol, числа кроме0 и непустые строки) вернутьtrue.
constToBoolean=(argument)=>{if(typeofargument==='boolean'){returnargument;}if([undefined,null,0,NaN,''].includes(argument)){returnfalse;}returntrue;}

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

Преобразование значения к типу 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)).
constToNumber=(argument)=>{if(typeofargument==='number'){returnargument;}if(typeofargument==='boolean'){returnargument ? 1 : 0;}if(typeofargument==='string'){returnargument==='' ? 0 : parseFloat(argument,10);}if(typeofargument==='symbol'){thrownewTypeError('Cannot convert a Symbol value to a number');}if(argument===undefined){returnNaN;}if(argument===null){return0;}if(!isPrimitive(argument)){returnToNumber(ToPrimitive(argument));}thrownewTypeError('Cannot convert argument to number');}

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

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

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

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

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

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

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

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

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

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

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

constfn=()=>{varfoo=1;}console.log(foo);// ReferenceError: foo is not definedfn();console.log(foo);// ReferenceError: foo is not defined

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

Примеры

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

Переменная var

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

/* main.js */// >>> Global Scope: { a: undefined }vara=5;// >>> Global Scope: { a: 5 }

Переменная const

Переменная let

Блок кода {}

Всплытие (hoisting)

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

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

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

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

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

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

console.log(foo);// undefinedvarfoo=3;console.log(bar);// undefinedbar='notes';

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Ещё пример

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

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

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

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

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

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

Операторы

Оператор typeof

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

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

typeofundefined// "undefined"typeof0// "number"typeoftrue// "boolean"typeof"foo"// "string"typeofSymbol("foo");// "symbol" typeof{}// "object"typeof[]// "object"typeofnull// "object" (врождённая ошибка языка)typeoffunction(){}// "function"

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

console.log(typeofnull);// "object"console.log(nullinstanceofObject);// false

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

console.log(typeofNaN);// "number"

Оператор typeof и undefined

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

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

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

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

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

Оператор void

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

void3// undefinedvoid(3)// undefinedvoid(3=='3')// undefinedvoid3=='3';// undefined == '3' --> false

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

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

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

if(field===void0)

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

constonClick=()=>voidthis.setState({isClicked: true});

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

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

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

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

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

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

letfoo=2,3;// SyntaxError: Unexpected number

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

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

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

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

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

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

constgetDataType=data=>typeofdata;// заменяем наconstgetDataType=data=>(console.log(data),typeofdata);getDataType('notes')// можно увидеть значение 'notes' в консоли

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

constarray=['n','o','t','e'];console.log(array.push('s'))// 5 (вернулась длина массива после добавления элемента)// хотим вернуть новый массив:constarray=['n','o','t','e'];constpush=(arr,val)=>(arr.push(val),arr);console.log(push(array,'s'));// ['n', 'o', 't', 'e', 's']

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

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

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

Оператор delete

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

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

constfoo={a: 1,b: 7};console.log(foo);// { a: 1, b: 7 }deletefoo.a;// trueconsole.log(foo);// { b: 7 }

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

constbar=[1,2,3];deletebar[0];// trueconsole.log(bar);// [empty, 2, 3]console.log(bar.length)// 3deletebar[2];// trueconsole.log(bar);// [empty, 2, empty]console.log(bar.length)// 3

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

foo='notes';console.log(window.foo);// 'notes'deletefoo;// trueconsole.log(window.foo);// undefined

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

varfoo='notes';deletefoo;// falseconsole.log(foo);// 'notes'functionbar(){}deletebar;// falseconsole.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(){} (любые объекты)

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

newBoolean(false)===true// falsenewBoolean(false)===false// falsenewBoolean(false)==true// falsenewBoolean(false)==false// true// Объяснение: при сравнении берётся `.valueOf()` объекта класса `Boolean`(newBoolean(false)).valueOf()// false // ещё примеры с неявным использованием `.valueOf()`:(newNumber(0))==false// 0 == false(newString(''))==false// '' == false

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

(newString(''))&&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)===falseBoolean(0)===falseBoolean('')===Boolean('')===Boolean(``)===falseBoolean(null)===falseBoolean(undefined)===falseBoolean(NaN)===false

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

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

letx=null;x??='foo'x??='bar'console.log(x)// 'foo'lety;// undefinedy??=0y??=1console.log(y)// 0letz;// undefinedz??=undefinedz??=nullz??=falsez??=trueconsole.log(z)// false

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

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

Объекты

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

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

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

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

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

constisObject=value=>typeofvalue==='object'&&!Array.isArray(value)&&value!==null;

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

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

Можно проще.

({})instanceofObject// true

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

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

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

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

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

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

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

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

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

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

constcloneObject=(obj)=>{constcopy={};for(letkeyinobj){copy[key]=obj[key];}returncopy;};constprototype={prop: 'prototype property'};constobj={field: 'value'};obj.__proto__=prototype;// так делать не желательно, но для примера можноconsole.log(obj);// { field: 'value' }console.log(obj.prop);// 'prototype property'constcopy=cloneObject(obj);console.log(copy);// { field: "value", prop: "prototype property" }

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

constcloneObject=obj=>eval(uneval(obj));constobj={/* ... */};constcopy=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().

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

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

constcloneObject=obj=>({ ...obj});

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

constcloneObject=(obj)=>{constcopy={};Object.keys(obj).forEach((key)=>{copy[key]=obj[key];});returncopy;};

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

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

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

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

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

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

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

constcloneObject=obj=>JSON.parse(JSON.stringify(obj));constobj={field: {nestedField: 'notes',},};constcopy=cloneObject(obj);console.log(obj===copy);// falsecopy.field.nestedField='changed';console.log(obj.field.nestedField);// 'notes' (изменение клона не повлияло на оригинал)console.log(obj.field===copy.field);// false

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

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

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

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

image

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

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

constv8=require('v8');constclone=obj=>v8.deserialize(v8.serialize(obj));

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

constuser={email: 'user@email.com',settings: {theme: 'dark'},comments: ['Hi!','Agree'].};constclone={
...user,settings: { ...user.settings},comments: [...user.comments],};

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

constisObject=value=>typeofvalue==='object'&&!Array.isArray(value)&&value!==null;constcloneObject=(obj)=>{letcopy={};for(letpropinobj){if(obj.hasOwnProperty(prop)){constvalue=obj[prop];/* если значение является объектом, рекурсивно копируем его свойства */copy[prop]=isObject(value) ? cloneObject(value) : value;}}returncopy;}constfoo={g: {h: 'h'}};condtbar=cloneObject(foo);// { g: { h: 'h' }}console.log(foo===bar);// falseconsole.log(foo.g===bar.g);// false

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

constcopy={};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 будут указывать на один и тот же объект.

consta={};constb={};constc=a;console.log(a==b,a===b);// false falseconsole.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 представленны объектами с разными ссылками ({} !== {}).

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

constisObject=value=>typeofvalue==='object'&&value!==null;constcompareObjects=(A,B)=>{constkeysA=Object.keys(A);constkeysB=Object.keys(B);/* Если количество свойств не совпадает, то объекты не эквивалентны. */if(keysA.length!==keysB.length){returnfalse;}/* Рассматриваются свойства объекта A в объекте B. Если объект B не имеет хотя бы одно  собственное (own) свойство или значения свойств не строго равны, то объекты не эквивалентны. */return!keysA.some(key=>!B.hasOwnProperty(key)||A[key]!==B[key]);};constshallowEqual=(A,B)=>{/* Если значения A и B проходят строгое равенство, то они эквивалентны. */if(A===B){returntrue;}/* Если оба значения равны NaN, то они эквивалентны. */if([A,B].every(Number.isNaN)){returntrue;}/* Eсли A и/или B не являются объектами, то они не эквивалентны,  поскольку не прошли проверки выше. */if(![A,B].every(isObject)){returnfalse;}/* Остался случай, когда A и B — объекты */returncompareObjects(A,B);};consta={field: 1};constb={field: 2};constc={field: {field: 1}};constd={field: {field: 1}};console.log(shallowEqual(1,1));// trueconsole.log(shallowEqual(1,2));// falseconsole.log(shallowEqual(null,null));// trueconsole.log(shallowEqual(NaN,NaN));// trueconsole.log(shallowEqual([],[]));// trueconsole.log(shallowEqual([1],[2]));// falseconsole.log(shallowEqual({},{}));// trueconsole.log(shallowEqual({},a));// falseconsole.log(shallowEqual(a,b));// falseconsole.log(shallowEqual(a,c));// falseconsole.log(shallowEqual(c,d));// false

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

importshallowComparefrom'react-addons-shallow-compare';classFooextendsComponent{shouldComponentUpdate(nextProps,nextState){returnshallowCompare(this,nextProps,nextState);}render(){/* ... */}}

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

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

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

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

constdeepEqual=(A,B)=>JSON.stringify(A)===JSON.stringify(B);constc={field: {field: 1}};constd={field: {field: 1}};conste={field: {field: 2}};console.log(deepEqual(c,d));// trueconsole.log(deepEqual(c,e));// false

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

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

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

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

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

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

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

constfoo={a: 'value',};foo.a='new value';// мутацияfoo.b=17;// мутация

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

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

Object.observe()

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

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

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

constfoo={a: 17,b: 'notes',};constcallback=changes=>console.log(changes);Object.observe(foo,callback);foo.c='mutations';// добавлениеfoo.a=7;// изменениеdeletefoo.b;// удаление/* changes: [{ name: 'c', object: <foo>, type: 'add',}, { name: 'a', object: <foo>, type: 'update', oldValue: 17,}, { name: 'b', object: <foo>, type: 'delete', oldValue: 'notes',}] */

MutationObserver

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

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

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

constcallback=(mutationList)=>{for(letmutationofmutationList){if(mutation.type==='childList'){console.log('Дочерний элемент добавлен или удален');/* mutation: { type, addedNodes, deletedNodes } */}elseif(mutation.type==='attributes'){console.log(`Атрибут ${mutation.attributeName} был изменён`);/* mutation: { type, target, attributeName, oldValue } */}}};constobserver=newMutationObserver(callback);constel=document.querySelector('.elem');constobserverOptions={childList: true,attributes: true,subtree: true,// false - только родительская вершина, true - родительская и дочерние};observer.observe(el,observerOptions);

Proxy

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

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

constproxy=newProxy(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). Проверяется, что передаваемое значение также является объектом.

conststorage={};constproxy=newProxy(storage,{set(target,property,value){if(valueinstanceofObject){target[property]=value;returntrue;}returnfalse;},});proxy.a=17;console.log(proxy.a);// undefinedconsole.log(storage);// {}proxy.b={name: 'Alen'};console.log(proxy.b);// { name: "Alen" }console.log(storage);// { b: { name: "Alen" }}

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

constincrement=a=>a+1;constproxy=newProxy(increment,{apply(target,thisArg,args){console.log(`Incrementing the value "${args[0]}"`);constresult=target(...args);console.log(`Result: "${result}"`);returnresult;},});increment(5);/* ничего не выводится */proxy(5);/* Incrementing the value "5".Result: "6" */

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

Reflect

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

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

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

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

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

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

constguest={type: 'guest'};// пользователь по умолчаниюconstuserTable={tom: {type: 'user',username: 'Tom'},max: {type: 'user',username: 'Max'},frank: {type: 'user',username: 'Frank'}};constproxy=newProxy(userTable,{get(target,property){if(propertyintarget){returnReflect.get(target,property);// эквивалетно target[property];}else{returnguest;}},});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() используется стрелочная функция, поскольку необходим доступ к буквам.

constnotes={0: 'n',1: 'o',2: 't',3: 'e',4: 's',[Symbol.iterator]: function(){letcurrent=0;letlast=4;return{next: ()=>{if(current<=last){return{done: false,value: this[current++]}}return{done: true};},};},};for(iofnotes){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 и его методов.

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

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

constfoo=[,0,1,2];console.log(foo);// [empty, 0, 1, 2]console.log(foo[0]);// undefinedconstbar=[,,,,,];console.log(bar);// [empty × 5]constbaz=Array(100);// пустой массив длины 100console.log(baz);// [empty × 100]constqaz=[];qaz[1000]=7;console.log(qaz);// [empty × 1000, 7]

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

constiterable='notes';/* строка - итерируемый объект */constfoo=Array.from(iterable);console.log(foo);// ['n', 'o', 't', 'e', 's']constbar=[...iterable];console.log(bar);//['n', 'o', 't', 'e', 's']

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

constfoo=Array(100).fill(0);console.log(foo);// [0 x 100]constbar=Array.from(Array(100).keys());console.log(bar);// [0, 1, 2, ..., 99]constbaz=Array.from({length: 100},(item,index)=>index+1);console.log(baz);// [1, 2, 3, ..., 100]

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

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

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

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

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

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

letfoo=[2];/* добавление элемента в конец */foo.push(3);console.log(foo);// [2, 3]/* добавление элемента в начало */foo.unshift(1);console.log(foo);// [1, 2, 3]/* удаление последнего элемента */constlastElem=foo.pop();console.log(lastElem);// 3console.log(foo);// [1, 2]/* удаление первого элемента */constfirstElem=foo.shift();console.log(firstElem);// 1console.log(foo);// [2]

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

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

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

letbaz=[3];deletebaz[0];console.log(baz);// [empty]console.log(baz[0]);// undefined

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

[]instanceofArray// trueArray.isArray([])// true

Сортировка

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

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

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

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

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

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

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

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

/* по возрастанию */constascendingComparator=(a,b)=>a-b;// если a > b, то a - b > 0/* более делальная версия, делающая то же самое */constanotherAscendingComparator=(a,b)=>{/* оператор > приводит свои операнды к числу */if(a>b){return1;}if(b>a){return-1;}return0;}/* по убыванию */constdescendingComparator=(a,b)=>b-a;// если a > b, то b - a < 0constnumbers=[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]

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

constenginerComparator=(a,b)=>b.skill-a.skill;constenginers=[{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, в которой он используется.

(functionfn(){console.log(argumentsinstanceofArray);// falseconsole.log(argumentsinstanceofObject);// trueconsole.log(arguments);// { 0: 1, 1: 2, 2: 3 callee: f, length: 3, Symbol(Symbol.iterator): f }for(iofarguments){console.log(i);// 1, 2, 3}})(1,2,3);

Функции

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

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

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

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

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

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

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

constfoo=1;constbar={a: 1};constbaz={c: 1};constfn=(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).

constinc=val=>val+=1;constsum=(a,b)=>a+b;constmul=(a,b)=>{returna*b;};

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

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

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

constArticle=()=>{};constarticle=newArticle();// TypeError: Article is not a constructor

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

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

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

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

constfn=async()=>'Notes';

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

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

ReferenceError

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

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

SyntaxError

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

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

TypeError

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

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

RangeError

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

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

EvalError

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

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

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

URIError

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

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

InternalError

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

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

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

thrownewError(/* ... */);

Promise

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

/* создание Promise */constexecutor=(resolve,reject)=>{/* ... */};constpromise=newPromise(executor);

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

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

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

constonResolved=value=>{/* ... */};constonRejected=reason=>{/* ... */};// функция onResolved сработает при успешном выполненииpromise.then(onResolved);// функция onRejected – при выполнении с ошибкойpromise.then(onResolved,onRejected);promise.catch(onRejected);

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

constexecutor=resolve=>voidsetTimeout(resolve,3000);constpromise=newPromise(executor);promise.then(()=>console.log('resolved!'));// через ~3 секунды выведется 'resolved!'

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

constexecutor=resolve=>voidsetTimeout(()=>resolve('resolved!'),3000);constpromise=newPromise(executor);promise.then(console.log);// через ~3 секунды выведется 'resolved!'

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

constexecutor=(resolve,reject)=>voidsetTimeout(()=>reject('rejected!'),3000);constpromise=newPromise(executor);promise.catch(console.log);// через ~3 секунды выведется 'rejected!'

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

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

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

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

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

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

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

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

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

constslow=newPromise(resolve=>setTimeout(resolve,250,'slow'));constinstant='instant';// тип не Promise , поэтому преобразуется в Promise.resolve('instant')constquick=newPromise(resolve=>setTimeout(resolve,50,'quick'));constonResolved=responses=>responses.map(response=>console.log(response));Promise.all([slow,instant,quick]).then(onResolved);// или то же самое с помощью async/awaittry{constresponses=awaitPromise.all([slow,instant,quick]);responses.map(response=>console.log(response));// 'slow', 'instant', 'quick'}catch(e){/* ... */}

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

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

Promise и async/await

asyncfunctionf(time){constfoo=awaitnewPromise(res=>setTimeout(()=>res('Notes 1'),time));console.log(foo);constbar=awaitPromise.resolve('Notes 2');console.log(bar);awaitPromise.reject('Error');}f(2000);// Notes 1// Notes 2// Uncaught (in promise) Error
f(2000).catch(e=>console.log(e));// Notes 1// Notes 2// Error
constasyncGeneratorStep=(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)=>newPromise((resolve,reject)=>{const_next=value=>voidstep('next',value);const_throw=error=>voidstep('throw',error);constgen=fn(args);functionstep(method,arg){asyncGeneratorStep(gen,resolve,reject,_next,_throw,method,arg);}_next(undefined);});constgenerator=function*(time){constfoo=yieldnewPromise(res=>setTimeout(()=>res('Notes 1'),time));console.log(foo);constbar=yieldPromise.resolve('Notes 2');console.log(bar);yieldPromise.reject('Error');};functionf(){return_asyncToGenerator(generator).apply(this,arguments);}f(2000);// Notes 1// Notes 2// Uncaught (in promise) Error
f(2000).catch(e=>console.log(e));// Notes 1// Notes 2// Error

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

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

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

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

Метод [[Call]]

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

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

object()

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

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

Метод [[Construct]]

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

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

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

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

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

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

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

InstantiateFunctionObject(scope)

Function Declaration

functionBindingIdentifier(FormalParameters){FunctionBody}

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

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

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

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

Function Expression

function(FormalParameters){FunctionBody}

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

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

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

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