Let, Const

  • Объявление переменных через const и let
  • Константы нельзя переопределять
  • В случае с объектом нельзя переопределять саму ссылку, но можно переопределять свойства объекта
  • Локальные переменные видны лишь на уровне блока {...}
  • До кучи локальные переменные в циклах хранят значения для каждой итерации, а не для конечной (это было бы понятно при наличии сложного примера). Просто знайте, если из цикла возвращается только последнее значение итератора, то вам стоит попробовать использовать let
  • Локальные переменные можно переопределять, но нельзя объявлять заново
  • И локальные переменные, и константы видны только после объявления

Destructuring

  • Деструктуризация позволяет присваивать значения сразу нескольким переменным и брать эти значения из массива или объекта
  • Пример с массивом:
    	let [firstL, lastL] = ["JS", "ES"];
    
    Переменные получат значения по порядку
  • Если значения нужно пропустить, то пишем пустые элементы массива (первого). Если значений справа больше, чем слева, то остальные просто будут игнорироваться. Если наоборот, то недостающие будут undefined
  • В случае, если нужно записать все не поместившиеся значения в отдельную переменную, то можно использовать оператор ... (spread). Пример:
    	let [firstL, lastL, ...rest]
    
    Причем в rest будет массив лишних значений. Вполне очевидно (я надеюсь, сука, что очевидно), что спред можно и нужно писать только последним параметром, так как он собирает все параметры, что ещё не были собраны
  • Если хотите обезопаситься от undefined, то можно задать значения по умолчанию (default). Пример:
    	let [firstL="JS", lastL="ES"] = [];
    
    Причем в дефолтное значение можно запихивать и функции. Дефолтные юзаются в случае, если прилетело значение undefined
  • Теперь помучаем объекты:
    	let {var1, var2} = {var1: 1, var2: 2};
    
    Обратите внимание, что по-умолчанию имена переменных и полей в объекте (справа) должны совпадать, чтобы всё прошло нормально
  • Если имена не совпадают, то нужно указать, что куда записывать:
    	let options = {
    		title: "123",
    		width: 100,
    		height: 200
    	};
    	let {width: w, height: h, title} = options;
    
    Таким образом будут присвоены значения переменным w, h, title
  • Дефолтные значения задаются также как и в случае с массивом. До кучи можно комбинировать дефолты с предыдущей фичей:
    	let {width:w=100, height:h=200, title} = options;
    
  • К сожалению, спреда для объектов не завезли, но он поддерживается в Babel в экспериментальном режиме
  • Если будете юзать деструктуризацию с уже существующими переменными, то в случае ошибок всё выражение заключите в круглые скобки. Это проблема с областями видимости локальных переменных. Пример:
    	let a, b;
    	{a, b} = {a: 5, b: 6}; // дерьмище
    	({a, b} = {a: 5, b: 6}); // тоже дерьмище, но зато работает
    
  • Деструктуризацию можно изи засовывать внутрь другой деструктуризации:
    	let options = {
    		size: {
    			width: 100,
    			height: 200
    		},
    		items: ["JS", "ES"]
    	};
    	let {size: {width, height}, items: [item1, item2]} = options;
    

