JavaScript сильно изменился за последние годы. Вот 12 новых возможностей, которые можно начать использовать уже сегодня!
История
Новые добавления в язык называются ECMAScript 6. Или ES6 или ES2015+.
С момента появления в 1995, JavaScript развивался медленно. Новые возможности добавлялись каждые несколько лет. ECMAScript появился в 1997, его целью было направить развитие JavaScript в нужное русло. Выходили новые версии – ES3, ES5, ES6 и так далее.
Как видите, между версиями ES3, ES5 и ES6 есть пропуски длиной в 10 и 6 лет. Новая модель – делать маленькие изменения каждый год. Вместо того, чтобы накопить огромное количество изменений и выпустить их все за раз, как это было с ES6.
Browsers Support
Все современные браузеры и среды исполнения уже поддерживают ES6!
Chrome, MS Edge, Firefox, Safari, Node и многие другие системы имеют встроенную поддержку большинства возможностей JavaScript ES6. Так что, все из этого пособия можно использовать прямо сейчас.
Поехали!
Главные возможности ES6
Все сниппеты можно вставлять в консоль браузера и запускать.
Block scope variables
В ES6 мы перешли от var
к let
/const
.
Что не так с var
?
Проблема var
в том, что переменная "протекает" в другие блоки кода, такие как циклы for
или блоки условий if
:
ES5
var x = 'outer';
function test(inner) {
if (inner) {
var x = 'inner'; // scope whole function
return x;
}
return x; // gets redefined on line 4
}
test(false); // undefined
test(true); // inner
В строке test(false)
можно ожидать возврат outer
, но нет, мы получаем undefined
. Почему?
Потому что даже не смотря на то, что блок if
не выполняется, на 4й строке происходит переопределение var x
как undefined
.
ES6 спешит на помощь:
ES6
let x = 'outer';
function test(inner) {
if (inner) {
let x = 'inner';
return x;
}
return x; // gets result from line 1 as expected
}
test(false); // outer
test(true); // inner
Изменив var
на let
мы откорректировали поведение. Если блок if
не вызывается, то переменная x
не переопределяется.
IIFE (immediately invoked function expression)
Давайте сначала рассмотрим пример:
ES5
{
var private = 1;
}
console.log(private); // 1
Как видите, private
протекает наружу. Нужно использовать IIFE (immediately-invoked function expression):
ES5
(function(){
var private2 = 1;
})();
console.log(private2); // Uncaught ReferenceError
Если взглянуть на jQuery/lodash или любые другие проекты с открытым исходным кодом, то можно заметить, что там IIFE используется для содержания глобальной среды в чистоте. А глобальные штуки определяются со специальными символами вроде _
, $
или jQuery
.
В ES6 не нужно использовать IIFE, достаточно использовать блоки и let
:
ES6
{
let private3 = 1;
}
console.log(private3); // Uncaught ReferenceError
Const
Можно также использовать const
если переменная не должна изменяться.
Итог:
- забудьте
var
, используйтеlet
иconst
. - Используйте
const
для всех референсов; не используйтеvar
. - Если референсы нужно переопределять, используйте
let
вместоconst
.
Template Literals
Не нужно больше делать вложенную конкатенацию, можно использовать шаблоны. Посмотрите:
ES5
var first = 'Adrian';
var last = 'Mejia';
console.log('Your name is ' + first + ' ' + last + '.');
С помощью бэктика () и интерполяции строк
${}` можно сделать так:
ES6
const first = 'Adrian';
const last = 'Mejia';
console.log(`Your name is ${first} ${last}.`);
Multi-line strings
Не нужно больше конкатенировать строки с + \n
:
ES5
var template = '<li *ngFor="let todo of todos" [ngClass]="{completed: todo.isDone}" >\n' +
' <div class="view">\n' +
' <input class="toggle" type="checkbox" [checked]="todo.isDone">\n' +
' <label></label>\n' +
' <button class="destroy"></button>\n' +
' </div>\n' +
' <input class="edit" value="">\n' +
'</li>';
console.log(template);
В ES6 можно снова использовать бэктики:
ES6
const template = `<li *ngFor="let todo of todos" [ngClass]="{completed: todo.isDone}" >
<div class="view">
<input class="toggle" type="checkbox" [checked]="todo.isDone">
<label></label>
<button class="destroy"></button>
</div>
<input class="edit" value="">
</li>`;
console.log(template);
Оба блока кода генерируют одинаковый результат
Destructuring Assignment
ES6 destructing – полезная и лаконичная штука. Посмотрите на примеры:
Получение элемента из массива
ES5
var array = [1, 2, 3, 4];
var first = array[0];
var third = array[2];
console.log(first, third); // 1 3
То же самое:
ES6
const array = [1, 2, 3, 4];
const [first, ,third] = array;
console.log(first, third); // 1 3
Обмен значениями
ES5
var a = 1;
var b = 2;
var tmp = a;
a = b;
b = tmp;
console.log(a, b); // 2 1
То же самое:
ES6
let a = 1;
let b = 2;
[a, b] = [b, a];
console.log(a, b); // 2 1
Деструктуризация нескольких возвращаемых значений
ES5
function margin() {
var left=1, right=2, top=3, bottom=4;
return { left: left, right: right, top: top, bottom: bottom };
}
var data = margin();
var left = data.left;
var bottom = data.bottom;
console.log(left, bottom); // 1 4
В строке 3 можно вернуть в виде массива:
return [left, right, top, bottom];
но вызывающему коду придется знать о порядке данных.
var left = data[0];
var bottom = data[3];
С ES6 вызывающий выбирает только нужные данные (строка 6):
ES6
function margin() {
const left=1, right=2, top=3, bottom=4;
return { left, right, top, bottom };
}
const { left, bottom } = margin();
console.log(left, bottom); // 1 4
Заметка: В строке 3 содержатся другие возможности ES6. Можно сократить { left: left }
до { left }
. Смотрите, насколько это лаконичнее по сравнению с версией ES5. Круто же?
Деструктуризация и сопоставление параметров
ES5
var user = {firstName: 'Adrian', lastName: 'Mejia'};
function getFullName(user) {
var firstName = user.firstName;
var lastName = user.lastName;
return firstName + ' ' + lastName;
}
console.log(getFullName(user)); // Adrian Mejia
То же самое (но короче):
ES6
const user = {firstName: 'Adrian', lastName: 'Mejia'};
function getFullName({ firstName, lastName }) {
return `${firstName} ${lastName}`;
}
console.log(getFullName(user)); // Adrian Mejia
Глубокое сопоставление
ES5
function settings() {
return { display: { color: 'red' }, keyboard: { layout: 'querty'} };
}
var tmp = settings();
var displayColor = tmp.display.color;
var keyboardLayout = tmp.keyboard.layout;
console.log(displayColor, keyboardLayout); // red querty
То же самое (но короче):
ES6
function settings() {
return { display: { color: 'red' }, keyboard: { layout: 'querty'} };
}
const { display: { color: displayColor }, keyboard: { layout: keyboardLayout }} = settings();
console.log(displayColor, keyboardLayout); // red querty
Это также называют деструктуризацией объекта (object destructing).
Как видите, деструктуризация может быть очень полезной и может подталкивать к улучшению стиля кодирования.
Советы:
- Используйте деструктуризацию для получения элементов из массива и для обмена значениями. Не нужно делать временные референсы – сэкономите время.
- Не используйте деструктуризацию массива для нескольких возвращаемых значений, вместо этого используйте деструктуризацию объекта.
Классы и объекты
В ECMAScript 6 мы перешли от “функций-конструкторов” к “классам” .
Каждый объект в JavaScript имеет прототип, который является другим объектом. Все объекты в JavaScript наследуют методы и свойства от своего прототипа.
В ES5 объектно-ориентированное программирование достигалось с помощью функций-конструкторов. Они создавали объекты следующим образом:
ES5
var Animal = (function () {
function MyConstructor(name) {
this.name = name;
}
MyConstructor.prototype.speak = function speak() {
console.log(this.name + ' makes a noise.');
};
return MyConstructor;
})();
var animal = new Animal('animal');
animal.speak(); // animal makes a noise.
В ES6 есть новый синтаксический сахар. Можно сделать то же самое с меньшим кодом и с использованием ключевых слов class
и construсtor
. Также заметьте, как четко определяются методы: construсtor.prototype.speak = function ()
vs speak()
:
ES6
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(this.name + ' makes a noise.');
}
}
const animal = new Animal('animal');
animal.speak(); // animal makes a noise.
Оба стиля (ES5/6) дают одинаковый результат.
Советы:
- Всегда используйте синтаксис
class
и не изменяйтеprototype
напрямую. Код будет лаконичнее и его будет легче понять. - Избегайте создания пустого конструктора. У классов есть конструктор по умолчанию если не задать собственный.
Наследование
Давайте продолжим предыдущий пример с классом Animal
. Допустим, нам нужен новый класс Lion
.
В ES5 придется немного поработать с прототипным наследованием.
ES5
var Lion = (function () {
function MyConstructor(name){
Animal.call(this, name);
}
// prototypal inheritance
MyConstructor.prototype = Object.create(Animal.prototype);
MyConstructor.prototype.constructor = Animal;
MyConstructor.prototype.speak = function speak() {
Animal.prototype.speak.call(this);
console.log(this.name + ' roars ');
};
return MyConstructor;
})();
var lion = new Lion('Simba');
lion.speak(); // Simba makes a noise.
// Simba roars.
Не будем вдаваться в детали, но заметьте несколько деталей:
- Строка 3, напрямую вызываем конструктор
Animal
с параметрами. - Строки 7-8, назначаем прототип
Lion
прототипом классаAnimal
. - Строка 11, вызываем метод
speak
из родительского классаAnimal
.
В ES6 есть новые ключевые слова extends
и super
.
ES6
class Lion extends Animal {
speak() {
super.speak();
console.log(this.name + ' roars ');
}
}
const lion = new Lion('Simba');
lion.speak(); // Simba makes a noise.
// Simba roars.
Посмотрите, насколько лучше выглядит код на ES6 по сравнению с ES5. И они делают одно и то же! Win!
Совет:
- Используйте встроенный способ наследования –
extends
.
Нативные промисы
Переходим от callback hell к промисам (promises)
ES5
function printAfterTimeout(string, timeout, done){
setTimeout(function(){
done(string);
}, timeout);
}
printAfterTimeout('Hello ', 2e3, function(result){
console.log(result);
// nested callback
printAfterTimeout(result + 'Reader', 2e3, function(result){
console.log(result);
});
});
Одна функция принимает callback чтобы запустить его после завершения. Нам нужно запустить ее дважды, одну за другой. Поэтому приходится вызывать printAfterTimeout
во второй раз в коллбеке.
Все становится совсем плохо когда нужно добавить третий или четвертый коллбек. Давайте посмотрим, что можно сделать с промисами:
ES6
function printAfterTimeout(string, timeout){
return new Promise((resolve, reject) => {
setTimeout(function(){
resolve(string);
}, timeout);
});
}
printAfterTimeout('Hello ', 2e3).then((result) => {
console.log(result);
return printAfterTimeout(result + 'Reader', 2e3);
}).then((result) => {
console.log(result);
});
С помощью then
можно обойтись без вложенных функций.
Стрелочные функции
В ES5 обычные определения функций не исчезли, но был добавлен новый формат – стрелочные функции.
В ES5 есть проблемы с this
:
ES5
var _this = this; // need to hold a reference
$('.btn').click(function(event){
_this.sendData(); // reference outer this
});
$('.input').on('change',function(event){
this.sendData(); // reference outer this
}.bind(this)); // bind to outer this
Нужно использовать временный this
чтобы ссылаться на него внутри функции или использовать bind
. В ES6 можно просто использовать стрелочную функцию!
ES6
// this will reference the outer one
$('.btn').click((event) => this.sendData());
// implicit returns
const ids = [291, 288, 984];
const messages = ids.map(value => `ID is ${value}`);
For…of
От for
переходим к forEach
а потом к for...of
:
ES5
// for
var array = ['a', 'b', 'c', 'd'];
for (var i = 0; i < array.length; i++) {
var element = array[i];
console.log(element);
}
// forEach
array.forEach(function (element) {
console.log(element);
});
ES6 for…of позволяет использовать итераторы
ES6
// for ...of
const array = ['a', 'b', 'c', 'd'];
for (const element of array) {
console.log(element);
}
Параметры по умолчанию
От проверки параметров переходим к параметрам по умолчанию. Вы делали что-нибудь такое раньше?
ES5
function point(x, y, isFlag){
x = x || 0;
y = y || -1;
isFlag = isFlag || true;
console.log(x,y, isFlag);
}
point(0, 0) // 0 -1 true
point(0, 0, false) // 0 -1 true
point(1) // 1 -1 true
point() // 0 -1 true
Скорее всего да. Это распространенный паттерн проверки наличия значения переменной. Но тут есть некоторые проблемы:
- Строка 8, передаем
0, 0
получаем0, -1
- Строка 9, передаем
false
, но получаемtrue
.
Если параметр по умолчанию это булева переменная или если задать значение 0, то ничего не получится. Почему? Расскажу после этого примера с ES6 ;)
В ES6 все получается лучше с меньшим количеством кода:
ES6
function point(x = 0, y = -1, isFlag = true){
console.log(x,y, isFlag);
}
point(0, 0) // 0 0 true
point(0, 0, false) // 0 0 false
point(1) // 1 -1 true
point() // 0 -1 true
Получаем ожидаемый результат. Пример в ES5 не работал. Нужно проверять на undefined
так как false
, null
, undefined
и 0
– это все falsy-значения. С числами можно так:
ES5
function point(x, y, isFlag){
x = x || 0;
y = typeof(y) === 'undefined' ? -1 : y;
isFlag = typeof(isFlag) === 'undefined' ? true : isFlag;
console.log(x,y, isFlag);
}
point(0, 0) // 0 0 true
point(0, 0, false) // 0 0 false
point(1) // 1 -1 true
point() // 0 -1 true
С проверкой на undefined
все работает как нужно.
Rest-параметры
От аргументов к rest-параметрам и операции spread.
В ES5 работать с переменным количеством аргументов неудобно.
ES5
function printf(format) {
var params = [].slice.call(arguments, 1);
console.log('params: ', params);
console.log('format: ', format);
}
printf('%s %d %.2f', 'adrian', 321, Math.PI);
С rest ...
все намного проще.
ES6
function printf(format, ...params) {
console.log('params: ', params);
console.log('format: ', format);
}
printf('%s %d %.2f', 'adrian', 321, Math.PI);
Операция Spread
Переходим от apply()
к spread. Опять же, ...
спешит на помощь:
Помните: мы используемapply()
чтобы превратить массив в список аргументов. например,Math.max()
принимает список параметров, но если у нас есть массив, то можно использоватьapply
.
ES5
Math.max.apply(Math, [2,100,1,6,43]) // 100
В ES6 используем spread:
ES6
Math.max(...[2,100,1,6,43]) // 100
Мы также перешли от concat
к spread'у:
ES5
var array1 = [2,100,1,6,43];
var array2 = ['a', 'b', 'c', 'd'];
var array3 = [false, true, null, undefined];
console.log(array1.concat(array2, array3));
В ES6:
ES6
const array1 = [2,100,1,6,43];
const array2 = ['a', 'b', 'c', 'd'];
const array3 = [false, true, null, undefined];
console.log([...array1, ...array2, ...array3]);
Заключение
JavaScript сильно изменился. Эта статья покрывает только базовые возможности, о которых должен знать каждый разработчик.
Комментарии (37)
khayr
25.10.2016 13:29-3А как насчет поддержки ES6 устаревшими браузерами? Тот же IE9, который является последней версией для Висты (которая до сих является поддерживаемым ОС), сможет выполнить код? Часто заказчики вообще требуют поддержки IE6.
Alvaro
25.10.2016 13:38+4Ладно IE6 или IE9. Многое из описанного не поддерживается и в IE11 о чем почему-то умолчали в статье
AlexanderBlago
25.10.2016 15:17+9Часто заказчики вообще требуют поддержки IE6.
вокруг вас там черти с вилами не бегают?
Sirion
25.10.2016 13:46+3Слава Богу, наконец-то кто-то написал псто про новые фичи в ES6.
AndreyRubankov
25.10.2016 18:25+425 октября 2016
Слава Богу, наконец-то кто-то написал просто про новые фичи в ES2015.
iShatokhin
25.10.2016 14:24забудьте var, используйте let и const.
Между тем, node.js team пока старается избегать их в местах, критичным к скорости выполнения (например, в циклах). https://github.com/nodejs/node/pull/8873 и https://gist.github.com/Salakar/6d7b84f7adf1f3bc62a754752a6e5d0e
Со времен JS-движки оптимизируют под новый стандарт.Zibx
25.10.2016 15:51+2Но как? В байткоде let плодит замыкание, у него нет других вариантов кроме как сделать замыкание, а это не бесплатная вещь. Фактически такие статьи сейчас предлагают завернуть любой цикл таким образом:
for(var i = 0; i < length; i++) (function(i){ тело })(i);
Это несомненно решает проблему когда в теле оказывается ассинхронный код, единственное что могут изобрести js-движки — jit оптимизацию, которая проверит что код синхронен и тогда заменит let на var (логику поведения). Но что если мне действительно надо в ассинхронном коде использовать последнее значение i?
ES6 принёс очень много загадочных мест.
Klimashkin
26.10.2016 09:27Да, над этим работают
https://docs.google.com/document/d/1EA9EbfnydAmmU_lM8R_uEMQ-U_v4l9zulePSBkeYWmY
ROBsoer
25.10.2016 14:29+4переменная "протекает" в другие блоки кода
протекает наружуВсега было "поднимается" ну или "всплыввает", зачем вводить новое понятие да еще и такое запутанное.
Все же четко: объявление переменной "поднимается" в начало области видимости (функции). И не нужно объяснять почему переменная настолько вредная, что "проникает" в другие блоки
Timoschenko
25.10.2016 17:35В целом, очень даже ничего.
Некоторые фишки очень даже понравились.Особенно «class, constructor».
Но вот это как-то не очень:
ES6 function settings() { return { display: { color: 'red' }, keyboard: { layout: 'querty'} }; } const { display: { color: displayColor }, keyboard: { layout: keyboardLayout }} = settings(); console.log(displayColor, keyboardLayout); // red querty
Source
25.10.2016 19:34Это, на мой взгляд, просто пример неуместного использования деструктуринга, строка получилась очень длинной и плохо читаемой, при этом вариант
settings.display.color
гораздо читабельнее в данном случае.
Но это не потому что деструктуринг плох, а потому что надо в меру фичи использовать.
То же можно сказать и про[...array1, ...array2, ...array3]
. Лучше бы+
починили (ну или какой-нибудь++
добавили), еслиconcat
не нравится.
sand14
26.10.2016 09:27-1Удивительно дело.
Модные и современные веб-языки и технологии проходят тернистый путь развития совершенно без учета граблей, через которые пришлось пройти в свое время традиционным универсальным языкам и платформам.
В C++ изначально было сделано по уму — friend-функции
А вот дальше пошло хуже, но хоть как-то развивалось:
- в Delphi «протекали» private-поля внутри модуля, потом протечки залепили с помощью strict protected;
- в Java protected-поля протекают внутри пакета,
- но потом появился ее близнец C#, в котором protected не протекает, а для контролируемых протечек внутри сборки придумали internal.
На мой взгляд, когда private var виден наружу, это именно «протечка», лучше называть вещи своими именами.
Ок, придумали, let и const теперь. Неясно, почему это нельзя было придумать сразу, с учетом опыта других языков.
Тем более путаница ключевых слов — если с const все понятно, то разницу между var и let нужно держать в голове, или гуглить. Опять же, неужели опыт других языков не учит, где тоже полно схожих ключевых слов с разной семантикой.justboris
26.10.2016 12:21Неясно, почему это нельзя было придумать сразу, с учетом опыта других языков.
Javascript появился в 1995 году, опыта других языков типа Java и тем более C# еще не было.
sand14
26.10.2016 14:58Здесь было немного сарказма про «модные веб-языки», но тем не менее:
- в 1995-м появился LiveScript, а не JavaScript
- Сколько раз его можно было переделать, пока браузеры не стали массово исполнять JS как стандарт де факто?
- в 1995-м уже давно был C++, и вовсю была Delphi, было на что ориентироваться
- Java вышла в 1995-96-м
Так что по ходу развития JS было на что ориентироваться.
Поэтому и неясно, почему такие детские болезни, как var в JS, дожили аж до 2016.justboris
26.10.2016 15:09Ну когда смогли, тогда и сделали.
Лучше поздно, чем никогда, правда?
sand14
26.10.2016 15:24Все верно, лучше поздно, чем никогда.
Но этот и множество других примеров в веб-технологиях удивляют — как будто их разработчики занимались разработкой где-то на Альфа Центавра, иной раз кажется, что ну совершенно без учета опыта индустрии.
А теперь если что-то и исправляется, то преподносится как откровение (а в индустрии в целом это уж 10-20-30 лет как решено).
Проблема в том, что веб давно стал очень актуальным, и дальше — больше (но — со всеми своими детскими болезнями), и было бы интересно понять причины, предпосылки сложившегося состояния дел именно в веб-отрасли.
А так, считаю, что и традиционным языкам есть чему получиться у того же JS (например, доступ к элементами класса как словарю «ключ — значение» — по сути, это рефлексия, встроенная в язык, а не в библиотеки платформы).justboris
26.10.2016 15:31Ответ на первой картинке в посте.
С 2000 года почти 10 лет никто языком не занимался вообще, браузеры реализовывали функциональность кто во что горазд.
Потом понадобилось некоторое время, чтобы разобраться с проблемами и прийти к какому-то удовлетворительному решению.taujavarob
27.10.2016 20:57>С 2000 года почти 10 лет никто языком не занимался вообще
Была попытка неудавшаяся протащить ООП в JavaScript в ES4.
Так что работа велась. ;-)
fijj
26.10.2016 13:00После первого примера дальше даже читать не стал. Область видимости для var — функция(она же объект). Почему вернуло undefined? Да потому что переменная объявляется внутри функции независимо от наличия условия, оно влияет только на присвоение значения этой переменной.
denisei
01.11.2016 11:29В примере с классами для ES5 не слишком ли усложнена обертка? Можно ведь и так написать:
var Animal = function(name){ this.name = name; this.speak = function(){ console.log(this.name + ' makes a noise.'); } }
Sirion
01.11.2016 12:46Это не одно и то же. Там метод вынесен в прототип.
denisei
02.11.2016 06:13Ок, просьба не пинать больно, а что нам дает замыкание в функцию по сравнению с такой записью?
var Animal = function(name){ this.name = name; } Animal.prototype.speak = function(){ console.log(this.name + ' makes a noise.'); }
Sirion
02.11.2016 08:08А вот на этот вопрос у меня нет хорошего ответа.
denisei
02.11.2016 11:35Вроде бы на примере с наследованием становится ясно что с классами проще, но опять же
var lion = Object.create(new Animal('Simba')) lion.speak = function(){ Animal.prototype.speak.call(this); console.log(this.name + ' roars.'); } lion.speak(); // Simba makes a noise.
Тоже нормально читается. В общем не отпускает чувство что пример нарочно усложнен для контраста, «как все клево стало».
justboris
02.11.2016 11:50+1В вашем первом примере на каждый новый экземпляр Animal будет создан свой инстанс метода speak.
Если вынести метод в прототип, то он будет один на все экземпляры. В теории, это сэкономит вам немного памяти, если объектов много.
freetonik
Кстати, наши онлайн-курсы учат новому стандарту с самого начала, с «основ программирования» :-)
novoxudonoser
А я долго гадал думал, зачем в таком большом количестве вы выбрасываете переводы так себе статей (хотя некоторые интересны). Теперь то понятно — пиарить совой проект обучения web проганью, вот только этих сервисов развелось как грязи в последнее время, чему то там конечно обучают, но только минимальным основам и так себе (Кто умеет, тот делает; кто не умеет, тот учит — пословица такая). В итоге на итак перегретый рынок it разработки выплёскиваются люди прошедшие пару курсов с завышенным чсв и требованием 1000$ в месяц и красной ковровой дорожки чтобы просто прийти на собеседование (к сожалению это гиперболизированная но всё же реальность). Вообщем печалька.
Ofigenen
Пусть выплескиваются. За минувший год у меня на собеседовании были десятка таких «талантов» с уровнем зарплатных ожиданий от *совсем мало* (студент на парт-тайм) до *офигеть сколько* (вуз закончил или даже не начинал) тысяч. Одному, пришедшему с претензией на senior, предложили junior-позицию за вменяемую сумму. Согласился, до сих пор с нами работает.
А изначальную позицию закрыли опытным и сильным девелопером. Стоит он дорого, но такие люди на контрасте с общей массой начинают еще больше цениться.