Нові функції ES2018, про які ви повинні знати

19 хв. читання

В липні 2018 світ побачила дев'ята редакція стандарту ECMAScript, що має назву ECMAScript 2018, або просто ES2018. Починаючи з ES2016, специфікації випускають щороку, на відміну від попередніх стандартів, що виходили раз на декілька років (проте й містили в собі більше змін та нових функцій). Сьогодні ми поговоримо про новинки останнього стандарту: нові можливості RegExp, rest/spread оператори, асинхронну ітерацію та Promise.prototype.finally. Також ES2018 знімає синтаксичні обмеження для екранованих послідовностей в рядках-шаблонах з використанням тегу.

Rest/spread властивості

Однією з цікавіших функцій, доданих в ES2015, був spread оператор. Він дозволяв копіювати масиви без зайвої головної болі. Замість викликів concat() чи slice() ви можете просто використовувати оператор ...:

const arr1 = [10, 20, 30];

// Робимо копію arr1
const copy = [...arr1];

console.log(copy);    // → [10, 20, 30]

const arr2 = [40, 50];

// Злиття arr2 та arr1
const merge = [...arr1, ...arr2];

console.log(merge);    // → [10, 20, 30, 40, 50]

Також цей оператор корисний коли потрібно передати масив як окремі аргументи в функцію:

const arr = [10, 20, 30]

// Еквівалент
// console.log(Math.max(10, 20, 30));
console.log(Math.max(...arr));    // → 30

ES2018 розширює можливості цього оператору, тепер ви можете використовувати його і в літералах об'єктів для копіювання властивостей одного об'єкту до іншого:

const obj1 = {
  a: 10,
  b: 20
};

const obj2 = {
  ...obj1,
  c: 30
};

console.log(obj2);    // → {a: 10, b: 20, c: 30}

В цьому прикладі оператор ... використано для копіювання всіх властивостей obj1 до obj2. До ES2018 цей код викликав би помилку. Також, якщо є дві (або більше) властивостей з однаковим ім'ям, збережена буде остання передана властивість.

const obj1 = {
  a: 10,
  b: 20
};

const obj2 = {
  ...obj1,
  a: 30
};

console.log(obj2);    // → {a: 30, b: 20}

Також spread оператор можна використовувати для об'єднання багатьох об'єктів, як альтернативу Object.assign():

const obj1 = {a: 10};
const obj2 = {b: 20};
const obj3 = {c: 30};

// ES2018
console.log({...obj1, ...obj2, ...obj3});    // → {a: 10, b: 20, c: 30}

// ES2015
console.log(Object.assign({}, obj1, obj2, obj3));    // → {a: 10, b: 20, c: 30}

Але слід зауважити, що spread оператор не завжди працює як Object.assign():

Object.defineProperty(Object.prototype, 'a', {
  set(value) {
    console.log('set called!');
  }
});

const obj = {a: 10};

console.log({...obj});    
// → {a: 10}

console.log(Object.assign({}, obj));    
// → set called!
// → {}

Як бачите, Object.assign() виконує успадкований сеттер. А от spread оператор ігнорує його.

Також важливо пам'ятати, що цей оператор працює лише з перелічуваними (enumerable) властивостями. В наступному прикладі властивість type не буде виведена, адже атрибут enumerable для неї встановлений в false.

const car = {
  color: 'blue'
};

Object.defineProperty(car, 'type', {
  value: 'coupe',
  enumerable: false
});

console.log({...car});    // → {color: "blue"}

Також успадковані властивості ігноруються (навіть якщо вони перелічувані):

const car = {
  color: 'blue'
};

const car2 = Object.create(car, {
  type: {
    value: 'coupe',
    enumerable: true,
  }
});

console.log(car2.color);                      // → blue
console.log(car2.hasOwnProperty('color'));    // → false

console.log(car2.type);                       // → coupe
console.log(car2.hasOwnProperty('type'));     // → true

console.log({...car2});                       // → {type: "coupe"}

