Малоизвестные опасности JavaScript
Оригинальная статья: Casper Beyer — Lesser-Known JavaScript Hazards
С тех пор, как мы преодолели ECMAScript 4 Harmony, в JavaScript появилось много новых, интересных функций. В то время как больше возможностей может позволить нам создать более качественный и читаемый код, также легко упустить из виду источники потенциальных проблем. Давайте рассмотрим некоторые часто встречаемые ошибки, причина которых не всегда очевидна.
Стрелочные функции и литеральные объекты
Стрелочные функции обеспечивают более короткий синтаксис, одна из доступных функций заключается в том, что вы можете написать свою функцию в виде лямбда-выражения с неявным возвращаемым значением. Это удобно при использование функционального стиля, например, когда вы должны использовать функцию map c массивом.
Для примера:
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, undefined, undefined, undefined]
Результатом здесь будет массив, содержащий неопределенные значения. Хотя мы может ожидать, что здесь будет возвращаться объект, интерпретатор видит что-то совершенно другое. Фигурные скобки интерпретируются как область видимости стрелочной функции, и переменная value фактически становится меткой Если бы мы экстраполировали вышеприведенную стрелочную функцию на то, что фактически выполняет интерпретатор, это выглядело бы примерно так:
const numbers = [1, 2, 3, 4]; numbers.map(function(n) { value: n return; });
Обходной путь довольно хитрый. Нам просто нужно обернуть объект в круглые скобки, что превращает его в выражение вместо выражения блока, например так:
const numbers = [1, 2, 3, 4]; numbers.map(n => ({ value: n }));
Это приведет к массиву, содержащему массив объектов со значениями, которые можно ожидать.
Стрелочные функции и Bindings
Еще одна хитрость, связанная с функциями стрелок, заключается в том, что у них нет своих привязок к this, то есть их значение this будет таким же, как и значение this лексической области видимости.
Таким образом, несмотря на то, что синтаксис может быть более простым, функции стрелок не являются заменой старых добрых функций. Вы можете быстро столкнуться с ситуацией, когда ваша this будет не соответствует вашим ожиданиям.
For example:
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 здесь будет объектом калькулятора, на самом деле это приведет к тому, что this будет либо неопределенной, либо глобальным объектом в зависимости от того, выполняется код в строгом режиме или нет. Это потому, что ближайшая лексическая область действия здесь — это глобальная область действия. В строгом режиме strict mode это undefined; в противном случае это объект window в браузерах (или объект process в среде, совместимой с Node.js).
В случае использования обычных функций когда вызывается объект, this будет указывать на объект, поэтому использование обычной функции все еще является способом использования функций-членов.
let calculator = { value: 0, add(values) { this.value = values.reduce((a, v) => a + v, this.value); }, }; calculator.add([10, 10]); console.log(calculator.value);
Кроме того, поскольку стрелочная функция не имеет этой привязки, Function.prototype.call, Function.prototype.bind и Function.prototype.apply также не будут работать с ними.
Итак, в следующем примере мы столкнемся с той же проблемой, что и раньше: this является глобальным объектом, когда вызывается функция добавления сумматора, несмотря на нашу попытку переопределить ее с помощью Function.prototype.call:
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]);
Хотя стрелочные функции более аккуратны, но они не могут заменить обычные функции-члены, где требуется this.
Автоматическая вставка точки с запятой
Хотя это не новая функция, автоматическая вставка точек с запятой (ASI) является одной из более странных функций в JavaScript, поэтому ее стоит упомянуть. Теоретически, вы можете пропускать точку с запятой в большей части кода (что происходит во многих проектах). Если у вас так принято то в этом нет ничего плохого, но вы должны знать что может произойти.
Возьмите следующий пример:
return
{
value: 42
}
Можно подумать, что он вернет литерал объекта, но на самом деле он вернет неопределенное значение, потому что происходит автоматическая вставка точки с запятой, что делает его пустым оператором возврата, за которым следуют оператор блока.
Другими словами, окончательный код, который фактически интерпретируется, больше похож на следующий:
return;
{
value: 42
};
Поэтому лучше, никогда не начинайте строку с открывающей скобки или строкового литерала, даже при использовании точек с запятой, потому что ASI всегда присутствует.
Поверхностные Set
Множества это по сути массив с не повторяющими элементами. Но
множества являются поверхностными (Shallow), что означает при наличие одинаковых массивов или объектов, они будут рассматриваться как уникальные элементы.
Для примера:
let set = new Set(); set.add([1, 2, 3]); set.add([1, 2, 3]); console.log(set.size); // 2
Размер этого множества будет равен двум, что имеет смысл, если рассматривать его элементы с точки зрения ссылок, и поэтому они являются разными объектами.
Однако строки неизменяемы (immutable). И одинаковые строки не будут дублироваться:
<strong>let</strong> set = <strong>new</strong> Set(); set.add([1, 2, 3].join(',')); set.add([1, 2, 3].join(',')); console.log(set.size); // 1
В результате размер множества будет равен единице, поскольку строки неизменяемы и встроены в JavaScript, что можно использовать в качестве обходного пути, если вам необходимо сохранить набор объектов. Их можно сериализовать и десериализовать.
Классы и временная мертвая зона
В JavaScript обычные функции поднимаются в верхнюю часть лексической области (hoisting), что означает, что приведенный ниже пример будет работать так, как можно было ожидать:
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
Finally это особый случай. Взгляните на следующий фрагмент:
try {
return true;
} finally {
return false;
}
Как вы думаете, что тут возвращается? Ответ является интуитивным понятным и в то же время может стать не совсем очевидным. Можно подумать, что первый оператор return заставляет функцию действительно завершиться и вернуть стек вызовов. Но в данном случае это исключение из этого правила, потому что оператор finally так же должен всегда выполнится, поэтому вместо return true возвращается оператор return false внутри блока finally.
В заключение
JavaScript легко выучить, но трудно освоить. Другими словами, он подвержен ошибкам и не очевидному поведению, если разработчик не заботится о том, что и почему он что-то делает.
Это особенно верно для ECMAScript 6 и его новых функций. В частности, особенности поведения есть в стрелочных функциях по. Если бы я сделал предположение, я бы сказал, что это потому, что разработчики рассматривают их как синтаксическая замена обычных функций. Но они не являются обычными функциями и не могут их заменить.
Чтение спецификации время от времени не повредит. Это не самое захватывающее чтиво в мире, но с точки зрения других спецификаций он не так уж и плох.
Такие инструменты, как AST Explorer, также помогают пролить свет на то, что происходит в некоторых из этих сложных случаев. Люди и компьютеры склонны видеть вещи по-разному.
С учетом сказанного я оставлю вас с последним примером в качестве упражнения.
Несколько вопросов на закрепление материала
Вопросы по JavaScript