habrahabr

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

  • суббота, 11 января 2020 г. в 00:22:23
https://habr.com/ru/post/483464/
  • JavaScript
  • Программирование



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.callFunction.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.


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