В цьому прикладі car2 успадковує властивість color від car. Із-за того, що копіюються лише власні властивості, color не буде виведено.

Пам'ятайте, що spread оператор робить поверхневу копію і якщо значенням якоїсь з властивостей є ще один об'єкт — він буде скопійований за посиланням, а не значенням.

const obj = {x: {y: 10}};
const copy1 = {...obj};    
const copy2 = {...obj}; 

console.log(copy1.x === copy2.x);    // → true

copy1.x посилається на той же об'єкт в пам'яті що й copy2.x, саме тому оператор === повертає true.

Ще однією корисною фічею, доданою в ES2015, є надлишкові параметри (rest parameters), що дозволили розробникам використовувати ... з масивами:

const arr = [10, 20, 30];
const [x, ...rest] = arr;

console.log(x);       // → 10
console.log(rest);    // → [20, 30]

В цьому прикладі перший елемент присвоєно x, а решта елементів записана в змінній rest. Цей паттерн (array destructing) став таким популярним, що схожу можливість було додано і до об'єктів:

const obj = {
  a: 10,
  b: 20,
  c: 30
};

const {a, ...rest} = obj;

console.log(a);       // → 10
console.log(rest);    // → {b: 20, c: 30}

Зауважте, що операнд з ... завжди повинен бути в кінці списку, інакше буде викинута помилка:

const obj = {
  a: 10,
  b: 20,
  c: 30
};

const {...rest, a} = obj;    // → SyntaxError: Rest element must be last element

І використання декількох операторів в одному об'єкті також викликає помилку (тільки якщо вони не вкладені):

const obj = {
  a: 10,
  b: {
    x: 20,
    y: 30,
    z: 40
  }
};

const {b: {x, ...rest1}, ...rest2} = obj;    // Без помилок

const {...rest, ...rest2} = obj;    // → SyntaxError: Rest element must be last element

Підтримка браузерами

Chrome Firefox Safari Edge
60 55 11.1 No
Chrome Android Firefox Android iOS Safari Edge Mobile Samsung Internet Android Webview
60 55 11.3 No 8.2 60

Node.js

  • 8.0.0 (з флагом --harmony)
  • 8.3.0 (повна підтримка)

Асинхронна ітерація

Ітерація по колекції даних — досить популярна задача в програмуванні. До ES2015 в JavaScript були такі способи для перебору колекцій як цикли for, for .. in і while та такі методи як .map(), .filter() та .forEach(). А от в ES2015 було додано ще й інтерфейс ітераторів для цього.

Об'єкт стає ітерованим якщо в нього є властивість Symbol.iterator. З ES2015 текстові рядки та такі колекції як Set, Map і Array вже мають цю властивість, що дозволяє вам перебирати елементи по одному:

const arr = [10, 20, 30];
const iterator = arr[Symbol.iterator]();
  
console.log(iterator.next());    // → {value: 10, done: false}
console.log(iterator.next());    // → {value: 20, done: false}
console.log(iterator.next());    // → {value: 30, done: false}
console.log(iterator.next());    // → {value: undefined, done: true}

Symbol.iterator сигналізує, що функція повертає ітератор. Головним чином взаємодія з ітератором відбувається через виклик методу .next(). Цей метод повертає об'єкт з двома властивостями: value, що містить поточне значення з колекції та done, що сигналізує що досягнуто кінець колекції (true/false).

За замовчуванням, звичайні об'єкти не є ітерованими. Але ви можете зробити їх такими:

const collection = {
  a: 10,
  b: 20,
  c: 30,
  [Symbol.iterator]() {
    const values = Object.keys(this);
    let i = 0;
    return {
      next: () => {
        return {
          value: this[values[i++]],
          done: i > values.length
        }
      }
    };
  }
};

const iterator = collection[Symbol.iterator]();
  
