[Перевод] Малоизвестные подводные камни JavaScript

ffb923d4333933384005d440284870f2.png

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 внешней лексической области.

Так что далеко не всегда можно заменить обычную функцию на стрелочную без проблем. Например:

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.


Взгляните на этот код:

try {
  return true;
} finally {
  return false;
}

Какое значение он вернёт? Разным людям интуиция может дать разный ответ. В JavaScript блок finally выполняется всегда, поэтому вернётся false.


JavaScript легко выучить, трудно понять и невозможно забыть. Разработчик всегда должен быть начеку с языком, и это только актуальнее для ECMAScript 6 со всеми его новыми возможностями.

Чтобы тренировать интуицию можно время от времени почитывать спецификацию или разбирать неочевидные конструкции в AST Explorer.

Статью я завершу уже ставшим классическим примером:


4592017e67435146d1d4471461170db7.gif

© Habrahabr.ru