Магия на кончиках пальцев

Тёмная сторона JavaScript

Баги и малоизвестные особенности JavaScript
Публикация от
Издание от 21 Apr 2016
#js #bugs

typeof null

Это оффициально признанный баг 6, не фиксищейся для поддержки совместимости со старым кодом 1 2. typeof null возвращает "object", однако null не является объектом 6, и что хуже, не ведёт себя как объект.

typeof null     // "object"

var z = null;   // null
z.prop = 1      // TypeError: z is null

Если мы заглянем в код библиотеки underscore.js 5, то увидим такую функцию как isObject(), как раз правильно проверяющую на тип объекта.

// версия 1.6.0 (2014)
_.isObject = function(obj) {
    return obj === Object(obj);
};

// версия 1.8.3 (2015)
// соответствует lodash
_.isObject = function(obj) {
    var type = typeof obj;
    return type === 'function' || type === 'object' && !!obj;
};

_.isObject(null)    // false

А также функцию isNull() проверяющую на соответствие null (тоже самое в lodash):

_.isNull = function(obj) {
    return obj === null;
};

_.isNull(null)      // true
_.isNull({})        // false
_.isNull(undefined) // false
_.isNull(NaN)       // false

Что есть NaN?

Возможно вы не знаете, но NaN является числом 3 (и не важно, что NaN акроним от Not a Number), просто это число никогда не равно самому себе 157.

typeof NaN  // "number"

NaN === NaN // false

В underscore.js есть пример проверки на NaN 5 (тоже самое в lodash):

_.isNaN = function(obj) {
    return toString.call(obj) === '[object Number]' && obj !== +obj;
};

_.isNaN(NaN)    // true

Есть также встроенная функция isNaN(), но она работает некорректно 7:

isNaN(NaN)          // true
isNaN(undefined)    // true
isNaN({})           // true
isNaN(new Date().toString())   // true
isNaN("words")      // true

В новом стандарте ECMAScript 6 (Harmony) предложена новая функция Number.isNaN() избавленная от этих недостатков. Вот её полифилл 8:

Number.isNaN = Number.isNaN || function(value) {
  return typeof value === 'number' && isNaN(value);
}

Операторы сложения и вычитания

Преобразование типов в JavaScript реализовано по-разному для операторов сложения и вычитания:

[]+[]   // ""
[]+{}   // "[object Object]"

[]-[]   // 0
[]-{}   // NaN

var l = ['a','b','c']
var o = {'q':1, 'w':2, 'e':3}

l+l     // 'a,b,ca,b,c'
l-l     // NaN
o+o     // '[object Object][object Object]'
o-o     // NaN

var L = [22]

L-[]    // 22
[]-L    // -22
+L      // 22
L+L     // '2222'
L-L     // 0
L+[]    // '22'
-L      // -22
100-L   // 78
100+L   // '10022'

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

Арифметика null

Преобразования осуществляемые при арифметических операциях и сравнениях > >= < <=, и при проверке равенства == -- различны. Алгоритм проверки равенства для undefined и null в спецификации прописан отдельно 4. В нём считается, что они равны между собой, но эти значения не равны никакому другому значению 9.

Из-за этого null ведёт себя странно при сравнении:

> null >= 0
true

> null <= 0
true

> null > 0
false

> null < 0
false

> null == 0
false

А значение undefined вообще «несравнимо»:

> undefined > 0
false

> undefined == 0
false

> undefined < 0
false

> undefined >= 0
false

> undefined <= 0
false

Пустой блок кода

Пустой блок кода {} в одних случаях может вести себя как null, а в других -- как "ничего", хотя сам по себе интерпритируется как undefined.

> undefined
undefined

> {}
undefined

> {}+1    // {} -- null?
1

> undefined+1
NaN

> {}+'1'    // {} -- wtf??!
1

> undefined+'1'
"undefined1"

> {}+null    // {} -- null?
0

> undefined+null
NaN

Обход массива

Существует несколько способов обойти массив. Одни из них обходят массив как массив, а другие как объект 10.

var list = []
list[0] = "x"
list[100] = "y"
list[10000] = "z"

> list.forEach(function(item) { console.log(item) })
x
y
z

> for (var i = 0; i < list.length; i++) { console.log(item) }
x
undefined
...
undefined
y
undefined
...
undefined
z

> for (var key in list) {
    if (
        // Здесь мы проверяем что ключ взят не из цепочки прототипов
        list.hasOwnProperty(key) &&
        // Тут, что это числовой ключ, поскольку, хотя разряженный
        // массив практически не отличается от объекта мы по прежнему
        // используем только числовые ключи
        /^0$|^[1-9]\d*$/.test(key) &&
        // Здесь мы проверяем что ключ не выходит за пределы допустимые
        // для ключей массива 2^32 - 2. Это число, согласно спецификации,
        // максимальный из возможных индексов массива
        key <= 4294967294
    ) {
        console.log(list[key])
    }
}
x
y
z