console.log(iterator.next());    // → {value: 10, done: false}
console.log(iterator.next());    // → {value: 20, done: false}
console.log(iterator.next());    // → {value: 30, done: false}
console.log(iterator.next());    // → {value: undefined, done: true}

Тепер цей об'єкт є ітерованим, адже ви оголосили властивість Symbol.iterator — функцію, що повертає ітератор. Ми отримуємо список ключів об'єкту за допомогою Object.keys() а потім перебираємо його в методі next. Цей метод повертає об'єкт з властивостями value та done, все як потрібно.

І хоча цей код працює добре, все можна зробити набагато простіше за допомогою генераторів:

const collection = {
  a: 10,
  b: 20,
  c: 30,
  [Symbol.iterator]: function * () {
    for (let key in this) {
      yield this[key];
    }
  }
};

const iterator = collection[Symbol.iterator]();
  
console.log(iterator.next());    // → {value: 10, done: false}
console.log(iterator.next());    // → {value: 20, done: false}
console.log(iterator.next());    // → {value: 30, done: false}
console.log(iterator.next());    // → {value: undefined, done: true}

Мінусом ітераторів є те, що вони не придатні для асинхронних джерел даних. Але в ES2018 було додано асинхронні ітератори. Вони, на відміну від звичайних ітераторів, повертатимуть не об'єкт {value, done}, а проміс, результатом виконання якого буде об'єкт {value, done}. Метод, що повертає асинхронний ітератор, повинен бути оголошений під ім'ям Symbol.asyncIterator. Наприклад:

const collection = {
  a: 10,
  b: 20,
  c: 30,
  [Symbol.asyncIterator]() {
    const values = Object.keys(this);
    let i = 0;
    return {
      next: () => {
        return Promise.resolve({
          value: this[values[i++]], 
          done: i > values.length
        });
      }
    };
  }
};

const iterator = collection[Symbol.asyncIterator]();
  
console.log(iterator.next().then(result => {
  console.log(result);    // → {value: 10, done: false}
}));

console.log(iterator.next().then(result => {
  console.log(result);    // → {value: 20, done: false} 
}));

console.log(iterator.next().then(result => {
  console.log(result);    // → {value: 30, done: false} 
}));

console.log(iterator.next().then(result => {
  console.log(result);    // → {value: undefined, done: true} 
}));

Зауважте, що того ж результату неможливо досягти з використанням звичайного ітератору, що повертає проміси. Так, ви зможете отримувати данні асинхронно, але статус ітератора (done) все ще потрібно визначати синхронно.

Як і в минулому прикладі, код можна скоротити за допомогою генераторів:

const collection = {
  a: 10,
  b: 20,
  c: 30,
  [Symbol.asyncIterator]: async function * () {
    for (let key in this) {
      yield this[key];
    }
  }
};

const iterator = collection[Symbol.asyncIterator]();
  
console.log(iterator.next().then(result => {
  console.log(result);    // → {value: 10, done: false}
}));

console.log(iterator.next().then(result => {
  console.log(result);    // → {value: 20, done: false} 
}));

console.log(iterator.next().then(result => {
  console.log(result);    // → {value: 30, done: false} 
}));

console.log(iterator.next().then(result => {
  console.log(result);    // → {value: undefined, done: true} 
}));

Зазвичай функція-генератор повертає об'єкт-генератор з методом next(). Цей метод в свою чергу повертає об'єкт {value, done}. Асинхронний генератор же повертає проміс, що повертає об'єкт {value, done}.

Найпростішим способом використання ітерації є цикл for .. of, але, на жаль, він не підтримує асинхронну ітерацію. Саме тому ES2018 вводить новий цикл: for .. await .. of. І відразу приклад:

const collection = {
  a: 10,
  b: 20,
  c: 30,
  [Symbol.asyncIterator]: async function * () {
    for (let key in this) {
      yield this[key];
    }
  }
};

(async function () {
  for await (const x of collection) {
    console.log(x);
  }
})();

// logs:
// → 10
// → 20
// → 30