Functions

  • Можно указывать параметры по-умолчанию
    	function show(title="Заголовок"){...};
    
    Дефолтные параметры используются, только если значение либо не было передано, либо был передан undefined
  • Также дефолтные параметры можно задавать как вызовы функций
  • В объявлении функции аргументы можно принимать через спред (отработает в массив)
    	function show(firstL, lastL, ...rest){...};
    
  • Также можно юзать спред и в вызове функции
  • Никто не мешает юзать деструктуризацию в объявлении и вызове функций
  • Баловаться с деструктуризацией нужно аккуратно, и прописывать дефолтные значения для входных параметров, чтобы всегда было, что деструктурировать
  • Так как функция - объект (здравствуйте, блять), теперь можно обращаться к полю name. Там хранится имя этой функции. Удивительно. Имена есть даже у анонимных функций. Но это ой как не точно
  • С какого-то перепуга теперь объявление функций внутри любого блока {...} видно только внутри этого блока. Каеф
  • Да и скорее всего любой блок {...} теперь имеет свою область видимости. Ну так говорят
  • Теперь можно создавать функции через оператор =>
    	let inc = x => x+1; // новое
    	let inc = function(x) {return x+1;}; // во-первых, это тупо выдаст ошибку из-за повторного объявления локальной переменной. Ну а во-вторых, это почти (но не полностью) аналогично предыдущей строке
    
  • Но у этих чудес есть ряд ограничений и особенностей:
    • Если хотите больше одного аргумента, то заключайте их в круглые скобки
    • Не хотите аргументов вообще - всё равно пишите пустые круглые скобки
    • Нужна функция со сложным кодом - пишите фигурные скобки после =>. Также в таком случае не забываем прописывать return, если он вообще нужен
    • У стрелочных функций нет своих this и arguments. В этих переменных хранятся значения из родительской области видимости. По этой причине нельзя использовать стрелочные функции со словом new (как конструкторы)

Strings

  • Новый вид кавычек `строка`
  • В них разрешен перенос строк
  • Можно вставлять значения переменных прямо в строку
    	let lang = "JS";
    	console.log(`This is ${lang}`);
    
    Можно вставлять не только переменные, но и целые выражения и прочую поеботу
  • str.includes(s); - проверяет, содержится ли s в str
  • str.endsWith(s); - проверяет, заканчивается ли str на s
  • str.startsWith(s); - начинается ли
  • str.repeat(times); - повторяет str times раз
  • Ещё там добавили что-то про шаблонизацию, улучшили поддержку unicode и блаблабла. Неинтересно пока что

Objects, Prototypes

  • При объявлении объекта можно использовать сторонние переменные для записи полей в объект. Сложно, но вот пример:
    	let lang = "JS";
    	let course = {
    		lang
    	};
    
    Поле в объекте будет иметь имя lang и значение "JS"
  • Также можно подставлять как имя поля значение какой-либо переменной
    	let lng = "lang";
    	let course = {
    		[lng]: "JS"
    	};
    
    Поле в объекте будет иметь имя lang
  • Причем никто не мешает засовывать туда (в квадратные скобки) целые выражения
  • Геттер для прототипа уже был, а вот и сеттер подъехал
    	Object.setPrototypeOf(obj, newProto);
    
  • Якобы теперь использование __proto__ не считается незаконным
  • Object.assign(target, src1, src2); - копирует свойства из всех src в target, сохраняя свойства самого target. Чем позднее указан источник, тем он приоритетнее. Копирование также идет и вглубь
  • Object.is(obj1, obj2); - сравнивает obj1 и obj2, но не обольщайтесь. Эта штука хоть и делает, что два NaN равны друг другу (при === это не так), и что +0 и -0 это два разных числа, но нормального сравнения объектов пока что не завезли
  • Теперь можно писать методы объекта попроще:
    	let user = {
    		sayHi(){...},
    		name: "John"
    	};
    
    Также попроще пишутся геттеры и сеттеры. Плюсом с этой похабщиной идет свойство (для этих новых методов) [[HomeObject]]. В нём хранится ссылка на объект, которому этот метод принадлежит
  • Приблуда super позволяет в методах (новых) объекта получать свойства его прототипа. Только в методах и только в новых, это важно и связано с [[HomeObject]]. Причем всё это будет работать только в примерно таком коде:
    	let animal = {
    		say() {
    			console.log("Allo")
    		}
    	};
    	let human = {
    		\_\_proto\_\_: animal,
    		say() {
    			super.say()
    		}
    	};
    	human.say();
    
    Пиздец да и только. Можно, конечно, поизъебываться со стрелочными функциями и обычными, но я пока что не знаю, зачем вообще всё это может понадобиться. Возможно, потом столкнусь и перепишу инфу
  • В заключение скажу, что [[HomeObject]] - неизменяемое свойство. Deal with it

