Основы

Типы

  • Также есть специальные числовые значения Infinity (бесконечность) и NaN.

    Значение NaN обозначает ошибку и является результатом числовой операции, если она некорректна.

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

  • Значение undefined означает «переменная не присвоена».

x = 1;             // число
x = "Тест";        // строка, кавычки могут быть одинарные или двойные
x = true;          // булево значение true/false
x = null;          // спец. значение (само себе тип)
x = undefined;     // спец. значение (само себе тип)
var user = { name: "Вася" };// объекты

тип данных Symbol служит для создания уникальных идентификаторов.

Оператор typeof 

Результатом typeof является строка, содержащая тип:

typeof undefined // "undefined"

typeof 0 // "number"

typeof true // "boolean"

typeof "foo" // "string"

typeof {} // "object"

typeof null // "object"  (1)

typeof function(){} // "function"  (2)

«prompt(вопрос[, по_умолчанию])»

Задать вопрос и возвратить введённую строку, либо null, если посетитель нажал «Отмена».

«confirm(вопрос)»

Задать вопрос и предложить кнопки «Ок», «Отмена». Возвращает, соответственно, true/false.

«alert(сообщение)»

Вывести сообщение на экран.

  • Для сложения строк используется оператор +. Если хоть один аргумент – строка, то другой тоже приводится к строке. Остальные арифметические операторы работают только с числами и всегда приводят аргументы к числу.
  • Сравнение === проверяет точное равенство, включая одинаковый тип. Это самый очевидный и надёжный способ сравнения.

  • Остальные сравнения == < <= > >= осуществляют числовое приведение типа

  • Сравнение строк – лексикографическое, символы сравниваются по своим unicode-кодам.  Метод str1.localeCompare(str2) возвращает -1, если str1 < str2, 1, если str1 > str2 и 0, если они равны

alert( '1' + 2 ); // "12"
alert( 2 - '1' ); // 1
alert( '2' > 1 ); // true, сравнивается как 2 > 1
alert( '01' == 1 ); // true, сравнивается как 1 == 1

alert( null > 0 );  // false, т.к. null преобразовано к 0
alert( null >= 0 ); // true, т.к. null преобразовано к 0
alert( null == 0 ); // false, в стандарте явно указано, что null равен лишь undefined

Постфиксная форма i++ отличается от префиксной ++i тем, что возвращает старое значение, бывшее до увеличения.

Также: значения null и undefined при == равны друг другу и не равны ничему ещё. А при операторах больше/меньше происходит приведение null к 0, а undefined к NaN.

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

  1. Строковое  
    String(val) или  val.toString()
    преобразование происходит наиболее очевидным способом, «как есть»: falseстановится "false", null – "null", undefined – "undefined" и т.п.
  2. Числовое
    в математических функциях и выражениях, а также при сравнении данных различных типов
    Number(val), либо "+ val":

3. логическое

Преобразование к true/false происходит в логическом контексте, таком как if(value), и при применении логических операторов.

Для явного преобразования используется двойное логическое отрицание !!value или вызовBoolean(value).

 строка "0" становится true

Преобразование типов для объектов

  • Строковое – если объект выводится через alert(obj).
  • Численное  – при арифметических операциях, сравнении с примитивом.
  • Логическое  – при if(obj) и других логических операциях.

1. Логическое - всегда True

2. Строковое. Стандартно "[object Object]"

используется его метод toString. Он должен возвращать примитивное значение, причём не обязательно именно строку.

3. Численное. Для численного преобразования используется метод valueOf, который также может возвратить любое примитивное значение. У большинства объектов valueOf не работает (возвращает сам объект и потому игнорируется), при этом для численного преобразования используется toString.

var user = {

  firstName: 'Василий',

  toString: function() {
    return 'Пользователь ' + this.firstName;
  }
};

var room = {
  number: 777,

  valueOf: function() { return this.number; },
  toString: function() { return this.number; }
};

alert( +room );  // 777, вызвался valueOf

delete room.valueOf; // valueOf удалён

alert( +room );  // 777, вызвался toString
alert( new Date() ); // toString: Дата в виде читаемой строки
alert( +new Date() ); // valueOf: кол-во миллисекунд, прошедших с 01.01.1970

// бинарный плюс для даты toString, для остальных объектов valueOf
alert( new Date + "" ); // "строка даты"

Логические операторы

 || (ИЛИ), && (И) и ! (НЕ).

|| запинается на «правде»,
&& запинается на «лжи».

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

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

alert( 0 && 1 ); // 0
alert( 1 && 2 && 3 ); // 3
alert( null || 1 || 2 ); // 1
// 1
while (условие) {
  ...
}

// 2
do {
  ...
} while (условие);

// 3
for (var i = 0; i < 10; i++) {
  ...
}

Поддерживаются директивы break/continue для выхода из цикла/перехода на следующую итерацию.

Для выхода одновременно из нескольких уровней цикла можно задать метку.

outer:
for(;;) {
    ...
  for(;;) {
    ...
    break outer;
  }
}

При сравнениях в конструкции switch используется оператор ===.

var age = prompt('Ваш возраст', 18);

switch (age) {
  case 18:
    alert( 'Никогда не сработает' ); // результат prompt - строка, а не число

  case "18": // вот так - сработает!
    alert( 'Вам 18 лет!' );
    break;

  default:
    alert( 'Любое значение, не совпавшее с case' );
}

Функции

// Function Expression
var f = function() { ... }

