Перлы языка JavaScript: строки, массивы и непонятные моменты

Возвращаясь к «косякам», JavaScript строки, массивы и разномастные формулы также оказались под прицелом Брайана Лера в его лекции «WTFJS».

Мы уже много раз говорили о многофункциональности JavaScript, но все еще остаются некоторые моменты которые не до конца ясны. К ним относятся логические несостыковки в математических уравнениях, Arrays и многом другом. С одной стороны может показаться, что создатели JavaScript не учли наличие и решение таких проблем. Давайте же заглянем в спецификации и посмотрим, где действительно скрывается причина подобных моментов.

Минимальное значение больше нуля

Number.MIN_VALUE – наименьшее число, которое больше нуля:

Number.MIN_VALUE> 0 // -> true

Объяснение:

Number.MIN_VALUE = 5e-324, т. е. наименьшее положительное число, которое может быть представлено с точностью до float – как можно ближе к нулю.

Теперь самое маленькое значение – Number.NEGATIVE_INFINITY, хотя оно не является действительно числовым в строгом смысле.

Функция – это не функция

Ошибка, присутствующая в V8 v5.5 или ниже (Node.js <= 7).

Как насчет такого? Посмотрите на эти JavaScript строки:

// Объявить класс, который экстендит значение null

class Foo extends null {}

// -> [Function: Foo]



new Foo instanceof null

// > TypeError: функция - не функция

Объяснение:

Это не является частью спецификации, а просто ошибка, которая теперь исправлена, поэтому в будущем с ней не должно быть проблем.

Добавление массивов

Что произойдет, если вы попытаетесь вывести сумму двух массивов?

[1, 2, 3] + [4, 5, 6] // -> '1,2,34,5,6'

Объяснение:

Конкатенация происходит. Если разбить ее на поэтапную модель, то выглядит все следующим образом:

[1, 2, 3] + [4, 5, 6]

// вызываем toString () (JavaScript строки)

[1, 2, 3] .toString () + [4, 5, 6] .toString ()

// конкатенация

'1,2,3' + '4,5,6'

// ->

'1,2,34,5,6'

Массив с плавающей запятой

Вы создали массив с четырьмя пустыми элементами. Несмотря на это, вы получите 3 элемента из-за плавающей запятой:

let а = [,,,]

a.length // -> 3

a.toString () // -> ',,'

Объяснение:

Трейлинг-запятые (также называемые «финальными запятыми») могут быть полезны при добавлении новых элементов, параметров или свойств в код JS. Хотите добавить новое свойство? Просто объявите новую строку без изменения предыдущей, если данная строка уже использует запятую. Такой подход значительно упростит работу, ведь само редактирование кода станет менее проблематичным.

Равномерность массива – монстр

Равномерность массива – это настоящий монстр в мире JS. Смотрите сами:

[] == '' // -> true

[] == 0 // -> true

[''] == '' // -> true

[0] == 0 // -> true

[0] == '' // -> false

[''] == 0 // -> true



[Null] == '' // true

[Null] == 0 // true

[Undefined] == '' // true

[Undefined] == 0 // true



[[]] == 0 // true

[[]] == '' // true



[[[[[[]]]]]] == '' // true

[[[[[[]]]]]] == 0 // true



[[[[[[Null]]]]]] == 0 // true

[[[[[[Null]]]]]] == '' // true



[[[[[Undefined]]]]]] == 0 // true

[[[[[Undefined]]]]]] == '' // true

Объяснение:

Вы должны быть очень осторожны! Это сложный пример, но он описан в спецификации 7.2.13. Абстрактное сравнение равенства.

undefined и Number

Если мы не передадим аргументы в конструктор Number, то получим 0. Значение undefined присваивается формальным аргументам, когда нет реальных, поэтому вполне ожидаемо, что Number без аргументов принимает значение undefined. Если же это неопределенность, мы однозначно получим результат в виде NaN.

Number () // -> 0

Number (undefined) // -> NaN

Объяснение:

Согласно спецификации:

  1. Если аргументы этой функции не были переданы, пусть n будет равно +0.
  2. Иначе что произойдет с n? ToNumber (value).
  3. В случае с undefined, ToNumber (undefined) должен возвращать NaN.

Также рекомендуем изучить соответствующие разделы:

parseInt – плохой парень

parseInt славится своими причудами:

parseInt ( 'е*ск'); // -> NaN

parseInt ('f*ck', 16); // -> 15