Classes

  • Теперь можно создавать классы по-нормальному. Якобы. Давайте взглянем
    	class User {
    		constructor(name) {
    			this.name = name;
    		};
    		sayHi() {
    			alert(this.name);
    		};
    	};
    	let user = new User("Admin");
    	user.sayHi();
    
    Всё из объявления класса идёт прямиком в прототип. Конструктор вызывается стандартно (через new)
  • В отличие от старых костылей новый костыль нельзя вызывать без new и его область видимости аналогична области видимости локальной переменной
  • Методы класса - новые методы, они имеют доступ к super. Ещё их принудили работать через use strict, даже если вы его не прописывали и вам не особо этого хочется
  • Методы класса innumerable, то есть через for..in мы их красивым списочком не получим
  • Никто не мешает ебашить классы как анонимные функции прямо в переменные (ну типа того):
    	let User = class {
    		sayHi() {
    			alert('Allo');
    		};
    	};
    
  • И также как и с функциями, классам в таком присвоении можно давать имена. Эти имена будут доступны только внутри них самих. То есть такой класс же вы сможете создать лишь через присвоенную переменную:
    	let SiteGuest = class User {
    		sayHi() {
    			alert('Allo');
    		};
    	};
    	new SiteGuest().sayHi(); // всё норм
    	new User(); // идите нахуй, нет такого класса
    
    Эту штуковину можно использовать банально для передачи как параметра внутрь чего-угодно
  • Геттеры, сеттеры и вычисляемые свойства (это там где вы как имя поля подставляете не строку, а указываете имя переменной с этим именем) вполне себе поддерживаются и в этих классах
  • В классах не может быть свойств со значениями, только методы (ну и конструктор среди них)
  • Можно также позволить юзать методы класса без создания экземпляра этого класса. Для этого внутри класса пишем:
    	static createGuest() {
    		return new User("Гость");
    	};
    
    И его можно будет вызвать просто по имени класса, а не через экземпляр класса:
    	let user = User.createGuest();
    
  • Наследование простейшее:
    	class User {...};
    	class Admin extends User {...};
    
    Там даже Admin.prototype.__proto__ == User.prototype; // true
  • Constructor родителя наследуется автоматически, если не указан в ребенке явным образом. Для прямого вызова конструктора родителя, необходимо написать super();. Причем super(); можно вызвать только внутри конструктора ребенка, и нельзя в других методах. Обратите внимание, что до вызова super(); указателя this нет

Symbol

  • Новый примитив, служит для создания уникальных идентификаторов
  • По сути, сделан для самого языка, а не для использования простыми смертными
  • Синтаксис такой:
    	let sym = Symbol(); // без new
    
    Тип соответственно symbol
  • Можно указывать имя для символа, чисто чтобы не запутаться (думал я)
    	let sym = Symbol("name");
    
    Но жопс тут в том, что это уникальные штуковины. Два символа с одинаковыми именами неравны
  • Тогда логичный вопрос - нахуя? Логичный ответ - не ебу
  • Причем символы по-умолчанию ведут себя как локальные переменные. Ключевое слово - "по-умолчанию"
  • Существует глобальный реестр этой приблуды. В нём совпадения имен недопустимы. Вроде бы. Читать и писать в реестр довольно легко и доступ туда можно получить откуда-угодно
    	let name = Symbol.for("name");
    	console.log(Symbol.for("name") == name); // true
    
  • Обратите, блять, внимание, что это именно штука для проверки соответствия имени символа к имени переменной этого символа. Всё ещё хуёвее, чем я предполагал
  • Также можно получить имя символа по переменной:
    	Symbol.keyFor(name);
    
    Передаем именно переменную, а не строку. И получаем именно строку с именем символа
  • Если глобального символа с нужным именем в реестре нет - вернет undefined
  • Символы можно подставлять как вычисляемые свойства объекта. И примечательно тут то, что они innumerable и до кучи к ним нельзя обратиться по имени (как-будто бы их нет). Но можно обратится по символу
  • Есть системные символы, которые понадобятся позже по ходу статей. Symbol.toPrimitive - идентификатор для свойства, задающего функцию преобразования объекта в примитив. Symbol.iterator - идентификатор для свойства, задающего функцию итерации по объекту. И так далее