В цьому прикладі конструкція for .. await .. of неявно викликає Symbol.asyncIterator та отримує асинхронний ітератор. В кожній ітерації циклу викликається метод next(), який повертає проміс, який в свою чергу повертає пару {value, done} і значення value записується в змінну x. І так доки done не буде true.

Пам'ятайте, що цикл for .. await .. of працює лише з асинхронними генераторами та асинхронними функціями, в іншому випадку буде викинуто SyntaxError.

Метод next() може повернути проміс, котрий викине помилку. Щоб обробити її ви можете обгорнути цикл в блок try .. catch:

const collection = {
  [Symbol.asyncIterator]() {
    return {
      next: () => {
        return Promise.reject(new Error('Something went wrong.'))
      }
    };
  }
};

(async function() {
  try {
    for await (const value of collection) {}
  } catch (error) {
    console.log('Caught: ' + error.message);
  }
})();

// → Caught: Something went wrong.

Підтримка браузерами

Chrome Firefox Safari Edge
63 57 12 No
Chrome Android Firefox Android iOS Safari Edge Mobile Samsung Internet Android Webview
63 57 12 No 8.2 63

Node.js

  • 8.10.0 (з флагом --harmony_async_iteration)
  • 10.0.0 (повна підтримка)

Promise.prototype.finally

Декілька сторонніх бібліотек вже реалізували цей метод, і, треба сказати, він виявився дуже корисним і Ecma Technical Committee вирішили офіційно додати .finally() в специфікацію. З цим методом ви можете виконати блок коду в незалежності від статусу промісу. Розглянемо простий приклад:

fetch('https://web.archive.org/web/20230322092221/https://www.google.com')
  .then((response) => {
    console.log(response.status);
  })
  .catch((error) => { 
    console.log(error);
  })
  .finally(() => { 
    document.querySelector('#spinner').style.display = 'none';
  });

Цей метод стає в пригоді коли вам потрібно виконати деякі дії в незалежності від статусу промісу. В прикладі вище ми просто ховаємо спінер після того як дані будуть отримані та оброблені. Замість дублювання логіки в .then() та .catch() ми виносимо її в окремий метод.

Ви можете досягти того самого результату використовуючи promise.then(func, func), але тоді вам доведеться винести функцію-обробник в окрему змінну:

fetch('https://web.archive.org/web/20230322092221/https://www.google.com')
  .then((response) => {
    console.log(response.status);
  })
  .catch((error) => { 
    console.log(error);
  })
  .then(final, final);

function final() {
  document.querySelector('#spinner').style.display = 'none';
}

Як і .then() та .catch(), .finally() повертає проміс, тому ви можете будувати цілі ланцюжки промісів з ним. Зазвичай ви будете використовувати .finally() як останню ланку, але іноді ви захочете додати ще один .catch() для обробки помилок, що можуть виникнути в самому .finally().

Підтримка браузерами

Chrome Firefox Safari Edge
63 58 11.1 18
Chrome Android Firefox Android iOS Safari Edge Mobile Samsung Internet Android Webview
63 58 11.1 No 8.2 63

Node.js

  • 10.0.0 (повна підтримка)

Нові функції регулярних виразів

ES2018 додає нових можливостей до об'єкту RegExp, а саме:

  • s (dotAll) флаг
  • Named capture groups
  • Lookbehind assertions
  • Unicode property escapes

s (dotAll) флаг

Крапка (.) — спеціальний символ в регулярних виразах, що відповідає будь-якому символу, окрім символу розриву рядка (\ або \ ). Тому, для того щоб захопити всі символи, включно з розривами рядку, ви можете використовувати дві протилежні групи, наприклад, [\\d\\D]. Такий вираз вказує JS, що потрібно шукати символ, що відповідає цифрі (\\d) або не-цифрі (\\D). В результаті це захоплює будь-який символ:

console.log(/one[\\d\\D]two/.test('one\
two'));    // → true