// Function Declaration
function f() { ..
  • Если функция объявлена в основном потоке кода, то это Function Declaration.
  • Если функция создана как часть выражения, то это Function Expression.

Структуры данных

isNaN(n) преобразует аргумент к числу и возвращает true, если получилось NaN

Функция isFinite(n) преобразует аргумент к числу и возвращает true, если это не NaN/Infinity/-Infinity:

Функция parseInt и ее аналог parseFloat преобразуют строку символ за символом, пока это возможно. При возникновении ошибки возвращается число, которое получилось. Функция parseInt читает из строки целое число, а parseFloat – дробное.

alert( isFinite(1) ); // true
alert( isFinite(Infinity) ); // false

function isNumeric(n) {
  return !isNaN(parseFloat(n)) && isFinite(n);
}

Числа

Math.floor - округляет вниз

Math.ceil - округляет вверх

Math.round - округляет до ближайшего целого

alert( Math.floor(3.1) );  // 3
alert( Math.ceil(3.1) );   // 4
alert( Math.round(3.1) );  // 3
var n = 12.36;
alert( n.toFixed(1) ); // "12.4"


var price = 6.35;

alert( price.toFixed(1) ); // 6.3
alert( Math.round(price * 10) / 10 ); // 6.4

Строка

charAt(позиция) - получить символ = str[poz]

toLowerCase() и toUpperCase() меняют регистр строки

indexOf(подстрока[, начальная_позиция]) - поиск подстроки. Возвращает позицию, или -1

substring(start, end) возвращает подстроку с позиции start до, но не включая end.

substr(start [, length]) length- количество-символов

slice(start [, end]) как и substring, с отрицательными аргументами отчитывается с конца

var str = "jQuery";
alert( str.charAt(0) ); // "j"
alert( str[0] ); // "j"

var pos = -1;
while ((pos = str.indexOf(target, pos + 1)) != -1) {
  alert( pos );
}

alert( "testme".slice(1, -1) ); // "estm", от 1 позиции до первой с конца.

Объекты

Ассоциативный массив – структура данных, в которой можно хранить любые данные в формате ключ-значение.

В переменной, которой присвоен объект, хранится не сам объект, а «адрес его места в памяти», иными словами – «ссылка» на него. При копировании переменной с объектом – копируется эта ссылка, а объект по-прежнему остается в единственном экземпляре.Так как объект всего один, то изменения через любую переменную видны в других переменных:

  • Существование свойства может проверять оператор in: if ("prop" in obj)
  •  перебор всех свойств из объекта

 

if ("name" in person) {
  alert( "Свойство name существует!" );
}

for (key in obj) {
  /* ... делать что-то с obj[key] ... */
}

var clone = {}; // новый пустой объект

// скопируем в него все свойства user
for (var key in user) {
  clone[key] = user[key];
}

Массивы

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

Длина length – не количество элементов массива, а последний индекс + 1.

new Array(число) создаёт массив заданной длины, без элементов.

Операции с концом массива:

  • arr.push(элемент1, элемент2...) добавляет элементы в конец.
  • var elem = arr.pop() удаляет и возвращает последний элемент.

Операции с началом массива:

  • arr.unshift(элемент1, элемент2...) добавляет элементы в начало.
  • var elem = arr.shift() удаляет и возвращает первый элемент.
// предпочтительное
var arr = [элемент1, элемент2...];

// new Array
var arr = new Array(элемент1, элемент2...);

Массив методы

  • push/pop, shift/unshift, splice – для добавления и удаления элементов.
  • splice(index[, deleteCount, elem1, ...,N]) -удаляет и вставляет
  • split/join – для преобразования строки в массив и обратно.
  • slice – копирует участок массива.
  • sort – для сортировки массива. Если не передать функцию сравнения – сортирует элементы как строки.
  • reverse – меняет порядок элементов на обратный.
  • concat – объединяет массивы.
  • indexOf/lastIndexOf – возвращают позицию элемента в массиве (не поддерживается в IE8-).
  • object.keys(obj) свойства объекта в виде массива
  • forEach – для перебора массива.
  • filter – для фильтрации массива.
  • every/some – для проверки массива.
  • map – для трансформации массива в массив.
  • reduce/reduceRight – для прохода по массиву с вычислением значения.
arr.forEach(function(item, i, arr) {
  alert( i + ": " + item + " (массив:" + arr + ")" );
});

var positiveArr = arr.filter(function(number) {
  return number > 0;
});

var nameLengths = names.map(function(name) {
  return name.length;
});

// для каждого элемента массива запустить функцию,
// промежуточный результат передавать первым аргументом далее
var result = arr.reduce(function(sum, current) {
  return sum + current;
}, 0);

Дата и время

Создание:

  • new Date() - создаёт дату с текущей датой  и временем
  • new Date(milliseconds) -количество мс после 1970 г
  • new Date(datestring)
  • new Date(year, month, date, hours, minutes, seconds, ms)​

Получение компонентов:

  • getFullYear()Получить год(из 4 цифр)
  • getMonth()Получить месяц, от 0 до 11.
  • getDate()Получить число месяца, от 1 до 31.
  • getHours(), getMinutes(), getSeconds(), getMilliseconds()

Установка компонентов:

       set....

 

 

даты можно вычитать, результат вычитания объектов Date – их временная разница, в миллисекундах.

Форматирование даты: 

  • date.toLocaleString(локаль, опции), в котором можно задать много настроек.
  • toString(), toDateString(), toTimeString() без локализации

Date.now() возвращает дату сразу в виде миллисекунд.

Замыкание

В JavaScript все глобальные переменные и функции являются свойствами специального объекта, который называется «глобальный объект» (global object). В браузере window

В результате инициализации, к началу выполнения кода:

  1. Функции, объявленные как Function Declaration, создаются полностью и готовы к использованию.
  2. Переменные объявлены, но равны undefined. Присваивания выполнятся позже, когда выполнение дойдет до них.

Замыкание – это функция вместе со всеми внешними переменными, которые ей доступны.

Говоря «замыкание функции», подразумевают не саму эту функцию, а именно внешние переменные.

  1. Все переменные и параметры функций являются свойствами объекта переменных LexicalEnvironment. Каждый запуск функции создает новый такой объект. На верхнем уровне им является «глобальный объект», в браузере – window.
  2. При создании функция получает системное свойство [[Scope]], которое ссылается на LexicalEnvironment, в котором она была создана.
  3. При вызове функции, куда бы её ни передали в коде – она будет искать переменные сначала у себя, а затем во внешних LexicalEnvironment с места своего «рождения».

При создании функции с использованием new Function, её свойство [[Scope]] ссылается не на текущийLexicalEnvironment, а на window.=>  такие функции не могут использовать замыкание. Но это хорошо, так как бережёт от ошибок проектирования, да и при сжатии JavaScript проблем не будет.

function makeCounter() {
  var currentCount = 1;

  // возвращаемся к функции
  function counter() {
      return currentCount++;
    }

  // ...и добавляем ей методы!
  counter.set = function(value) {
    currentCount = value;
  };

  counter.reset = function() {
    currentCount = 1;
  };

  return counter;
}

var counter = makeCounter();

alert( counter() ); // 1
alert( counter() ); // 2

counter.set(5);
alert( counter() ); // 5

Модуль  – это оборачивание пакета функционала в единую внешнюю функцию, которая тут же выполняется.

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

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

«модуль» – это всего лишь функция-обёртка для скрытия переменных.

(function() {

  alert( "объявляем локальные переменные, функции, работаем" );
  // ...

}());

на месте» разрешено вызывать только Function Expression.

Контекст вызова

Для доступа к текущему объекту из метода используется ключевое слово this. Значением this является объект перед «точкой», в контексте которого вызван метод,

Значение this называется контекстом вызова и будет определено в момент вызова функции. ( без контекста

[object Window]или undefined )

Ссылочный тип (Reference Type) – точка возвращает не функцию а ссылочный тип:

  • Скобки () получают из base значение свойства name и вызывают в контексте base.
  • Другие операторы получают из base значение свойства name и используют, а остальные компоненты игнорируют.

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

var user = {
  name: "Вася",
  hi: function() { alert(this.name); },
  bye: function() { alert("Пока"); }
};

user.hi(); // Вася (простой вызов работает)

// а теперь вызовем user.hi 
или user.bye в зависимости от имени
(user.name == "Вася" ? 
user.hi : user.bye)(); // undefined

Конструкторы

Конструкторы -  функции, которые предназначены для создания объектов . Названия пишутся с большой буквы

Любая функция может быть вызвана с new, при этом она получает новый пустой объект в качестве this, в который она добавляет свойства.
Вызов return с объектом вернёт объект, а с чем угодно, кроме объекта – возвратит, как обычно, this.

function Animal(name) {
  // this = {};

  // в this пишем свойства, методы
  this.name = name;
  this.canWalk = true;

  // return this;
}
function User(firstName, lastName) {
  // вспомогательная переменная
  var phrase = "Привет";

  //  вспомогательная вложенная функция
  function getFullName() {
      return firstName + " " + lastName;
    }

  this.sayHi = function() {
    alert( phrase + ", " + getFullName() ); // использование
  };
}

Явное указание this: call/ apply

func.call(context, arg1, arg2, ...)

Вызов func.call(context, a, b...) – то же, что обычный вызов func(a, b...), но с явно указанным this(=context).

 «одалживание» метода -  вызвать  метод одного объекта, в том числе встроенного,  в контексте другого.

// вызов arr.slice() скопирует все элементы из this в новый массив
  var args = [].slice.call(arguments);

Вызов функции при помощи func.apply работает аналогично func.call, но принимает массив аргументов вместо списка.

func.call(context, arg1, arg2);
// идентичен вызову
func.apply(context, [arg1, arg2]);
// получить максимум из элементов arr
alert( Math.max.apply(null, arr) ); // 5

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

function bind(context) {
   let func = this;
  return function() { // (*)
    return func.apply(context, arguments);
  };
}

в результате вызова bind(func, context) мы получаем «функцию-обёртку», которая прозрачно передаёт вызов в func, с теми же аргументами, но фиксированным контекстом context.

var wrapper = func.bind(context[, arg1, arg2...])

Если указаны аргументы arg1, arg2... – они будут прибавлены к каждому вызову новой функции, причем встанут перед теми, которые указаны при вызове.

Карринг (currying) или каррирование – термин функционального программирования, который означает создание новой функции путём фиксирования аргументов существующей.

function mul(a, b) {
  return a * b;
};
// double умножает только на два
var double = mul.bind(null, 2);
 // контекст фиксируем null, он не
 используется
Object.defineProperty(obj, prop, descriptor)-метод для управления свойствами

Дескрипторы

descriptor – объект, который описывает поведение свойства. В нём мб следующие поля:

  • value – значение свойства, по умолчанию undefined
  • writable – значение свойства можно менять, если true. По умолчанию false.
  • configurable – если true, то свойство можно удалять, а также менять его в дальнейшем при помощи новых вызовов defineProperty. По умолчанию false.
  • enumerable – если true, то свойство просматривается в цикле for..in и методе Object.keys(). По умолчанию false.
  • get – функция, которая возвращает значение свойства. По умолчанию undefined.
  • set – функция, которая записывает значение свойства. По умолчанию undefined.

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

Методы объектов

Object.freeze(obj) Запрещает добавление, удаление и изменение свойств, все текущие свойства делает configurable: false, writable: false.

// 2. указание значения через дескриптор (удаляемое, изменяемое, перечисляемое) свойство.
Object.defineProperty(user, "name", { value: "Вася", configurable: true, writable: true, enumerable: true });

Object.defineProperty(user, "name", {
  value: "Вася",
  writable: false, // запретить присвоение "user.name="
  configurable: false // запретить удаление "delete user.name"
});

// помечаем toString как не подлежащий перебору в for..in
Object.defineProperty(user, "toString", {enumerable: false});
Object.defineProperty(user, "fullName", {

  get: function() {
    return this.firstName + ' ' + this.surname;
  },

  set: function(value) {
      var split = value.split(' ');
      this.firstName = split[0];
      this.surname = split[1];
    }
});

user.fullName = "Петя Иванов";
alert( user.firstName ); // Петя
alert( user.surname ); // Иванов
var user = {
  firstName: "Вася",
  surname: "Петров",

  get fullName() {
    return this.firstName + ' ' + this.surname;
  },

  set fullName(value) {
    var split = value.split(' ');
    this.firstName = split[0];
    this.surname = split[1];
  }
};

alert( user.fullName ); // Вася Петров (из геттера)

user.fullName = "Петя Иванов";
alert( user.firstName ); // Петя  (поставил сеттер)
alert( user.surname ); // Иванов (поставил сеттер)

В литералах

Статические и фабричные методы

Методы и свойства, которые не привязаны к конкретному экземпляру объекта, называют «статическими». Их записывают прямо в саму функцию-конструктор.

function Article() {
  Article.count++;
}

Article.count = 0; // статическое свойство-переменная
Article.DEFAULT_FORMAT = "html"; // статическое свойство-константа

Они хранят данные, специфичные не для одного объекта, а для всех статей целиком.

Journal.compare = function(journalA, journalB) {
  return journalA.date - journalB.date;
};

function findMin(journals) {
  var min = 0;
  for (var i = 0; i < journals.length; i++) {
    // используем статический метод
    if (Journal.compare(journals[min], journals[i]) > 0) min = i;
  }
  return journals[min];
}

«Фабричный статический метод» –  статический метод, который служит для создания новых объектов

function User() {
  this.sayHi = function() {
    alert(this.name)
  };
}

User.createAnonymous = function() {
  var user = new User;
  user.name = 'Аноним';
  return user;
}

User.createFromData = function(userData) {
  var user = new User;
  user.name = userData.name;
  user.age = userData.age;
  return user;
}

// Использование

var guest = User.createAnonymous();
guest.sayHi(); // Аноним

var knownUser = User.createFromData({
  name: 'Вася',
  age: 25
});
knownUser.sayHi(); // Вася
function User(userData) {
  if (userData) { // если указаны данные -- одна ветка if
    this.name = userData.name;
    this.age = userData.age;
  } else { // если не указаны -- другая
    this.name = 'Аноним';
  }

  this.sayHi = function() {
    alert(this.name)
  };
  // ...
}

// Использование

var guest = new User();
guest.sayHi(); // Аноним

var knownUser = new User({
  name: 'Вася',
  age: 25
});
knownUser.sayHi(); // Вася
  • Лучшая читаемость кода. Как конструктора – вместо одной большой функции несколько маленьких, так и вызывающего кода – явно видно, что именно создаётся.
  • Лучший контроль ошибок, т.к. если в createFromData ничего не передали, то будет ошибка, а полиморфный конструктор создал бы анонимного посетителя.
  • Удобная расширяемость. Например, нужно добавить создание администратора, без аргументов. Фабричный метод сделать легко: User.createAdmin = function() { ... }. А для полиморфного конструктора вызов без аргумента создаст анонима, так что нужно добавить параметр – «тип посетителя» и усложнить этим код.

Преимущества: 

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

А в остальных случаях отличная альтернатива – фабричные методы.

Декораторы

Декоратор – приём программирования, который позволяет взять существующую функцию и изменить/расширить ее поведение.

Декоратор получает функцию и возвращает обертку, которая делает что-то своё «вокруг» вызова основной функции.

Некоторые другие возможности

Определение типа данных

  •  typeof для примитивов (null=object)
  • Array.isArray(arr)
  • скрытое свойство [[Class]] для встроенных объектов
  • obj instanceof Func.  Оператор obj instanceof Func проверяет тот факт, что obj является результатом вызова new Func. Он учитывает цепочку __proto__, поэтому наследование поддерживается.
  • утиная типизация( смысл в в проверке необходимых методов и свойств)
function getClass(obj) {
  return {}.toString.call(obj).slice(8, -1);
}
if (something.splice) {
  alert( 'Это утка! То есть, массив!' );
}

if (x.getTime) {
  alert( 'Дата!' );
  alert( x.getTime() ); // работаем с датой
}

JSON

 JSON - формат , который используется для представления объектов в виде строки. Используется для передачи по сети

  • JSON.parse – читает объекты из строки в формате JSON. Для интеллектуального восстановления из строки у JSON.parse(str, reviver) есть второй параметр reviver, который является функцией function(key, value).
  • JSON.stringify – превращает объекты в строку в формате JSON, используется, когда нужно из JavaScript передать данные по сети.Во втором параметре JSON.stringify(value, replacer) можно указать массив свойств, которые подлежат сериализации.

При сериализации объекта вызывается его метод toJSON.

Если такого метода нет – перечисляются его свойства, кроме функций.

var numbers = "[0, 1, 2, 3]";

numbers = JSON.parse(numbers);

alert( numbers[1] ); // 1

var str = '{"title":"Конференция",
"date":"2014-11-30T12:00:00.000Z"}';