Iterators

  • Итераторы, мать их. Вот вам пример с циклом for. У вас там есть какая-то переменная, значения которой меняются описанным образом до тех пор, пока выполняются условия. Особо там не разгуляешься, но для большей части задач - подходит. Итераторы же это штучка поинтереснее. Для них ещё придумали конструкцию for..of
  • Массив - самый простой итератор
    	let arr = [1, 2, 3];
    	for(let value of arr) {
    		alert(value);
    	};
    
    Тут всё работает так, как и ожидается. Ничего нового, просто синтаксис чутка другой
  • Точно также можем пройтись по строке "Allo" вместо массива. Будут возвращаться буквы по одной, что логично
  • Но что если у нас вот такой объект:
    	let range = {
    		from: 1,
    		to: 5
    	};
    
    По коду очевидно, что мы хотим из этого получить, но как?
  • Используем системный символ
    	range[Symbol.iterator] = function() {
    		let current = this.from;
    		let last = this.to;
    		return {
    			next() {
    				if(current <= last) {
    					return {done: false, value: current++};
    				}
    				else {
    					return {done: true}
    				};
    			};
    		};
    	};
    	for(let num of range) {
    		alert(num);
    	};
    
    Мы пишем в свойство объекта с символьным именем. Там должна быть именно функция, которая возвращает объект с методом next(). Это всё обязательно. А вот уже как этот метод будет отдавать значения - это уже ваша забота. Только существуют ограничения, что должно быть 2 поля: value и done. В value ожидаемо будет передаваемое значение, а в done информация, закончился или нет обход по итератору. Ну и обратите внимание, что тут current меняется прямо в теле next(). Если вам это непонятно, то перечитайте статью с инфой про инкременты и дикременты
  • Метод next() может быть определен в объекте, а не в итераторе. В таком случае итератор нужно будет прописывать внутри объекта, также по символьному имени, но его тело будет содержать только return this;
  • Наличие системного итератора позволяет использовать спред и деструктуризацию
  • Итераторы могут быть бесконечными. Понадобится break
  • Никто не мешает получать встроенные итераторы простым обращением к ним через символы