Объяснение:

Это происходит потому, что parseInt будет производить посимвольный синтаксический разбор до тех пор, пока не наткнется на символ, которого он не знает. f в 'f *ck' является шестнадцатеричной цифрой 15.

Парсинг Infinity в данной ситуации представляет собой нечто подобное:

//

parseInt ('Infinity', 10) // -> NaN

// ...

parseInt ('Infinity', 18) // -> NaN ...

parseInt ('Infinity', 19) // -> 18

// ...

parseInt ('Infinity', 23) // -> 18 ...

parseInt ('Infinity', 24) // -> 151176378

// ...

parseInt ('Infinity', 29) // -> 385849803

parseInt ('Infinity', 30) // -> 13693557269

// ...

parseInt ('Infinity', 34) // -> 28872273981

parseInt ('Infinity', 35) // -> 1201203301724

parseInt ('Infinity', 36) // -> 1461559270678 ...

parseInt ('Infinity', 37) // -> NaN

Также будьте осторожны нулевым значением:

parseInt (null, 24) // -> 23

Объяснение:

Да, JavaScript строки нередко ведут себя крайне странно: привыкайте. Приведенный выше пример конвертирует значение null в строку «null» и пытается его преобразовать. В интервале от 0 до 23 нет цифр, которые он может преобразовать, поэтому он возвращает NaN.

Не забывайте и о восьмеричных значениях:

parseInt ('06'); // 6

parseInt ('08'); // 8 поддерживается ECMAScript 5

parseInt ('08'); // 0, не поддерживается ECMAScript 5

Объяснение:

Если входная строка начинается с «0», то все зависит от поддержки ECMAScript 5, что возможно не во всех современных браузерах.

parseInt всегда конвертирует ввод в строку:

parseInt ({toString: () => 2, valueOf: () => 1}) // -> 2

Number ({toString: () => 2, valueOf: () => 1}) // -> 1

Математика с true и false

Давайте сгенерируем небольшой математический пример:

True + true // -> 2

(True + true) * (true + true) - true // -> 3

Хммм...

Объяснение:

Мы можем закреплять за значениями числа с помощью конструктора Number. Совершенно очевидно, что true будет равно единице:

Number (true) // -> 1

Оператор унарного плюса пытается преобразовать его значение в число. Он может преобразовывать JavaScript строки, с помощью которых реализовано представление целых чисел, и float. А еще, как видим, это могут быть значения, отличные от строки: true, false и null. Если он не может разобрать конкретное значение, в результате будет выдан NaN. Это означает, что мы можем все упростить:

+ true // -> 1

Когда вы выполняете сложение или умножение, вызывается метод ToNumber. Согласно спецификации, этот метод возвращает:

  1. Если аргумент равен true, возвращается 1.
  2. Если аргумент false, возвращается +0.

Вот почему мы можем добавлять логические значения в виде регулярных чисел и получать правильные результаты.

Соответствующие разделы по теме:

HTML-комментарии возможны в JavaScript

Вы удивитесь, но <!-- (который известен как комментарий HTML) является допустимым комментарием в JavaScript.

// действительный комментарий

<!-- тоже действительный комментарий

Объяснение:

Впечатлены? HTML-подобные комментарии предназначены для того, чтобы браузеры, которые не понимают тег <script>, быстрее загибались. К таким можно отнести Netscape 1.x, который давно перестал быть популярным. Поэтому нет смысла добавлять HTML-комментарии в теги скриптов.

Поскольку Node.js основан на движке V8, комментарии, подобные HTML, также поддерживаются средой выполнения Node.js. Более того, они являются частью спецификации:

NaN не является числом

Тип NaN – это число:

typeof NaN // -> 'number'

Объяснения того, как работают операторы typeof и instanceof:

[] И null – объекты

typeof [] // -> 'object'

typeof null // -> 'object'



// Однако

Null instanceof Object // false

Объяснение:

Поведение оператора typeof подробно разъясняется в этом разделе спецификации:

Согласно спецификации, оператор typeof возвращает JavaScript строки в соответствии с таблицей 35: typeof Operator Results. Для нулевых, обычных и многих других объектов, которые не реализовывают [[Call]], он возвращает строку «object».

Тем не менее, вы можете проверить тип объекта с помощью метода toString.

Object.prototype.toString.call ([])

// -> '[object Array]'



Object.prototype.toString.call (новая дата)

// -> '[object Date]'



