Подводные камни JavaScript

в 8:08, , рубрики: Новости, метки:

Мне очень нравится JavaScript и я считаю его мощным и удобным. Но для большинства начинающих JS-программистов, много проблем создаёт недопонимание аспектов языка. Часто конструкции языка ведут себя «нелогично». В данной статье я хочу привести примеры «граблей», на которые я наступил; объяснить поведение языка и дать пару советов.

Типы

Как написано в спецификации ECMAScript, всего существует 6 типов:

  1. Undefined
  2. Null
  3. Boolean
  4. String
  5. Number
  6. Object

Все значения должны принадлежать к ним. В JS есть оператор typeof, который, как казалось бы, должен возвращать тип объекта. Казалось бы, один из перечисленных. Что получается на самом деле:

  typeof 5;             //"number",        ок, похоже на правду
  typeof "hello";       //"string" 
  typeof true;          //"boolean" 
  typeof undefined;     //"undefined"
  typeof {};            //"object".        Пока 5 из 5
  typeof null;          //"object".        WTF?
  typeof function(){};  //"function".      Разве у нас есть тип function?

Проблема: несмотря на то, что тип у null — Null, оператор возвращает 'object'; а тип у функции — Object, оператор возвращает 'function', а такого типа нет.
Объяснение: typeof возвращает не тип, а строку, которая зависит от аргумента и не является именем типа.
Совет: забудьте про типы. Серьезно, я считаю что знание 6 типов JS не даст вам пользы, а оператор typeof используется довольно часто, поэтому лучше запомнить результаты его работы:

Тип аргумента Результат
Undefined undefined
Null object
Boolean boolean
Number number
String string
Object (результаты оператора new, inline-объекты ({key: value})) object
Object (функции) function

Магические значения: undefined, null, NaN

В спецификации описаны так:

  • undefined value — primitive value used when a variable has not been assigned a value
  • Undefined type — type whose sole value is the undefined value
  • null value — primitive value that represents the intentional absence of any object value
  • Null type — type whose sole value is the null value
  • NaN — number value that is a IEEE 754 “Not-a-Number” value

У себя в голове я держу следующее:

  • undefined — значение переменной, которая не была инициализирована. Единственное значение типа Undefined.
  • null — умышленно созданный «пустой» объект. Единственное значение типа Null.
  • NaN — специальное значение типа Number, для выражения «не чисел», «неопределенности». Может быть получено, например, как результат деления 0 на 0 (из курса матанализа помним, что это неопределенность, а деление других чисел на 0 — это бесконечность, для которой в JS есть значения Infinity).

С этими значениями я обнаружил много «магии». Для начала, булевы операции с ними:

  !!undefined; //false
  !!NaN; //false
  !!null; //false
  //как видим, все 3 значения при приведении к boolean дают false

  null == undefined; //true

  undefined === undefined; //true
  null === null; //true

  NaN == undefined; //false
  NaN == null; //false

  NaN === NaN; //false!
  NaN == NaN; //false!

Проблема: с чем бы мы ни сравнивали NaN, результатом сравнения всегда будет false.
Объяснение: NaN может возникать в результате множества операций: 0/0, parseInt('неприводимая к числу строка'), Math.sqrt(-1) и было бы странно, если корень из -1 равнялся 0/0. Именно поэтому NaN !== NaN.
Совет: не использовать булевы операторы с NaN. Для проверки нужно использовать функцию isNaN.

  typeof a; //'undefined'
  a; //ReferenceError: a is not defined

Проблема: оператор typeof говорит нам, что тип необъявленной переменной — undefined, но при обращении к ней происходит ошибка.
Объяснение: на самом деле, есть 2 понятия — Undefined и Undeclared. Так вот, необъявленная переменная является Undeclared-переменной и обращение к ней вызывает ошибку. Объявленная, но не инициализированная переменная принимает значение undefined и при обращении к ней ошибок не возникает.
Совет: перед обращением к переменной, вы должны быть уверенны, что она объявлена. Если вы обратитесь к Undeclared-переменной, то код, следующий за обращением, не будет выполнен.

  var a; //вновь объявленная переменная, для которой не указано значение, принимает значение undefined
  console.log(undefined); //undefined
  console.log(a); // undefined
  a === undefined; //true
  undefined = 1;
  console.log(undefined); //1
  a === undefined; //false

Проблема: в любой момент мы можем прочитать и записать значение undefined, следовательно, кто-то может перезаписать его за нас и сравнение с undefined будет некорректным.
Объяснение: undefined — это не только значение undefined типа Undefined, но и глобальная переменная, а значит, любой может её переопределить.
Совет: просто сравнивать переменные с undefined — плохой тон. Есть 3 варианта решения данной проблемы, для создания «пуленепробиваемого» кода.

  • Вы можете сравнивать не значение переменной, а её тип: «typeof a === 'undefined'».
  • Использовать паттерн immediately-invoked function:

  (function(window, undefined){
    //т.к. второй аргумент не был передан, значение переменной undefined будет «правильным».
  }(this));

  • Для получения реального «undefined»-значения можно использовать оператор void (кстати, я не знаю другого применения этому оператору):

  typeof void(0) === 'undefined' // true