var event = JSON.parse(str, function(key, value) {
  if (key == 'date') return new Date(value);
  return value;
});
var room = {
  number: 23,
  occupy: function() {
    alert( this.number );
  }
};

alert( JSON.stringify(room) );
/*
  {
    "number":23"            
  }
*/

var room = {
  number: 23,
  toJSON: function() {
      return this.number;
    }
};

alert( JSON.stringify(room) ); // 23
var user = {
  name: "Вася",
  age: 25,
  window: window
};

var str = JSON.stringify(user, function(key, value) {
  if (key == 'window') return undefined;
  return value;
});

setTimeout и setInterval

Методы setInterval(func, delay) и setTimeout(func, delay) позволяют запускать func регулярно/один раз через delay миллисекунд.

Оба метода возвращают идентификатор таймера. Его используют для остановки выполнения вызовом clearInterval/clearTimeout.

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

Минимальная задержка по стандарту составляет 4 мс.

Рекурсивный setTimeout гарантирует паузу между вызовами, setInterval – нет.

function func(phrase, who) {
  alert( phrase + ', ' + who );
}

setTimeout(func, 1000, "Привет", "Вася"); // Привет, Вася

var timerId = setTimeout(function() { alert(1) }, 1000);
alert(timerId); // число - идентификатор таймера

clearTimeout(timerId);
var timerId = setInterval(function() {
  alert( "тик" );
}, 2000);