Object.prototype.toString.call (нуль)

// -> '[object Null]'

Магически увеличивающиеся числа

999999999999999 // -> 999999999999999

9999999999999999 // -> 10000000000000000



10000000000000000 // -> 10000000000000000

10000000000000000 + 1 // -> 10000000000000000

10000000000000000 + 1.1 // -> 10000000000000002

Объяснение:

Это вызвано стандартом IEEE 754-2008 для двоичной системы с плавающей запятой. В данном масштабе все округляется до ближайшего четного числа. Прочитайте больше:

Точность 0.1 + 0.2

Известная шутка. Добавление 0.1 и 0.2 является смертельно точным:

0.1 + 0.2 // -> 0.30000000000000004

(0.1 + 0.2) === 0.3 // -> false

Объяснение:

Ответ на вопрос «Связана ли эта проблема с плавающей запятой?» на StackOverflow:

Константы 0.2 и 0.3 в вашей программе также будут приближаться к их истинным значениям. Бывает, что ближайший double к 0.2 больше, чем рациональное число 0.2, но ближайший double к 0.3 меньше рационального числа 0.3. Сумма 0.1 и 0.2 больше, чем рациональное число double и, следовательно, не согласуется с константой в вашем коде.

Эта проблема настолько известна, что есть даже сайт под названием 0.30000000000000004.com. Такое встречается в каждом языке, который использует математику с плавающей запятой, а не только JavaScript.

Патч-номера

Вы можете добавить свои собственные методы для обертки таких объектов, как Number или String.

Number.prototype.isOne = function () {

return Number (this) === 1

}



1.0.isOne () // -> true

1..isOne () // -> true

2.0.isOne () // -> false

(7) .isOne () // -> false

Объяснение:

Очевидно, вы можете экстендить объект Number, как и любой другой объект в JavaScript. Однако не рекомендуется это делать, если поведение определенного метода не является частью спецификации. Вот список свойств Number:

Сравнение трех чисел

1 <2 <3 // -> true

3> 2> 1 // -> false

Объяснение:

Почему так? Ну, проблема в первой части выражения. Вот как это работает:

1 <2 <3 // 1 <2 -> true

true <3 // true -> 1

1 <3 // -> true



3> 2> 1 // 3> 2 -> true

true > 1 // true -> 1

1> 1 // -> false

Мы можем исправить это с помощью оператора больше или равно (> =):

3> 2> = 1 // true

Подробнее о реляционных операторах в спецификации:

Смешная математика

Часто результаты арифметических операций в JavaScript могут быть довольно неожиданными. Рассмотрим следующие примеры:

3  - 1  // -> 2
 3  + 1  // -> 4
'3' - 1  // -> 2
'3' + 1  // -> '31'

'' + '' // -> ''
[] + [] // -> ''
{} + [] // -> 0
[] + {} // -> '[object Object]'
{} + {} // -> '[object Object][object Object]'

'222' - -'111' // -> 333

[4] * [4]       // -> 16
[] * []         // -> 0
[4, 4] * [4, 4] // NaN

Объяснение:

Что происходит в первых четырех примерах? Вот небольшая таблица, чтобы понять принцип добавления в языке JavaScript:

Number  + Number  -> addition
Boolean + Number  -> addition
Boolean + Boolean -> addition
Number  + String  -> concatenation
String  + Boolean -> concatenation
String  + String  -> concatenation

Как насчет других примеров? Методы ToPrimitive и ToString неявно вызываются для [] и {} перед добавлением. Подробнее о процессе оценки в спецификации:

JavaScript строки не являются экземплярами String

'str' // -> 'str'

typeof 'str' // -> 'string'

'str' instanceof String // -> false

Объяснение:

Конструктор возвращает строку:

typeof String ('str') // -> 'string'

String ('str') // -> 'str'

String ('str') == 'str' // -> true

Попробуем с новым кодом:

new String ('str') == 'str' // -> true

typeof new String ('str') // -> 'object'

JavaScript строки – объект? Что это?

new String ('str') // -> [String: 'str']

Дополнительная информация о конструкторе String в спецификации:

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ

eFusion
01 марта 2020

ТОП-15 книг по JavaScript: от новичка до профессионала

В этом посте мы собрали переведённые на русский язык книги по JavaScript – ...
admin
10 июня 2018

Лайфхак: в какой последовательности изучать JavaScript

Огромный инструментарий JS и тонны материалов по нему. С чего начать? Расск...