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

Spread the love

Оригинальная статья: Casper BeyerLesser-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

1. Что будет в консоли?

function sayHi() {
  console.log(name);
  console.log(age);
  var name = "Lydia";
  let age = 21;
}

sayHi();
 
 
 
 

2. Что будет в консоли?

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 1);
}

for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 1);
}
 
 
 

3. Что будет в консоли?

const shape = {
  radius: 10,
  diameter() {
    return this.radius * 2;
  },
  perimeter: () => 2 * Math.PI * this.radius
};

shape.diameter();
shape.perimeter();
 
 
 
 

4. Каким будет результат?

const obj = { 1: "a", 2: "b", 3: "c" };
const set = new Set([1, 2, 3, 4, 5]);

obj.hasOwnProperty("1");
obj.hasOwnProperty(1);
set.has("1");
set.has(1);
 
 
 
 

Вопрос 1 из 4

Была ли вам полезна эта статья?
[10 / 4.2]

Spread the love
Подписаться
Уведомление о
guest
0 Комментарий
Inline Feedbacks
View all comments