// через 5 сек остановить повторы
setTimeout(function() {
  clearInterval(timerId);
  alert( 'стоп' );
}, 5000);
var timerId = setTimeout(function tick() {
  alert( "тик" );
  timerId = setTimeout(tick, 2000);
}, 200
function mul(a, b) {
	return a * b;
}

function square(a) {
	return mul(a, a);
}

function printSquare() {
	console.log(square(42));
}

printSquare();

Callstack

  • push printSquare
  • push square
  • push mul
  • pop  mul
  • pop  square
  • pop  printSquare
function asyncJob() {
  setTimeout(fucntion job() {
    console.log("I'm done");
  }, 5000);
}

asyncJob();
  • push asyncJob
  • push setTimeout
  • push job to callback queue
  • pop  setTimeout
  • pop  asyncJob
  • now WebAPI waits 5000ms
  • push job
  • pop job

ООП в функциональном стиле

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

Классом в объектно-ориентированной разработке называют шаблон/программный код, предназначенный для создания объектов и методов.

Методы и свойства объекта  разделяются на две группы:

  • Внутренний интерфейс – это свойства и методы, доступ к которым может быть осуществлен только из других методов объекта, их также называют «приватными» (есть и другие термины, встретим их далее).
  • Внешний интерфейс – это свойства и методы, доступные снаружи объекта, их называют «публичными».

Локальные переменные, включая параметры конструктора, можно считать приватными свойствами.

Свойства, записанные в this, можно считать публичными.

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

Для лучшего контроля над свойством его делают приватным, а запись значения осуществляется через специальный метод, который называют «сеттер» (setter method).

название для сеттера – setСвойство

var waterAmount = 0;

// "умная" установка свойства
  this.setWaterAmount = function(amount) {
    if (amount < 0) {
      throw new Error("Значение должно быть положительным");
    }
    if (amount > capacity) {
      throw new Error("Нельзя залить воды больше, чем " + capacity);
    }

    waterAmount = amount;
  };

this.getWaterAmount = function() {
    return waterAmount;
  };

Для того, чтобы дать возможность внешнему коду узнать его значение, создадим специальную функцию – «геттер» (getter method).

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

При вызове без параметров такой метод возвращает свойство, а при передаче параметра – назначает его.

 инкапсуляция - отделение и защита внутреннего интерфейса

Плюсы:

  1. Защита пользователей от выстрела себе в ногу.  Если пользователь объекта будет менять то, что не рассчитано на изменение снаружи – последствия могут быть непредсказуемыми.
  2. Удобство в поддержке. При наличии чётко выделенного внешнего интерфейса, разработчик может свободно менять внутренние свойства и методы, без оглядки на коллег. Гораздо легче разрабатывать, если знаешь, что ряд методов (все внутренние) можно переименовывать, менять их параметры, и вообще, переписать как угодно, так как внешний код к ним абсолютно точно не обращается.
  3. Управление сложностью. Всегда удобно, когда детали реализации скрыты, и доступен простой, понятно документированный внешний интерфейс.
  1.  вызвать getBoilTime с явным указанием контекста: getBoilTime.call(this)
  2. привязать getBoilTime к объекту через bind
  3. Сохранение This в замыкании. предварительно скопировать this во вспомогательную переменную и обращаться из внутренних функций уже к ней.
 this.run = function() {
    setTimeout(onReady, getBoilTime.call(this)); 
  };

var getBoilTime = function() {
    return this.waterAmount * WATER_HEAT_CAPACITY * 80 / power;
  }.bind(this);

 var self = this;

  function getBoilTime() {
      return self.waterAmount * WATER_HEAT_CAPACITY * 80 / power;
    }

Наследование

Наследование – создание новых «классов» на основе существующих.

Механизм наследования позволяет определить базовый класс Машина, в нём описать то, что свойственно всем машинам, а затем на его основе построить другие, более конкретные: Кофеварка и т.п.

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

  1. Объявляется конструктор родителя Machine. В нём могут быть приватные (private), публичные (public) и защищённые (protected) свойства:
function Machine(params) {
  // локальные переменные и функции доступны только внутри Machine
  var privateProperty;

  // публичные доступны снаружи
  this.publicProperty = ...;

  // защищённые доступны внутри Machine и для потомков
  // мы договариваемся не трогать их снаружи
  this._protectedProperty = ...
}

var machine = new Machine(...)
machine.public();

2. Для наследования конструктор потомка вызывает родителя в своём контексте через apply. После чего может добавить свои переменные и методы:

function CoffeeMachine(params) {
  // универсальный вызов с передачей любых аргументов
  Machine.apply(this, arguments);// передавая ей в качестве контекста this текущий объект

  this.coffeePublicProperty = ...
}

3. В CoffeeMachine свойства, полученные от родителя, можно перезаписать своими. Но обычно требуется не заменить, а расширить метод родителя. Для этого он предварительно копируется в переменную:

function CoffeeMachine(params) {
  Machine.apply(this, arguments);

  var parentProtected = this._protectedProperty;
  this._protectedProperty = function(args) {
    parentProtected.apply(this, args); // (*)
    // ...
  };
}

Строку (*) можно упростить до parentProtected(args), если метод родителя не использует this, а, например, привязан к var self = this:

function Machine(params) {
  var self = this;

  this._protected = function() {
    self.property = "value";
  };
}

ООП в прототипном стиле

  • В JavaScript есть встроенное «наследование» между объектами при помощи специального свойства __proto__.
  • При установке свойства rabbit.__proto__ = animal говорят, что объект animal будет «прототипом» rabbit.
  • При чтении свойства из объекта, если его в нём нет, оно ищется в __proto__. Прототип задействуется только при чтении свойства. Операции присвоения obj.prop = или удаления delete obj.propсовершаются всегда над самим объектом obj.

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

прототип – это «резервное хранилище свойств и методов» объекта, автоматически используемое при поиске.

__proto__=[[Prototype]]

Вызов obj.hasOwnProperty(prop) возвращает true, если свойство prop принадлежит самому объекту obj, иначе false.

Object.getPrototypeOf(obj) Возвращает obj.__proto__ (кроме IE8-)

Object.setPrototypeOf(obj, proto) Устанавливает obj.__proto__ = proto (кроме IE10-).

Создание объекта с прототипом: Object.create(proto, descriptors)

Создаёт пустой объект с __proto__, равным первому аргументу (кроме IE8-), второй необязательный аргумент может содержать дескрипторы свойств.

Чтобы новым объектам автоматически ставить прототип, конструктору ставится свойство prototype.

При создании объекта через new, в его прототип __proto__ записывается ссылка из prototype функции-конструктора.

var animal = {
  eats: true
};

function Rabbit(name) {
  this.name = name;
}

Rabbit.prototype = animal;

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

function Rabbit() {}

Rabbit.prototype = {
  constructor: Rabbit
};
function Rabbit(name) {
  this.name = name;
  alert( name );
}

var rabbit = new Rabbit("Кроль");

var rabbit2 = new rabbit.constructor("Крольчиха");
Object.myCreate=function (proto) {
  function F() {};
  F.prototype = proto;
  var object = new F;
  return object;
}

"все объекты наследуют от Object", а если более точно, то от Object.prototype.,  у которого __proto__ равно null.

«Псевдоклассом» или, более коротко, «классом», называют функцию-конструктор вместе с её prototype.

Если обратиться к свойству примитива, то будет создан объект соответствующего типа, например new String для строки, new Number для чисел, new Boolean – для логических выражений.

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

Добавление методов в Object.prototype, если оно не сопровождается Object.defineProperty с установкой enumerable (IE9+), «сломает» циклы for..in, поэтому стараются в этот прототип методы не добавлять.

Object.defineProperty(Object.prototype, 'each', {
  enumerable: false
});

Функциональный стиль записывает в каждый объект и свойства и методы, а прототипный – только свойства. Поэтому прототипный стиль – быстрее и экономнее по памяти.

Rabbit.prototype.__proto__ = Animal.prototype;
Rabbit.prototype = Object.create(Animal.prototype);
Rabbit.prototype.constructor = Rabbit;

Однако, прямой доступ к __proto__ не поддерживается в IE10-, поэтому мы используем функцию Object.create.

// --------- Класс-Родитель ------------
// Конструктор родителя пишет свойства конкретного объекта
function Animal(name) {
  this.name = name;
  this.speed = 0;
}

// Методы хранятся в прототипе
Animal.prototype.run = function() {
  alert(this.name + " бежит!")
}

// --------- Класс-потомок -----------
// Конструктор потомка
function Rabbit(name) {
  Animal.apply(this, arguments);
}

// Унаследовать
Rabbit.prototype = Object.create(Animal.prototype);

// Желательно и constructor сохранить
Rabbit.prototype.constructor = Rabbit;

// Методы потомка
Rabbit.prototype.run = function() {
  // Вызов метода родителя внутри своего
  Animal.prototype.run.apply(this, arguments);
  alert( this.name + " подпрыгивает!" );
};

// Готово, можно создавать объекты
var rabbit = new Rabbit('Кроль');
rabbit.run();

Примесь (англ. mixin) – класс или объект, реализующий какое-либо чётко выделенное поведение. Используется для уточнения поведения других классов, не предназначен для самостоятельного использования.

// примесь
var sayHiMixin = {
  sayHi: function() {
    alert("Привет " + this.name);
  },
  sayBye: function() {
    alert("Пока " + this.name);
  }
};

// использование:
function User(name) {
  this.name = name;
}

// передать методы примеси
for(var key in sayHiMixin) User.prototype[key] = sayHiMixin[key];

// User "умеет" sayHi
new User("Вася").sayHi(); // Привет Вася

Самый простой вариант примеси – это объект с полезными методами, которые мы просто копируем в нужный прототип.

Объект мог специальным вызовом генерировать «уведомление о событии», а на эти уведомления другие объекты могли «подписываться», чтобы их получать.

var eventMixin = {
  /**
   * Подписка на событие
   * Использование:
   *  menu.on('select', function(item) { ... }
  */
  on: function(eventName, handler) {
    if (!this._eventHandlers) this._eventHandlers = {};
    if (!this._eventHandlers[eventName]) {
      this._eventHandlers[eventName] = [];
    }
    this._eventHandlers[eventName].push(handler);
  },
  /**
   * Прекращение подписки
   *  menu.off('select',  handler)
   */
  off: function(eventName, handler) {
    var handlers = this._eventHandlers && this._eventHandlers[eventName];
    if (!handlers) return;
    for(var i=0; i<handlers.length; i++) {
      if (handlers[i] == handler) {
        handlers.splice(i--, 1);
      }
    }
  },
  /**
   * Генерация события с передачей данных
   *  this.trigger('select', item);
   */
  trigger: function(eventName /*, ... */) {

    if (!this._eventHandlers || !this._eventHandlers[eventName]) {
      return; // обработчиков для события нет
    }

    // вызвать обработчики
    var handlers = this._eventHandlers[eventName];
    for (var i = 0; i < handlers.length; i++) {
      handlers[i].apply(this, [].slice.call(arguments, 1));
    }
  }
};
// Класс Menu с примесью eventMixin
function Menu() {
  // ...
}

for(var key in eventMixin) {
  Menu.prototype[key] = eventMixin[key];
}

// Генерирует событие select при выборе значения
Menu.prototype.choose = function(value) {
  this.trigger("select", value);
}

// Создадим меню
var menu = new Menu();

// При наступлении события select вызвать эту функцию
menu.on("select", function(value) {
  alert("Выбрано значение " + value);
});

// Запускаем выбор (событие select вызовет обработчики)
menu.choose("123");

JS6

Переменные: let и const

Переменные let:

  • Видны только после объявления и только в текущем блоке.
  • Нельзя переобъявлять (в том же блоке).
  • При объявлении переменной в цикле for(let …) – она видна только в этом цикле. Причём каждой итерации соответствует своя переменная let.

Переменная const – это константа, в остальном – как let.

let apples = 5; // (*)

if (true) {
  let apples = 10;

  alert(apples); // 10 (внутри блока)
}

alert(apples); // 5 (снаружи блока значение не изменилось)

Деструктуризация  (destructuring assignment) позволяет разбивать объект или массив на переменные при присвоении

Деструкторизация

let [firstName, lastName] = ["Илья", "Кантор"];

alert(firstName); // Илья
alert(lastName);  // Кантор

оператора "..."(«spread») получит «всё остальное»

let [firstName, lastName, ...rest] = "Ю Ц Имп Рима".split(" ");

alert(rest);      // Император,Рима (массив из 2х элементов)
// значения по умолчанию
let [firstName="Гость", lastName="Анонимный"] = [];

// lastName получит значение, соответствующее текущей дате:
let [firstName, lastName=defaultLastName()] = ["Вася"];
// первый и второй элементы не нужны
let [, , title] = "Юлий Цезарь Император Рима".split(" ");

let options = {
  title: "Меню",
  width: 100,
  height: 200
};

let {title, width, height} = options;

let {width: w, height: h, title} = options;
let a, b;
({a, b} = {a:5, b:6}); // внутри выражения это уже не блок

Синтаксис:

Здесь двоеточие : задаёт отображение свойства prop в переменную varName, а равенство =default задаёт выражение, которое будет использовано, если значение отсутствует (не указано или undefined).

Для массивов имеет значение порядок, поэтому нельзя использовать :, но значение по умолчанию – можно:

 

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

  • Вложенные объекты и массивы тоже работают, при деструктуризации нужно лишь сохранить ту же структуру, что и исходный объект/массив.

let {prop : varName = default, ...} = object
let [var1 = default, var2, ...rest] = array
  • Array.from() создаёт новый экземпляр Array из массивоподобного или итерируемого объекта.
  • Метод Array.of() создаёт новый экземпляр массива Array из произвольного числа агрументов, вне зависимости от числа или типа аргумента.
  • Метод fill() заполняет все элементы массива от начального до конечного индексов одним значением.
  • метод find(), который возвращает значение найденного в массиве элемента вместо его индекса.
  • Метод findIndex() возвращает индекс в массиве, если элемент удовлетворяет условию проверяющей функции. В противном случае возвращается -1.

Метод Number.isInteger() определяет, является ли переданное значение целым числом.

Функции

  • Можно задавать параметры по умолчанию, а также использовать деструктуризацию для чтения приходящего объекта.
  • Оператор spread (троеточие) в объявлении позволяет функции получать оставшиеся аргументы в массив: function f(arg1, arg2, ...rest).
  • Тот же оператор spread в вызове функции позволяет передать её массив как список аргументов (вместо apply).
  • У функции есть свойство name, оно содержит имя, указанное при объявлении функции, либо, если его нет, то имя свойства или переменную, в которую она записана. Есть и некоторые другие ситуации, в которых интерпретатор подставляет «самое подходящее» имя.
  • Объявление Function Declaration в блоке {...} видно только в этом блоке.
  • Появились функции-стрелки:
    • Без фигурных скобок возвращают выражение expr: (args) => expr.
    • С фигурными скобками требуют явного return.
    • Не имеют своих this и arguments и super, при обращении получают их из окружающего контекста.
    • Не могут быть использованы как конструкторы, с new.
function showMenu(title = "Заголовок", width = 100, height = 200) {
}
// title=Заголовок width=null height=200
showMenu(undefined, null);

по умолчанию - при передаче undefined

function showName(firstName, lastName, ...rest) {
  alert(firstName + ' ' + lastName + ' - ' + rest);
}

// выведет: Юлий Цезарь - Император,Рима
showName("Юлий", "Цезарь", "Император", "Рима");
let numbers = [2, 3, 15];

// Оператор ... в вызове передаст
 массив как список аргументов
// Этот вызов аналогичен 
Math.max(2, 3, 15)
let max = Math.max(...numbers);

Math.max(...numbers);
Math.max.apply(Math, numbers);
let options = {
  title: "Меню"
};

function showMenu({title="Заголовок", width:w=100, height:h=200}) {
  alert(title + ' ' + w + ' ' + h);
}
// для вызова без аргументов
function showMenu({title="Заголовок", width:w=100, height:h=200} = {}) {
  alert(title + ' ' + w + ' ' + h);
}

if (true) {

  sayHi(); // работает

  function sayHi() {
    alert("Привет!");
  }

}
sayHi(); // ошибка, функции не существует
let inc = x => x+1;
//let inc = function(x) { return x + 1; };

let sum = (a,b) => a + b;
// let inc = function(a, b) { return a + b; };

let getTime = () => {
  let date = new Date();
  let hours = date.getHours();
  let minutes = date.getMinutes();
  return hourse + ':' + minutes;
};

let sorted = arr.sort( (a,b) => a - b );
function defer(f, ms) {
  return function() {
    setTimeout(() => f.apply(this, arguments), ms)
  }
}

function defer(f, ms) {
  return function() {
    let args = arguments;
    let ctx = this;
    setTimeout(function() {
      return f.apply(ctx, args);
    }, ms);
  }
}

Строки

let str = `обратные кавычки`;

Добавлены ряд полезных методов общего назначения:

  • str.includes(s) – проверяет, включает ли одна строка в себя другую, возвращает true/false.
  • str.endsWith(s) – возвращает true, если строка str заканчивается подстрокой s.
  • str.startsWith(s) – возвращает true, если строка str начинается со строки s.
  • str.repeat(times) – повторяет строку str times раз.
  • В них разрешён перевод строки.

  • Можно вставлять выражения при помощи ${…}.

  • + функции-щаблонизаторы

let apples = 2;
let oranges = 3;

alert(`${apples} + ${oranges} 
= ${apples + oranges}`);
 // 2 + 3 = 5

Объекты и прототипы

Улучшения в описании свойств:

  • Запись name: name можно заменить на просто name
  • Если имя свойства находится в переменной или задано выражением expr, то его можно указать в квадратных скобках [expr].
  • Свойства-функции можно оформить как методы: "prop: function() {}" → "prop() {}". У них есть специального внутреннего свойства [[HomeObject]] , ссылающийся на объект, которому метод принадлежит.

В методах работает обращение к свойствам прототипа через super.parentProperty.

Для работы с прототипом:

  • Object.setPrototypeOf(obj, proto) – метод для установки прототипа.
  • obj.__proto__ – ссылка на прототип.

Дополнительно:

  • Метод Object.assign(target, src1, src2...) – копирует свойства из всех аргументов в первый объект.
  • Метод Object.is(value1, value2) проверяет два значения на равенство. В отличие от === считает +0 и -0 разными числами. А также считает, что NaN равно самому себе.
let name = "Вася";
let isAdmin = true;

let user = {
  name,
  isAdmin
};
let a = "Мой ";
let b = "Зелёный ";
let c = "Крокодил";

let user = {
  [(a + b + c).toLowerCase()]: "Вася"
};

alert( user["мой зелёный крокодил"] ); // Вася
// clone = пустой объект + все свойства user
let clone = Object.assign({}, user);
let name = "Вася";
let user = {
  name,
  // вместо "sayHi: function() {" пишем "sayHi() {"
  sayHi() {
    alert(this.name);
  }
};
let user = {
  name,
  surname,
  get fullName() {
    return `${name} ${surname}`;
  }
};

alert( user.fullName ); // Вася Петров

При обращении через super используется [[HomeObject]] текущего метода, и от него берётся __proto__. Поэтому super работает только внутри методов.

let rabbit = {
  __proto__: animal,
  walk() {
    alert(super.walk); // walk() { … }
    super.walk(); // I'm walking

    setTimeout(() => super.walk()); // I'm walking
  }
};

используется super внешней функции.

  • Классы можно объявлять как в основном потоке кода, так и «инлайн», по аналогии с Function Declaration и Expression.
  • В объявлении классов можно использовать методы, геттеры/сеттеры и вычислимые названия методов.
  • При наследовании вызов конструктора родителя осуществляется через super(...args), вызов родительских методов – через super.method(...args).

Классы

class Название [extends Родитель]  {
  constructor
  методы
}

Функция constructor запускается при создании new User, остальные методы записываются в User.prototype.

Статические свойства класса User – это свойства непосредственно User, то есть доступные из него «через точку». Для их объявления используется ключевое слово static.

A.isPrototypeOf(B);

Конструктор constructor родителя наследуется автоматически. То есть, если в потомке не указан свой constructor, то используется родительский

Если же у потомка свой constructor, то, чтобы в нём вызвать конструктор родителя – используется синтаксис super() с аргументами для родителя.

class User {

  constructor(name) {
    this.name = name;
  }

  sayHi() {
    alert(this.name);
  }

}

let user = new User("Вася");
user.sayHi(); // Вася
function User(name) {
  this.name = name;
}

User.prototype.sayHi = function() {
  alert(this.name);
};
class User {
  constructor(firstName, lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
  }

  static createGuest() {
    return new User("Гость", "Сайта");
  }
};

let user = User.createGuest();
class Animal {
  constructor(name) {
    this.name = name;
  }

  walk() {
    alert("I walk: " + this.name);
  }
}

class Rabbit extends Animal {
 constructor() {
    // вызвать конструктор Animal с аргументом "Кроль"
    super("Кроль"); // то же, что и Animal.call(this, "Кроль")
  }
  walk() {
    super.walk();
    alert("...and jump!");
  }
}
class Rabbit extends Animal {
  constructor() {
    alert(this); // ошибка, this не определён!
    // обязаны вызвать super() до обращения к this
    super();
    // а вот здесь уже можно использовать this
  }
}
class A {}
class B extends A {}
const isIt = A.isPrototypeOf(B);

Итераторы

Итерируемые объекты – это те, содержимое которых можно перебрать в цикле. Итератор – объект, предназначенный для перебора другого объекта.

Для перебора таких объектов добавлен новый синтаксис цикла: for..of.

let arr = [1, 2, 3]; // массив — пример итерируемого объекта

for (let value of arr) {
  alert(value); // 1, затем 2, затем 3
}

Конструкция for..of в начале своего выполнения автоматически вызывает Symbol.iterator(), При вызове метода Symbol.iterator перебираемый объект должен возвращать другой объект («итератор»), который умеет осуществлять перебор. У итератора должен быть метод next(), возвращающий объект {done: Boolean, value: any}, где value – очередное значение, а done: true в конце.  Внешний код при переборе через for..of видит только значения

'use strict';

let range = {
  from: 1,
  to: 5
}

// сделаем объект range итерируемым
range[Symbol.iterator] = function() {

  let current = this.from;
  let last = this.to;

  // метод должен вернуть объект с методом next()
  return {
    next() {
      if (current <= last) {
        return {
          done: false,
          value: current++
        };
      } else {
        return {
          done: true
        };
      }
    }

  }
};

for (let num of range) {
  alert(num); // 1, затем 2, 3, 4, 5
}
  • Перебираемый объект range сам не реализует методы для своего перебора.
  • Для этого создаётся другой объект, который хранит текущее состояние перебора и возвращает значение. Этот объект называется итератором и возвращается при вызове метода range[Symbol.iterator].
  • У итератора должен быть метод next(), который при каждом вызове возвращает объект со свойствами:
    • value – очередное значение,
    • done – равно false если есть ещё значения, и true – в конце.

Set, Map, WeakSet и WeakMap.

  • Map – коллекция записей вида ключ: значение, лучше Object тем, что перебирает всегда в порядке вставки и допускает любые ключи.
  • Set – коллекция уникальных элементов(т.е каждое значение встречается 1 раз), также допускает любые ключи.

Основная область применения Map – ситуации, когда строковых ключей не хватает (нужно хранить соответствия для ключей-объектов), либо когда строковый ключ может быть совершенно произвольным.

  • WeakMap и WeakSet – «урезанные» по функционалу варианты Map/Set, которые позволяют только «точечно» обращаться элементам (по конкретному ключу или значению). Они не препятствуют сборке мусора, то есть если ссылка на объект осталась только в WeakSet/WeakMap – он будет удалён.

основная задача – быть «вторичным» хранилищем данных для объектов, актуальный список которых (и сами они) хранятся в каком-то другом месте.

let map = new Map();

map
  .set('1', 'str1') // ключ-строка
  .set(1, 'num1')   // число
  .set(true, 'bool1'); // булевое значение

// map сохраняет тип ключа
alert( map.get(1)   ); // 'num1'
alert( map.get('1') ); // 'str1'

alert( map.size ); // 3
let recipeMap = new Map([
  ['огурцов',   '500 гр'],
  ['помидоров', '350 гр'],
  ['сметаны',   '50 гр']
]);

// объект user является ключом 
visitsCountMap.set(user, 123);

Методы для удаления записей:

  • map.delete(key) удаляет запись с ключом key, возвращает true, если такая запись была, иначе false.
  • map.clear() – удаляет все записи, очищает map.

Для проверки существования ключа:

  • map.has(key) – возвращает true, если ключ есть, иначе false.

Для итерации по map используется один из трёх методов:

  • map.keys() – возвращает итерируемый объект для ключей,
  • map.values() – возвращает итерируемый объект для значений,
  • map.entries() – возвращает итерируемый объект для записей [ключ, значение], он используется по умолчанию в for..of.
  • метод forEach
// цикл по ключам
for(let fruit of recipeMap.keys()) {
  alert(fruit); // огурцов, помидоров, сметаны
}

// цикл по значениям [ключ,значение]
for(let amount of recipeMap.values()) {
  alert(amount); // 500 гр, 350 гр, 50 гр
}

// цикл по записям
for(let entry of recipeMap) { // то же что и recipeMap.entries()
  alert(entry); // огурцов,500 гр , и т.д., массивы по 2 значения
}
recipeMap.forEach( (value, key, map) => {
  alert(`${key}: ${value}`); // огурцов: 500 гр, и т.д.
});
let set = new Set();

let vasya = {name: "Вася"};
let petya = {name: "Петя"};
let dasha = {name: "Даша"};

// посещения, некоторые пользователи заходят много раз
set.add(vasya);
set.add(petya);
set.add(dasha);
set.add(vasya);
set.add(petya);

// set сохраняет только уникальные значения
alert( set.size ); // 3

set.forEach( user => alert(user.name ) ); // Вася, Петя, Даша
  • set.add(item) – добавляет в коллекцию item, возвращает set (чейнится).
  • set.delete(item) – удаляет item из коллекции, возвращает true, если он там был, иначе false.
  • set.has(item) – возвращает true, если item есть в коллекции, иначе false.
  • set.clear() – очищает set.

let set = new Set(["апельсины", "яблоки", "бананы"]);

// то же, что: for(let value of set)
set.forEach((value, valueAgain, set) => {
  alert(value); // апельсины, затем яблоки, затем бананы
});

Генераторы

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

Генераторы создаются при помощи функций-генераторов function*(…) {…}..

function* generateSequence() {
  yield 1;
  yield 2;
  return 3;
}
// generator function создаёт generator
let generator = generateSequence();

let one = generator.next();

alert(JSON.stringify(one)); 
// {value: 1, done: false}

При запуске generateSequence() код не выполняется. Функция возвращает специальный объект, который называют «генератором».

При создании генератора код находится в начале своего выполнения.

Основным методом генератора является next(). При вызове он возобновляет выполнение кода до ближайшего ключевого слова yield. По достижении yield выполнение приостанавливается, а значение – возвращается во внешний код:

 //get the values from a generator in two ways
var convertedToAnArray = Array.from(generatorFunction());

var iterator = generatorFunction();
var [...iteratedOver] = [iterator.next().value, iterator.next().value];

 const {value} = generator.next();

Композиция – встраивание одного генератора в поток другого.

function* generateSequence(start, end) {
  for (let i = start; i <= end; i++) yield i;
}

function* generateAlphaNum() {

  // 0..9
  yield* generateSequence(48, 57);

  // A..Z
  yield* generateSequence(65, 90);

  // a..z
  yield* generateSequence(97, 122);

}

let str = '';

for(let code of generateAlphaNum()) {
  str += String.fromCharCode(code);
}

alert(str); // 0..9A..Za..z

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

То есть, при yield* интерпретатор переходит внутрь генератора-аргумента, к примеру, generateSequence(48, 57), выполняет его, и все yield, которые он делает, выходят из внешнего генератора.

  • Возвращает value во внешний код, приостанавливая выполнение генератора.
  • Внешний код может обработать значение, и затем вызвать next с аргументом: generator.next(arg).
  • Генератор продолжит выполнение, аргумент next будет возвращён как результат yield (и записан в result).
function* gen() {
  // Передать вопрос во внешний код и подождать ответа
  let result = yield "2 + 2?";
  alert(result);
}
let generator = gen();
let question = generator.next().value;
// "2 + 2?"
setTimeout(() => generator.next(4), 2000);

function* generatorFunction() {
      let a=yield 1;
      yield a;
 }
var iterator = generatorFunction();
var iteratedOver = [iterator.next().value, iterator.next(2).value];
1.2
  1. Первый вызов generator.next() – всегда без аргумента, он начинает выполнение и возвращает результат первого yield («2+2?»)`. На этой точке генератор приостанавливает выполнение.
  2. Результат yield переходит во внешний код (в question). Внешний код может выполнять любые асинхронные задачи, генератор стоит «на паузе».
  3. Когда асинхронные задачи готовы, внешний код вызывает generator.next(4) с аргументом. Выполнение генератора возобновляется, а 4 выходит из присваивания как результат let result = yield ....

Вызов let result = yield value делает следующее:

Для того, чтобы передать в yield ошибку, используется вызов generator.throw(err). При этом на строке с yield возникает исключение.

function* gen() {
  try {
    // в этой строке возникнет ошибка
    let result = yield "Сколько будет 2 + 2?"; // (**)

    alert("выше будет исключение ^^^");
  } catch(e) {
    alert(e); // выведет ошибку
  }
}

let generator = gen();

let question = generator.next().value;

generator.throw(new Error("ответ не найден 
в моей базе данных")); // (*)
function* gen() {
  // В этой строке возникнет ошибка
  let result = yield "Сколько будет 2 + 2?";
}

let generator = gen();

let question = generator.next().value;

try {
  generator.throw(new Error("ответ не найден 
в моей базе данных"));
} catch(e) {
  alert(e); // выведет ошибку
}

«Вброшенная» в строке (*) ошибка возникает в строке с yield (**). Далее она обрабатывается как обычно. В примере выше она перехватывается try..catch и выводится.

Если ошибку не перехватить, то она «выпадет» из генератора. По стеку ближайший вызов, который инициировал выполнение – это строка с .throw.

Модули

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

Модули в стандарте ECMAScript предоставляют удобные средства для этого.

  • AMD – одна из самых древних систем организации модулей, требует лишь наличия клиентской библиотеки, к примеру, require.js, но поддерживается и серверными средствами.
  • CommonJS – система модулей, встроенная в сервер Node.JS. Требует поддержки на клиентской и серверной стороне.
  • UMD – система модулей, которая предложена в качестве универсальной. UMD-модули будут работать и в системе AMD и в CommonJS.

Все перечисленные выше системы требуют различных библиотек или систем сборки для использования.

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

Другие модули могут подключать их через вызов import.

Экспорт:

  • export можно поставить прямо перед объявлением функции, класса, переменной.
  • Если export стоит отдельно от объявления, то значения в нём указываются в фигурных скобках: export {…}.
  • Также можно экспортировать «значение по умолчанию» при помощи export default.

Импорт:

  • В фигурных скобках указываются значения, а затем – модуль, откуда их брать: import {a, b, c as d} from "module".
  • Можно импортировать все значения в виде объекта при помощи import * as obj from "module".
  • Без фигурных скобок будет импортировано «значение по умолчанию»: import User from "user".
// экспорт прямо перед объявлением
export let one = 1;
let two = 2;

export {one as once, two as twice};

export class User {
  constructor(name) {
    this.name = name;
  }
};

export function sayHi() {
  alert("Hello!");
};

// отдельно от объявлений было бы так:
// export {User, sayHi}
import {one, two} from "./nums";

// импорт one под именем item1, а two – под именем item2
import {one as item1, two as item2} from "./nums";
alert( `${item1} and ${item2}` ); // 1 and 2

import * as numbers from "./nums";
// теперь экспортированные переменные - свойства numbers
alert( `${numbers.one} and ${numbers.two}` ); // 1 and 2

Для такой ситуации, когда один модуль экспортирует одно значение, предусмотрено особое ключевое сочетание export default. Такое значение можно импортировать без фигурных скобок.

export default class User {
  constructor(name) {
    this.name = name;
  }
};

import User from './user';
new User("Вася");

// если бы user.js содержал
// export class User { ... }
// …то при импорте User понадобились бы фигурные скобки:

import {User} from './user';
new User("Вася");

Promise

Promise  – удобный способ организации асинхронного кода

Promise – это специальный объект, который содержит своё состояние. Вначале pending («ожидание»), затем – одно из: fulfilled («выполнено успешно») или rejected («выполнено с ошибкой»).

На promise можно навешивать коллбэки двух типов:

  • onFulfilled – срабатывают, когда promise в состоянии «выполнен успешно».
  • onRejected – срабатывают, когда promise в состоянии «выполнен с ошибкой».

Способ использования, в общих чертах, такой:

  1. Код, которому надо сделать что-то асинхронно, создаёт объект promise и возвращает его.
  2. Внешний код, получив promise, навешивает на него обработчики.
  3. По завершении процесса асинхронный код переводит promise в состояние fulfilled (с результатом) или rejected (с ошибкой). При этом автоматически вызываются соответствующие обработчики во внешнем коде.
var promise = new Promise(function(resolve, reject) {
  // Эта функция будет вызвана автоматически

  // В ней можно делать любые асинхронные операции,
  // А когда они завершатся — нужно вызвать одно из:
  // resolve(результат) при успешном выполнении
  // reject(ошибка) при ошибке
})

Универсальный метод для навешивания обработчиков:

promise.then(onFulfilled, onRejected)
  • onFulfilled – функция, которая будет вызвана с результатом при resolve.
  • onRejected – функция, которая будет вызвана с ошибкой при reject.
// onFulfilled сработает при успешном выполнении
promise.then(onFulfilled)
// onRejected сработает при ошибке
promise.then(null, onRejected)
.catch(onRejected) – это то же самое.

Когда промис переходит в состояние «выполнен» – с результатом (resolve) или ошибкой (reject) – это навсегда.

'use strict';
// Создаётся объект promise
let promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    // переведёт промис в состояние fulfilled с результатом "result"
    resolve("result");
  }, 1000);
});

// promise.then навешивает обработчики на успешный результат или ошибку
promise
  .then(
    result => {
      // первая функция-обработчик - запустится при вызове resolve
      alert("Fulfilled: " + result); // result - аргумент resolve
    },
    error => {
      // вторая функция - запустится при вызове reject
      alert("Rejected: " + error); // error - аргумент reject
    }
  );

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

Функция httpGet(url) будет возвращать промис, который при успешной загрузке данных с url будет переходить в fulfilled с этими данными, а при ошибке – в rejected с информацией об ошибке:

function httpGet(url) {
  return new Promise(function(resolve, reject) {

    var xhr = new XMLHttpRequest();
    xhr.open('GET', url, true);

    xhr.onload = function() {
      if (this.status == 200) {
        resolve(this.response);
      } else {
        var error = new Error(this.statusText);
        error.code = this.status;
        reject(error);
      }
    };

    xhr.onerror = function() {
      reject(new Error("Network Error"));
    };

    xhr.send();
  });
}
httpGet("/article/promise/user.json")
  .then(
    response => alert(`Fulfilled: ${response}`),
    error => alert(`Rejected: ${error}`)
  );
httpGet(...)
  .then(...)
  .then(...)
  .then(...)

При чейнинге, то есть последовательных вызовах .then…then…then, в каждый следующий then переходит результат от предыдущего.

Если очередной then вернул промис, то далее по цепочке будет передан не сам этот промис, а его результат.

// сделать запрос
httpGet('/article/promise/user.json')
  // 1. Получить данные о пользователе в JSON и передать дальше
  .then(response => {
    console.log(response);
    let user = JSON.parse(response);
    return user;
  })
  // 2. Получить информацию с github
  .then(user => {
    console.log(user);
    return httpGet(`https://api.github.com/users/${user.name}`);
  })
  // 3. Вывести аватар на 3 секунды (можно с анимацией)
  .then(githubUser => {
    console.log(githubUser);
    githubUser = JSON.parse(githubUser);

    let img = new Image();
    img.src = githubUser.avatar_url;
    img.className = "promise-avatar-example";
    document.body.appendChild(img);

    setTimeout(() => img.remove(), 3000); // (*)
  });
  1. Функция в первом then возвращает «обычное» значение user. Это значит, что then возвратит промис в состоянии «выполнен» с user в качестве результата. Он станет аргументом в следующем then.
  2. Функция во втором then возвращает промис (результат нового вызова httpGet). Когда он будет завершён (может пройти какое-то время), то будет вызван следующий then с его результатом.
  3. Третий then ничего не возвращает.
  • В каждом then мы получаем текущий результат работы.
  • Можно его обработать синхронно и вернуть результат (например, применить JSON.parse). Или же, если нужна асинхронная обработка – инициировать её и вернуть промис.