ES2018 додає новий режим, в якому крапка може бути використана для досягнення такого результату. Активується цей режим за допомогою флагу s:

console.log(/one.two/.test('one\
two'));     // → false
console.log(/one.two/s.test('one\
two'));    // → true

Named Capture Groups

В деяких випадках використання номеру для звернення до захопленої групи символів не зовсім зручно. Для прикладу візьмемо регулярний вираз для парсингу дати: /(\\d{4})-(\\d{2})-(\\d{2})/. Оскільки, наприклад, американська та британська нотації відрізняються — ви не можете бути впевнені котра група відповідає дню, а котра місяцю:

const re = /(\\d{4})-(\\d{2})-(\\d{2})/;
const match= re.exec('2019-01-10');

console.log(match[0]);    // → 2019-01-10
console.log(match[1]);    // → 2019
console.log(match[2]);    // → 01
console.log(match[3]);    // → 10

ES2018 додає можливість присвоювати цим групам імена, використовуючи такий синтаксис: (?<name>...). Тепер ми можемо переписати минулий приклад і зробити його однозначним:

const re = /(?<year>\\d{4})-(?<month>\\d{2})-(?<day>\\d{2})/;
const match = re.exec('2019-01-10');

console.log(match.groups);          // → {year: "2019", month: "01", day: "10"}
console.log(match.groups.year);     // → 2019
console.log(match.groups.month);    // → 01
console.log(match.groups.day);      // → 10

Ви можете звертатися до цих груп далі в регулярному виразі, використовуючи конструкцію \\k<name>. Наприклад, ось так можна знайти дубльовані підряд слова: /\\b(?<dup>\\w+)\\s+\\k<dup>\\b/:

const re = /\\b(?<dup>\\w+)\\s+\\k<dup>\\b/;
const match = re.exec('Get that that cat off the table!');        

console.log(match.index);    // → 4
console.log(match[0]);       // → that that

Для використання цих груп в методі .replace() слід використовувати інший синтаксис: $<name>. Наприклад:

const str = 'red & blue';

console.log(str.replace(/(red) & (blue)/, '$2 & $1'));    
// → blue & red

console.log(str.replace(/(?<red>red) & (?<blue>blue)/, '$<blue> & $<red>'));    
// → blue & red

Lookbehind Assertions

Так, ця функція вже роками доступна в інших мовах програмування. Тепер і в JS. Раніше JS підтримував лише lookahead assertions. Для lookbehind assertions використовується такий синтаксис: (?<=...), що дозволяє захоплювати підрядок на основі підрядку, що йде перед ним. Наприклад, якщо ви хочете дістати з тексту ціну в доларах, євро чи фунтах без захоплення самого виразу: /(?<=\\$|£|€)\\d+(\\.\\d*)?/

const re = /(?<=\\$|£|€)\\d+(\\.\\d*)?/;

console.log(re.exec('199'));     
// → null

console.log(re.exec('$199'));    
// → ["199", undefined, index: 1, input: "$199", groups: undefined]

console.log(re.exec('€50'));     
// → ["50", undefined, index: 1, input: "€50", groups: undefined]

Є ще версія цього порівняння з запереченням: (?<!...), що дозволяє захоплювати підрядок лише якщо перед ним немає заданого підрядка. Приклад нижче спрацьовує на слово 'available' лише тоді, коли перед ним немає префіксу 'un':

const re = /(?<!un)available/;

console.log(re.exec('We regret this service is currently unavailable'));    
// → null

console.log(re.exec('The service is available'));             
// → ["available", index: 15, input: "The service is available", groups: undefined]

Unicode Property Escapes

ES2018 додає новий тип для екранованих послідовностей, відомий як Unicode property escape, що забезпечує повну підтримку юнікоду в регулярних виразах. Наприклад, ви хочете спарсити символ юнікоду ㉛. І хоча це й число, \\d його проігнорує, адже він спрацьовує лише на ASCII-символи (0-9). Але завдяки Unicode property escape це стає можливим:

const str = '㉛';

console.log(/\\d/u.test(str));    // → false
console.log(/\\p{Number}/u.test(str));     // → true

Або якщо ви хочете знайти будь-який символ з алфавіту юнікоду:

const str = 'ض';

console.log(/\\p{Alphabetic}/u.test(str));     // → true

// \\w не знайде символ ض
  console.log(/\\w/u.test(str));    // → false

Також існує і версія з запереченням \\P{...}:

console.log(/\\P{Number}/u.test('㉛'));    // → false
console.log(/\\P{Number}/u.test('ض'));    // → true

console.log(/\\P{Alphabetic}/u.test('㉛'));    // → true
console.log(/\\P{Alphabetic}/u.test('ض'));    // → false

Список всіх груп юнікод-символів можна знайти тут.

Підтримка браузерами

Chrome Firefox Safari Edge
s (dotAll) флаг 62 No 11.1 No
Named Capture Groups 64 No 11.1 No
Lookbehind Assertions 62 No No No
Unicode Property Escapes 64 No 11.1 No
Chrome Android Firefox Android iOS Safari Edge Mobile Samsung Internet Android Webview
s (dotAll) флаг 62 No 11.3 No 8.2 62
Named Capture Groups 64 No 11.3 No No 64
Lookbehind Assertions 62 No No No 8.2 62
Unicode Property Escapes 64 No 11.3 No No 64

Node.js

  • 8.3.0 (з флагом --harmony)
  • 8.10.0 (підтримка s (dotAll) флагу та lookbehind assertions)
  • 10.0.0 (повна підтримка)

Усунення обмеження для екранованих послідовностей в форматованих рядках

Якщо перед форматованим рядком стоїть якийсь тег, котрий його обробляє — такий рядок називаються теговим літералом.

function fn(string, substitute) {
  if(substitute === 'ES6') {
    substitute = 'ES2015'
  }
  return substitute + string[1];
}

const version = 'ES6';
const result = fn`${version} was a major update`;

console.log(result);    // → ES2015 was a major update

В даному випадку тег — звичайна функція, що обробляє шаблон. Функція просто оброблює динамічну частину рядка та повертає його.

До ES2018, тегові шаблони мали синтаксичні обмеження, що стосувалися екранованих послідовностей. Бекслеш та символи, що йшли за ним були інтерпретовані як спеціальна послідовність: \\X для 16-ричних послідовностей, \\u для юнікод-послідовностей, а \\ та число за ним були інтерпретовані як 8-рична послідовність. В результаті такі рядки як "C:\\xxx\\uuu" чи "\\ubuntu" викликали SyntaxError.

ES2018 прибирає ці обмеження для тегових шаблонів і замість викидання помилки заміняє невідому послідовності на undefined:

function fn(string, substitute) {
  console.log(substitute);    // → escape sequences:
  console.log(string[1]);     // → undefined
}

const str = 'escape sequences:';
const result = fn`${str} \\ubuntu C:\\xxx\\uuu`;

Але пам'ятайте, що для звичайних шаблонів все одно буде викинута помилка:

const result = `\\ubuntu`;
// → SyntaxError: Invalid Unicode escape sequence

Підтримка браузерами

Chrome Firefox Safari Edge
62 56 11 No
Chrome Android Firefox Android iOS Safari Edge Mobile Samsung Internet Android Webview
62 56 11 No 8.2 62

Node.js

  • 8.0.0 (з флагом --harmony)
  • 8.3.0 (повна підтримка)

І хоча ці нові функції не здаються чимось дуже-дуже корисним, всі вони — важливий внесок у нашу улюблену (або ненависну) мову і роблять її кращою. Також відсутність якихось величезних зсувів повязана з переходом на щорічний випуск специфікацій (раніше вони випускалися раз на декілька років), і це, маю сказати, дуже добре.

Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Codeguida 5.6K
Приєднався: 8 місяців тому
Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.

Вхід / Реєстрація