Map

  • Map - что-то типа объекта, в котором именами свойств может быть что-угодно (даже другой объект), и порядок этих свойств жёстко определен (в обычных объектах свойства могут перебираться например по алфавиту или я хз)
  • Тут, надеюсь, всё понятно
    	let map = new Map();
    	map.set('1', 'str');
    	map.set(1, 123);
    	console.log(map.get(1));
    	console.log(map.get('1');
    
  • В map.size хранится количество элементов в Map
  • Метод set можно чейнить (вызывать цепочно): map.set('1', 'str').set(1, 123);
  • При объявлении Map можно сразу же присвоить значения:
    	let map = new Map([
    		['1', 'str'],
    		[1, 123]
    	]);
    
    В конструктор должен быть передан итерируемый объект, не обязательно массив
  • map.delete(key); - удаляет запись по ключу
  • map.clear(); - очищает Map
  • map.has(key); - проверяет наличие записи по ключу
  • map.keys(); - итерируемый объект с ключами
  • map.values(); - итерируемый объект со значениями
  • map.entries(); - итерируемый объект с записями [ключ, значение]
  • Последние три можно использовать в for..of
  • Обращаю внимание, что перебор свойств идет в том же порядке, в котором происходила запись
  • Также у Map есть свой forEach

Set

  • Set - напоминает массив, в котором нет повторяющихся элементов. Если вы попробуете в него записать что-то, что там уже есть - дубликата не появится
  • Пример
    	let set = new Set();
    	set.add(123);
    	set.add(234);
    	set.add(345);
    	set.add(123);
    	set.add(234);
    	console.log(set.size); // количество элементов = 3
    
  • set.delete(item); - удалить значение
  • set.has(item); - проверка наличия
  • set.clear(); - очистить Set
  • Перебор элементов аналогичен Map

WeakMap, WeakSet

  • WeakMap и WeakSet отличаются от своих "сильных" версий (хыдыхыды) тем, что их нельзя очищать, узнавать их размер, обходить и выводить все элементы. В них можно лишь добавлять/удалять/получать/проверять наличие записи по ключу
  • У WeakMap ключами могут быть только объекты. И если ссылка на этот ключ-объект будет удалена отовсюду (кроме этого нашего WeakMap), то и запись с эти ключом, и ссылка на этот объект будут удалены, но не моментально, а тогда, когда захочет сборщик мусора. Поэтому аккуратнее с такими штуками

Promises

  • Promise - обещание сделать что-то в конце выполнения какого-то кода. Это "что-то" зависит от исхода выполнения кода. Исходов всего два - успех (fulfilled) или ошибка (rejected)
  • К промису цепочкой присоединяются коллбэки (обработчики результата), которые будут брать результат из промиса или предыдущего коллбэка, делать с ним что-то своё и отдавать дальше
  • Пример
    	var promise = new Promise(function(resolve, reject){...});
    
  • Этот код будет выполнен обязательно, и только после его выполнения отрабатывают обработчики. Синтаксис такой:
    	promise.then(onFulfilled, onRejected).then(...)...
    
  • Можно не указывать второй параметр. Также можно передать null в качестве первого параметра. Надеюсь, очевидно, что в таком случае будет происходить и при каких условиях
  • Если нужно поймать именно ошибку, можно писать не then, а catch. Причем, если во всех обработчиках по ходу цепочки нет отлова реджекта, то можно ловить его в самом конце одним единственным catch
  • Если в коде промиса где-то прописать throw, то он будет воспринят как reject
  • Чтобы выйти из промиса с ошибкой - пишем reject() и передаем в него то, что вы хотите передать обработчикам ошибок
  • По аналогии для передачи нормального результата пишем resolve() и отдаем ему что-то, но только одно. Это ограничение действует и на reject
  • Чтобы в обработчиках ловить два этих ответа (один при успехе, другой при ошибке), нужно просто указать переменные, куда их складывать
    	promise
    		.then(
    			result => {...},
    			error => {...}
    		);
    
  • После того как промис принял одно из двух значений (успех или ошибка), вы этот результат уже никак не поменяете
  • Промисы очень удобно использовать, например, для рендера результатов запроса куда-нибудь
  • Обратите внимание, что (чтобы всё нормально работало) из первой функции (выполнение которой ждут все обработчики) должен вернутся промис
  • Также обратите внимание, что если catch возвращает результат работы над ошибкой через return, то этот результат перейдет в ближайший then. Если же ошибку не получилось обработать там - то выкидываем её через throw. Оттуда она попадет в ближайший catch. Если же вы не всё учтете и что-то где-то не поймаете, всё спокойно может рухнуть к хуям
  • Чтобы запустить несколько промисов одновременно и работать с их результатами пачкой, то можно использовать такую конструкцию:
    	Promise.all([
    		promiseFunction(...),
    		promiseFunction(...)
    	]).then(results => {
    		console.log(results);
    	});
    
    Обратите внимание, что нужно передать сюда не обязательно массив. Любой итерируемый объект подойдет. В итоге в results у нас будут лежать результаты всех промисов. Эти результаты будут переданы в обработчики только после того, как будут выполнены все промисы
  • Если же вам нужен результат выполнения только одного промиса из списка (самого шустрого), то используем Promise.race(...);
  • Промис может состоять всего из одной функции: reject или resolve. Вас никто не ограничивает

Generators

  • Генераторы - новый вид функций, который позволяет получать от функции разные значения при разных вызовах
  • Вот такое объявление самого генератора:
    	function* generateSome(){
    		yield 1;
    		yield 2;
    		return 3;
    	};
    
  • Затем создаем экземпляр такого генератора:
    	let generator = generateSome();
    
  • Теперь, чтобы получать значения из этого экземпляра, нужно использовать у него метод next():
    	let one = generator.next();
    
  • В one будет объект {value: 1, done: false}
  • Очевидно, какие значения будут при последующих вызовах. При последнем вызове done: false сменится на true. Дальнейшие обращения к этому генератору будут возвращать {value: undefined, done: true} (по крайней мере в Хроме так)
  • Нужно заново запустить или даже одновременно два? Никто не мешает
  • Звездочку в объявлении можно передвигать к названию. Но лучше не надо
  • Генераторы вполне себе итерируемые, поэтому можно их запихивать в for..of. Только вот учтите, что проитерируются все значения кроме последнего (потому что там return). Для конкретно этого случая можно поменять return на yield, а вот во всех остальных - не стоит
  • Можно загонять генераторы внутрь другого генератора и там шалить с ними:
    	function* generateSequence(start, end) {
    		for (let i = start; i <= end; i++) yield i;
    	};
    	function* generateAlphaNum() {
    		yield* generateSequence(48, 57);
    		yield* generateSequence(65, 90);
    		yield* generateSequence(97, 122);
    	};
    
    Надеюсь, что понятно, что yield* как бы намекает на обращение именно к генератору
  • Через yield можно не только возвращать значения из генератора, но и передавать их внутрь генератора извне:
    	function* gen() {
    		let result = yield "2 + 2?";
    		alert(result);
    	};
    	let generator = gen();
    	let question = generator.next().value;
    	setTimeout(() => generator.next(4), 2000);
    
    По-дурацки, но работает
  • Можно закинуть через .throw() в генератор ошибку и даже написать там обработчик для ошибок, но зачем?
  • Наиболее интересным является использование генераторов для последовательной генерации промисов

Modules

  • Если нужно поделить большой код на куски, то вполне сойдут модули
  • Для начала определяем в файле, что будет передавать наружу:
    	export let one = 1;
    	let two = 2;
    	let three = 3;
    	export {two as var1, three as var2};
    
    Передавать можно что-угодно, но я буду называть их переменными
  • Можно определять внешние названия для экспортируемых переменных через as
  • Нельзя экспортировать переменные без имени
  • В основном файле уже подключаем модуль и необходимые переменные из него:
    	import {two, three} from "./nums"
    
    Последним указывается путь до модуля, можно без расширения
  • В импорте также можно использовать as
  • Можно импортировать вообще всё из модуля через *
  • Также если экспортируется всего одна переменная, то можно у неё прописать export default (дальше синтаксис как выше). Тогда можно будет при импорте писать не объект со списком переменных (пишется именно объект, даже если всего одна переменная), а просто название дефолтной переменной из модуля

Proxy

  • Прокси позволяют модифицировать различные способы обращения к разным переменным и функциям. Проблема только в том, что обращаться нужно будет именно к объекту прокси, через который уже будет идти обращение к переменной
  • Перехватывать можно почти всё, впадлу писать список
  • Пример
    	let user = {};
    	let proxy = new Proxy(user, {
    		get(target, prop) {
    			alert(`Reading ${prop}`);
    			return target[prop];
    		},
    		set(target, prop, value) {
    			alert(`Writing ${prop} ${value}`);
    			target[prop] = value;
    			return true;
    		}
    	});
    	proxy.firstName = "123"; // присваиваем
    	proxy.firstName; // считываем
    
  • Причем вам никто не мешает обращаться к переменной и напрямую (как раньше)
  • Тут уже вы можете разгуляться, как вам позволит фантазия