Если мы хотим, чтобы после setTimeout (*) асинхронная цепочка могла быть продолжена, то последний then тоже должен вернуть промис. Это общее правило: если внутри then стартует новый асинхронный процесс, то для того, чтобы оставшаяся часть цепочки выполнилась после его окончания, мы должны вернуть промис.

// вместо setTimeout(() => img.remove(), 3000); (*)

  return new Promise((resolve, reject) => {
    setTimeout(() => {
      img.remove();
      // после таймаута — вызов resolve,
      // можно без результата, чтобы управление перешло в следующий then
      // (или можно передать данные пользователя дальше по цепочке)
      resolve();
    }, 3000);
  });

При возникновении ошибки – она отправляется в ближайший обработчик onRejected.

Такой обработчик нужно поставить через второй аргумент .then(..., onRejected) или, что то же самое, через .catch(onRejected).

.then.
.then
  .catch(error => {
    alert(error); // Error: Not Found
  });

Есть два варианта развития событий:

  1. Если ошибка не критичная, то onRejected возвращает значение через return, и управление переходит в ближайший .then(onFulfilled).
  2. Если продолжить выполнение с такой ошибкой нельзя, то он делает throw, и тогда ошибка переходит в следующий ближайший .catch(onRejected).

Параллельное выполнение

