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.
Статью я завершу уже ставшим классическим примером:
Автор: germn