JavaScript уже который год дополняется новыми возможностями и синтаксическим сахаром. Но в погоне за прогрессом легко не заметить яму под ногами.
В этой статье мы поговорим о малоизвестных, но периодически встречаемых на практике ловушках языка.
Стрелочные функции и литералы объектов
Стрелочные функции позволяют записать функцию короче и зачастую нагляднее. Это особенно удобно при работе в функциональном стиле.
Например, такой код:
const numbers = [1, 2, 3, 4];
numbers.map(function(n) {
return n * n;
});
Можно записать как:
const numbers = [1, 2, 3, 4];
numbers.map(n => n * n);
Результат выполнения предсказать несложно: [1, 4, 9, 16].
Но дело обстоит не так радужно, когда мы пытаемся работать с объектами:
const numbers = [1, 2, 3, 4];
numbers.map(n => { value: n });
Результатом выполнения будет массив из undefined. Хотя по началу может показаться, что стрелочная функция возвращает объекты, интерпретатор видит ситуацию иначе. Фигурные скобки воспринимаются языком как тело функции, а value как label. Короче говоря, вот эквивалент кода выше:
const numbers = [1, 2, 3, 4];
numbers.map(function(n) {
value:
n
return;
});
К счастью, обойти проблему несложно. Достаточно лишь использовать круглые скобки:
const numbers = [1, 2, 3, 4];
numbers.map(n => ({ value: n }));
Теперь всё работает, как планировалось, но помнить об этом приходится постоянно.
Стрелочные функции и this
Ещё одна особенность стрелочных функции заключается в отсутствии «своего» this. Это приводит к тому, что this внутри стрелочной функции — это this внешней лексической области.
Так что далеко не всегда можно заменить обычную функцию на стрелочную без проблем. Например:
let calculator = {
value: 0,
add: (values) => {
this.value = values.reduce((a, v) => a + v, this.value);
},
};
calculator.add([1, 2, 3]);
console.log(calculator.value);
this здесь будет не объектом калькулятора, а undefined в strict режиме или глобальным объектом в обычном. Глобальный объект будет разным для разного окружения — объект окна в браузере или объект процесса в Node.js.
Сравните код выше с кодом использующим обычную функцию:
let calculator = {
value: 0,
add(values) {
this.value = values.reduce((a, v) => a + v, this.value);
},
};
calculator.add([10, 10]);
console.log(calculator.value);
Результат — 20
Кстати, по причине отсутствия своего this стрелочная функция не будет работать с Function.prototype.call, Function.prototype.bind, и Function.prototype.apply. Переменная создаётся при объявлении и не может быть перезаписана:
const adder = {
add: (values) => {
this.value = values.reduce((a, v) => a + v, this.value);
},
};
let calculator = {
value: 0
};
adder.add.call(calculator, [1, 2, 3]);
console.log(calculator.value);
Результат — 0
Так что при всей своей красоте стрелочные функции не смогут заменить настоящие там, где нужно работать с this.
Авто добавление точки с запятой
Автоматическое добавление точек с запятой (ASI) хотя и появилось уже давно, всё ещё заслуживает упоминания в статье о подводных камнях. Теоретически, вы можете не ставить точку с запятой в большинстве случаев (как многие и поступают). На практике, следует помнить, что это фича, и использующий её код может вести себя обманчиво.
Давайте рассмотрим такой пример:
return
{
value: 42
}
Возвращает объект, верно? А вот и нет: код вернёт undefined, потому что точка с запятой будет добавлена сразу после return.
Вот что будет выполнять интерпретатор на самом деле:
return;
{
value: 42
};
Чтобы не попасться в ловушку, никогда не начинайте строку с открывающейся скобки или литерала шаблонной строки, даже когда проставляете точки с запятыми вручную, так как ASI от этого работать не перестанет.
«Неглубокие» множества
Множества являются «неглубокими», т.е. дублирующими разные массивы и объекты, даже если те равны по значению.
Например:
let set = new Set();
set.add([1, 2, 3]);
set.add([1, 2, 3]);
console.log(set.size);
Вернёт 2, так как было добавлено два разных (хоть и равных) массива.
Но для неизменяемых объектов результат будет другим:
let set = new Set();
set.add([1, 2, 3].join(','));
set.add([1, 2, 3].join(','));
console.log(set.size);
Вернёт 1, так как строки неизменяемы и встроены в JavaScript.
Классы и «поднятие»
В JavaScript функции «поднимаются» (hoisted) к началу внешней лексической области, поэтому такой код будет работать:
let segment = new Segment();
function Segment() {
this.x = 0;
this.y = 0;
}
Но с классами дело обстоит иначе. Они должны быть объявлены до момента использования, а иначе, как в примере ниже, код вернёт ошибку:
let segment = new Segment();
class Segment {
constructor() {
this.x = 0;
this.y = 0;
}
}
Результатом будет ReferenceError.
Finally
Взгляните на этот код:
try {
return true;
} finally {
return false;
}
Какое значение он вернёт? Разным людям интуиция может дать разный ответ. В JavaScript блок finally выполняется всегда, поэтому вернётся false.
Заключение
JavaScript легко выучить, трудно понять и невозможно забыть. Разработчик всегда должен быть начеку с языком, и это только актуальнее для ECMAScript 6 со всеми его новыми возможностями.
Чтобы тренировать интуицию можно время от времени почитывать спецификацию или разбирать неочевидные конструкции в AST Explorer.
Статью я завершу уже ставшим классическим примером:
Комментарии (10)
Aingis
10.01.2020 16:15+1Неочевидные моменты для тех, кто не прочитал ни одного учебника или справочной информации, где всё это написано чуть ли не на первых страницах. Даже если загуглить те же стрелочные функции, первой же ссылкой идёт подробное описание на MDN, где затронуты все эти моменты.
ReklatsMasters
10.01.2020 16:49+1Подписался на хаб javascript.
Ожидание: статьи про архитектуру и производительность
Реальность: поток статей про скобочки, запятые и thisAingis
10.01.2020 17:48Про производительность было около 8 лет назад много статей. Хотя скорее не на Хабре, наверное, и больше англоязычных. Весь этот performance. Вспоминается рассказ про нутро V8 Вячеслава Егорова, но он уже подустарел. Не совсем про производительность, но есть фундаментальная статья: «Принципы работы современных веб-браузеров».
Про архитектуру вообще почти ничего нет. Кроме доклада «Масштабируемая архитектура фронтенда» Романа Дворнова и посоветовать нечего.
sbnur
10.01.2020 18:15Перевод, скажем, не слишком качественный (с отсебятиной)
Например, в оригинале в самом конце — "With that said, I’ll leave you with this final example as an exercise", в переводе — "Статью я завершу уже ставшим классическим примером"
И так далее, если пройтись по тексту.
Возникает мысль, а является ли переводчик специалистом по теме предмета перевода.
Может отсюда и замечания, сделанные выше
v1000
11.01.2020 21:24самое смешное, когда подобные баги скурпулезно переносят из одной версии языка в другую. И их нельзя «взять и починить», потому что сломается множество программ, с заточенными на эти баги костылями, особенно те, в которых эти баги стали фичами.
PaulZi
11.01.2020 22:07Говорить про то, что this в стрелочной функции указывает на родительский скоп — малоизвестным подводным камнем, как говорить, что после case надо делать break.
aol-nnov
> Статью я завершу
про банан забавнее, имхо :)