Promise.all(iterable)

Вызов Promise.all(iterable) получает массив (или другой итерируемый объект) промисов и возвращает промис, который ждёт, пока все переданные промисы завершатся, и переходит в состояние «выполнено» с массивом их результатов.если какой-то из промисов завершился с ошибкой, то результатом Promise.all будет эта ошибка. При этом остальные промисы игнорируются.

Promise.all([
  httpGet('/article/promise/user.json'),
  httpGet('/article/promise/guest.json')
]).then(results => {
  alert(results);
});

Вызов Promise.race, как и Promise.all, получает итерируемый объект с промисами, которые нужно выполнить, и возвращает новый промис. Но, в отличие от Promise.all, результатом будет только первый успешно выполнившийся промис из списка. Остальные игнорируются.

Promise.race([
  httpGet('/article/promise/user.json'),
  httpGet('/article/promise/guest.json')
]).then(firstResult => {
  firstResult = JSON.parse(firstResult);
  alert( firstResult.name ); // iliakan или guest, смотря что загрузится раньше
});

Promise.resolve(value)

Вызов Promise.resolve(value) создаёт успешно выполнившийся промис с результатом value.

Он аналогичен конструкции:

 

Promise.resolve используют, когда хотят построить асинхронную цепочку, и начальный результат уже есть.