Теперь попробуем совершить аналогичные действия с null:

  console.log(null); //null
  null = 1; //ReferenceError: Invalid left-hand side in assignment

Проблема: несмотря на некоторые сходства между null и undefined, null мы перезаписать не можем. На самом деле проблема не в этом, а в том, что язык ведёт себя нелогично: даёт перезаписать undefined, но не даёт перезаписать null.
Объяснение: null — это не глобальная переменная и вы не можете её создать, т. к. null — зарезервированное слово.
Совет: в JavaScript не так много зарезервированных слов, проще их запомнить и не использовать как имена переменных, чем вникать, в чём проблема, когда она возникнет.

И теперь сделаем тоже самое с NaN:

  console.log(NaN); //NaN
  NaN = 1;
  console.log(NaN); //NaN
  isNaN(NaN); //true

Проблема: при переопределении undefined всё прошло успешно, при переопределении null возникла ошибка, а при переопределении NaN операция не вызвала ошибки, но свойство не было переопределено.
Объяснение: нужно понимать, что NaN — переменная глобального контекста (объекта window). Помимо этого, к NaN можно «достучаться» через Number.NaN. Но это неважно, ниодно из этих свойств вы не сможете переопределить, т. к. NaN — not writable property:

  Object.getOwnPropertyDescriptor(window, NaN).writable; //false
  Object.getOwnPropertyDescriptor(Number, NaN).writable; //false

Совет: как JS-программисту, вам нужно знать об атрибутах свойств:

Атрибут Тип Смысл
enumerable Boolean Если true, то данное свойство будет участвовать в циклах for-in
writable Boolean Если false, то значение этого свойства нельзя будет изменить
configurable Boolean Если false, то значение этого свойства нельзя изменить, удалить и изменить атрибуты свойства тоже нельзя
value Любой Значение свойства при его чтении
get Object (или Undefined) функция-геттер
set Object (или Undefined) функция-сеттер

Вы можете объявлять неудаляемые или read-only свойства и для созданных вами объектов, используя метод Object.defineProperty:

  var obj = {};
  Object.defineProperty(obj, 'a', {writable: true,  configurable: true,  value: 'a'});
  Object.defineProperty(obj, 'b', {writable: false, configurable: true,  value: 'b'});
  Object.defineProperty(obj, 'c', {writable: false, configurable: false, value: 'c'});

  console.log(obj.a); //a
  obj.a = 'b';
  console.log(obj.a); //b
  delete obj.a; //true

  console.log(obj.b); //b
  obj.b = 'a';
  console.log(obj.b); //b
  delete obj.b; //true

  console.log(obj.c); //c
  obj.b = 'a';
  console.log(obj.c); //c
  delete obj.b; //false

Работа с дробными числами

Давайте вспомним 3-й класс и сложим несколько десятичных дробей. Результаты сложения в уме проверим в консоли JS:

  0.5 + 0.5; //1
  0.5 + 0.7; //1.2
  0.1 + 0.2; //0.30000000000000004;
  0.1 + 0.7; //0.7999999999999999;
  0.1 + 0.2 - 0.2; //0.10000000000000003

Проблема: при сложении некоторых дробных чисел, выдаётся арифметически неверный результат.
Объяснение: такие результаты получаются из-за особенностей работы c числами с плавающей точкой. Это не является особенностью JavaScript, другие языки работают также (я проверил в PHP, Python и Ruby).
Совет: во-первых, вы, как программист, обязаны знать об особенностях работы компьютера с числами с плавающей точкой. Во-вторых, в большинстве случаев достаточно просто округлять результаты. Но, если вдруг необходимо выдавать пользователю точный результат, например, при работе с данными о деньгах, вы можете просто умножать все аргументы на 10 и результат делить обратно на 10, например так:

  function sum() {
    var result = 0;
    for (var i = 0, max = arguments.length; i< max; i++ ) {
      result += arguments[i]*10;
    }
    return result / 10;
  }
  sum(0.5, 0.5); //1
  sum(0.5, 0.7); //1.2
  sum(0.1, 0.2); //0.3
  sum(0.1, 0.7); //0.8
  sum(0.1, 0.2, -0.2); //0.1

Вывод

Это только несколько необычных примеров с непредсказуемым результатом. Если помнить о них, то получится не наступить на те же грабли и быстро понять, в чём проблема. Если найдёте новый «нелогичный» кусок кода, то попробуйте осознать, что происходит с точки зрения языка, почитав спецификацию или MDN.

Автор: KELiON

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js