new Promise((resolve) => resolve(value))
Promise.resolve(window.location) // начать с этого значения
  .then(httpGet) // вызвать для него httpGet
  .then(alert) // и вывести результат

Async functions

Developers can define an async function which may or may not contain await for promise-based asychronous operations. Under the hood, an async function is a function that returns Promise

async fetchData(query) => {
  try {
    const response = await axios.get(`/q?query=${query}`)
    const data = response.data
  	return data
  } catch (error) {
    console.log(error)
  }
}
fetchData(query).then(data => {
  this.props.processfetchedData(data)
})

После вызова, функция async возвращает Promise. Когда результат был получен, Promise завершается, возвращая полученное значение.  Когда функция async выбрасывает исключение, Promise ответит отказом с выброшенным (throws) значением.

Функция async может содержать выражение await, которое приостанавливает выполнение функции async и ожидает ответа от переданного Promise, затем возобновляя выполнение функции async и возвращая полученное значение.

а значением, возвращённым при разрешении этого промиса, будет то, что возвратит инструкция return
 

Object.values returns an array of object's own enumerable property values. We can iterate over it using good Array.prototype.forEach, but with ES6 arrow function and implicit return:

let obj = {a: 1, b: 2, c: 3}
Object.values(obj).forEach(value=>console.log(value)) // 1, 2, 3

let obj = {a: 1, b: 2, c: 3}
for (let value of Object.values(obj)) {
  console.log(value)
}
// 1, 2, 3

Object.entries, on the other hand, will return an array of object's own enumerable property key-value pairs (as an array). Each item of the resulting array will be an array too.

let obj = {a: 1, b: 2, c: 3}
Object.entries(obj).forEach(([key, value]) => {
 console.log(`${key} is ${value}`)
})
// a is 1, b is 2, c is 3

let obj = {a: 1, b: 2, c: 3}
for (let [key, value] of Object.entries(obj)) {
  console.log(`${key} is ${value}`)
}

padStart(targetLength, pad) returns a string of a given length (targetLength) by inserting pads at the beginning.

console.log('react'.padStart(10).length)         // "       react" is 10
console.log('backbone'.padStart(10).length)         // "  backbone" is 10

JS

By Anastasia Shemetillo